package ciscript_test import ( "encoding/json" "net/http" "net/http/httptest" "sync" "testing" ) // commitStatus is the part of an entry in Gitea's combined-status // response that script/ci-mark-superseded reads. type commitStatus struct { Context string `json:"context"` Status string `json:"status"` Description string `json:"description"` } // postedStatus is the part of a create-status request body the script // writes. type postedStatus struct { Context string `json:"context"` State string `json:"state"` Description string `json:"description"` } // fakeGitea serves the two endpoints the script talks to. Like Gitea, // the newest status for a context replaces the previous one, so a // second run of the script sees what the first one wrote. type fakeGitea struct { mu sync.Mutex statuses map[string][]commitStatus posted map[string][]postedStatus // failRead is a commit whose combined-status read answers HTTP // 500, standing in for a status API that is down. failRead string } // newFakeGitea returns the fake and the base URL to hand the script as // GITHUB_API_URL. func newFakeGitea(t *testing.T) (*fakeGitea, string) { t.Helper() fake := &fakeGitea{ mu: sync.Mutex{}, statuses: map[string][]commitStatus{}, posted: map[string][]postedStatus{}, failRead: "", } srv := httptest.NewServer(fake.routes()) t.Cleanup(srv.Close) return fake, srv.URL } func (f *fakeGitea) routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc( "GET /repos/{owner}/{repo}/commits/{sha}/status", f.handleCombined, ) mux.HandleFunc( "POST /repos/{owner}/{repo}/statuses/{sha}", f.handleCreate, ) return mux } func (f *fakeGitea) handleCombined( w http.ResponseWriter, r *http.Request, ) { f.mu.Lock() defer f.mu.Unlock() sha := r.PathValue("sha") if f.failRead != "" && f.failRead == sha { http.Error(w, "boom", http.StatusInternalServerError) return } body := struct { Statuses []commitStatus `json:"statuses"` }{Statuses: f.statuses[sha]} payload, err := json.Marshal(body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write(payload) } func (f *fakeGitea) handleCreate(w http.ResponseWriter, r *http.Request) { var got postedStatus err := json.NewDecoder(r.Body).Decode(&got) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } sha := r.PathValue("sha") f.mu.Lock() defer f.mu.Unlock() f.posted[sha] = append(f.posted[sha], got) f.replaceLocked(sha, commitStatus{ Context: got.Context, Status: got.State, Description: got.Description, }) w.WriteHeader(http.StatusCreated) } // failStatusRead makes the combined-status read for one commit answer // HTTP 500. func (f *fakeGitea) failStatusRead(sha string) { f.mu.Lock() defer f.mu.Unlock() f.failRead = sha } // setStatus gives a commit its latest status for a context. func (f *fakeGitea) setStatus(sha string, status commitStatus) { f.mu.Lock() defer f.mu.Unlock() f.replaceLocked(sha, status) } // postedFor returns the statuses the script created for a commit. func (f *fakeGitea) postedFor(sha string) []postedStatus { f.mu.Lock() defer f.mu.Unlock() return append([]postedStatus(nil), f.posted[sha]...) } // replaceLocked requires f.mu. func (f *fakeGitea) replaceLocked(sha string, status commitStatus) { for i, existing := range f.statuses[sha] { if existing.Context == status.Context { f.statuses[sha][i] = status return } } f.statuses[sha] = append(f.statuses[sha], status) }