Files
2025-11-19 12:40:54 +01:00

183 lines
4.4 KiB
Go

package mock_server
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
)
type Token struct {
Name string `json:"name"`
Token string `json:"token"`
ExpiryDate int64 `json:"expiryDate"`
Permissions []string `json:"permissions"`
}
type MockBitbucketServer struct {
Mu sync.Mutex
Tokens map[string][]Token // key = "project/repo"
Server *http.Server
URL string
}
func NewMockBitbucketServer() *MockBitbucketServer {
m := &MockBitbucketServer{
Tokens: make(map[string][]Token),
}
mux := http.NewServeMux()
// ---------------------------------------------------------------------
// ONE handler, dispatching by HTTP method (GET / PUT / DELETE)
// ---------------------------------------------------------------------
mux.HandleFunc("/rest/access-tokens/latest/projects/", func(w http.ResponseWriter, r *http.Request) {
// Debug log incoming requests to help troubleshoot test failures.
fmt.Printf("[mock] %s %s\n", r.Method, r.URL.Path)
if r.Method == http.MethodPut {
body, _ := io.ReadAll(r.Body)
fmt.Printf("[mock] body: %s\n", string(body))
r.Body = io.NopCloser(bytes.NewReader(body))
}
parts := strings.Split(r.URL.Path, "/")
// Expected for LIST and CREATE:
// ["","rest","access-tokens","latest","projects",p,"repos",r]
//
// Expected for DELETE:
// ["","rest","access-tokens","latest","projects",p,"repos",r,token]
if len(parts) < 8 {
http.Error(w, "bad path", http.StatusBadRequest)
return
}
// parts layout: ["", "rest", "access-tokens", "latest", "projects", <project>, "repos", <repo>, ...]
project := parts[5]
repo := parts[7]
key := project + "/" + repo
switch r.Method {
//------------------------------------------------------------------
// LIST TOKENS (GET)
//------------------------------------------------------------------
case http.MethodGet:
m.Mu.Lock()
values := m.Tokens[key]
m.Mu.Unlock()
resp := map[string]interface{}{
"values": values,
}
_ = json.NewEncoder(w).Encode(resp)
//------------------------------------------------------------------
// CREATE TOKEN (PUT)
//------------------------------------------------------------------
case http.MethodPut:
var payload map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
name := payload["name"].(string)
expDays := int(payload["expiryDays"].(float64))
expiry := time.Now().Add(time.Duration(expDays) * 24 * time.Hour).UnixMilli()
token := Token{
Name: name,
Token: "secret-" + name,
ExpiryDate: expiry,
Permissions: []string{
"REPO_READ",
},
}
m.Mu.Lock()
m.Tokens[key] = append(m.Tokens[key], token)
m.Mu.Unlock()
_ = json.NewEncoder(w).Encode(token)
//------------------------------------------------------------------
// DELETE TOKEN (DELETE)
//------------------------------------------------------------------
case http.MethodDelete:
if len(parts) < 9 {
http.Error(w, "missing token name", http.StatusBadRequest)
return
}
tokenName := parts[8]
m.Mu.Lock()
old := m.Tokens[key]
newList := make([]Token, 0)
for _, t := range old {
if t.Name != tokenName {
newList = append(newList, t)
}
}
m.Tokens[key] = newList
m.Mu.Unlock()
w.WriteHeader(http.StatusOK)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})
m.Server = &http.Server{
Handler: mux,
Addr: "127.0.0.1:0",
}
return m
}
// Start starts the mock server on a random port.
func (m *MockBitbucketServer) Start() error {
ln, err := net.Listen("tcp", m.Server.Addr)
if err != nil {
return err
}
m.URL = "http://" + ln.Addr().String()
go m.Server.Serve(ln)
return nil
}
// Helper to simulate a drift scenario by deleting all tokens for a repo.
func (m *MockBitbucketServer) ClearTokensFor(key string) {
m.Mu.Lock()
m.Tokens[key] = nil
m.Mu.Unlock()
}
func (m *MockBitbucketServer) SetExpiredToken(key string) {
m.Mu.Lock()
defer m.Mu.Unlock()
if len(m.Tokens[key]) == 0 {
// create one if none exists
m.Tokens[key] = []Token{{
Name: "expired-token",
Token: "secret",
ExpiryDate: time.Now().Add(-1 * time.Hour).UnixMilli(),
Permissions: []string{"REPO_READ"},
}}
return
}
// expire the first existing token
m.Tokens[key][0].ExpiryDate = time.Now().Add(-1 * time.Hour).UnixMilli()
}