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", , "repos", , ...] 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() }