package agent import ( "encoding/json" "io" "net/http" "net/http/httptest" "testing" ) // fakeVault serves the AppRole login and the gitea creds secret at credsPath // only, so a read of any other path 404s. func fakeVault(t *testing.T, wantRoleID, credsPath, giteaToken string) *httptest.Server { t.Helper() mux := http.NewServeMux() mux.HandleFunc("/v1/auth/approle/login", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("approle login method = %s, want POST", r.Method) } var body map[string]string _ = json.NewDecoder(r.Body).Decode(&body) if body["role_id"] != wantRoleID { t.Errorf("role_id = %q, want %q", body["role_id"], wantRoleID) } if _, ok := body["secret_id"]; ok { t.Errorf("secret_id must not be sent") } _, _ = io.WriteString(w, `{"auth":{"client_token":"s.vaulttoken"}}`) }) mux.HandleFunc("/v1/"+credsPath, func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("X-Vault-Token"); got != "s.vaulttoken" { t.Errorf("X-Vault-Token = %q, want s.vaulttoken", got) } _, _ = io.WriteString(w, `{"data":{"token":"`+giteaToken+`"}}`) }) return httptest.NewServer(mux) } func TestFetchGiteaToken(t *testing.T) { srv := fakeVault(t, "role-xyz", "gitea/creds/unkin-agent", "gitea-abc") defer srv.Close() tok, err := fetchGiteaToken(srv.URL, "role-xyz", "gitea/creds/unkin-agent") if err != nil { t.Fatalf("fetchGiteaToken: %v", err) } if tok != "gitea-abc" { t.Errorf("token = %q, want gitea-abc", tok) } } func TestFetchGiteaTokenLoginError(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/v1/auth/approle/login", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) _, _ = io.WriteString(w, `{"errors":["permission denied"]}`) }) srv := httptest.NewServer(mux) defer srv.Close() if _, err := fetchGiteaToken(srv.URL, "role-xyz", "gitea/creds/unkin-agent"); err == nil { t.Fatal("expected error on 403 login") } } func TestCreatePRRequestBody(t *testing.T) { var gotPath, gotAuth string var gotBody CreatePROptions mux := http.NewServeMux() mux.HandleFunc("/api/v1/repos/unkin/repo/pulls", func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotAuth = r.Header.Get("Authorization") _ = json.NewDecoder(r.Body).Decode(&gotBody) _, _ = io.WriteString(w, `{"number":7,"state":"open","html_url":"https://git.unkin.net/unkin/repo/pulls/7","head":{"sha":"deadbeef"}}`) }) srv := httptest.NewServer(mux) defer srv.Close() c := &GiteaClient{BaseURL: srv.URL, Token: "gitea-abc", HTTP: srv.Client()} pr, err := c.CreatePR("unkin/repo", CreatePROptions{Base: "main", Head: "feature", Title: "T", Body: "B"}) if err != nil { t.Fatalf("CreatePR: %v", err) } if gotPath != "/api/v1/repos/unkin/repo/pulls" { t.Errorf("path = %q", gotPath) } if gotAuth != "token gitea-abc" { t.Errorf("auth header = %q, want 'token gitea-abc'", gotAuth) } if gotBody.Base != "main" || gotBody.Head != "feature" || gotBody.Title != "T" || gotBody.Body != "B" { t.Errorf("request body = %+v", gotBody) } if pr.Number != 7 || pr.HTMLURL == "" { t.Errorf("parsed PR = %+v", pr) } } func TestCreateComment(t *testing.T) { var gotBody map[string]string mux := http.NewServeMux() mux.HandleFunc("/api/v1/repos/unkin/repo/issues/7/comments", func(w http.ResponseWriter, r *http.Request) { _ = json.NewDecoder(r.Body).Decode(&gotBody) _, _ = io.WriteString(w, `{"id":99,"user":{"login":"unkin-agent"},"body":"hi"}`) }) srv := httptest.NewServer(mux) defer srv.Close() c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()} cm, err := c.CreateComment("unkin/repo", 7, "hi") if err != nil { t.Fatalf("CreateComment: %v", err) } if gotBody["body"] != "hi" { t.Errorf("comment body = %q", gotBody["body"]) } if cm.ID != 99 { t.Errorf("comment id = %d, want 99", cm.ID) } } func TestWhoami(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/api/v1/user", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"login":"unkin-agent","id":42}`) }) srv := httptest.NewServer(mux) defer srv.Close() c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()} u, err := c.Whoami() if err != nil { t.Fatalf("Whoami: %v", err) } if u.Login != "unkin-agent" { t.Errorf("login = %q, want unkin-agent", u.Login) } } func TestFetchState(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/api/v1/repos/unkin/repo/pulls/7", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"number":7,"state":"open","merged":false,"mergeable":true,"title":"feat","html_url":"u","head":{"sha":"cafebabecafebabe"}}`) }) mux.HandleFunc("/api/v1/repos/unkin/repo/commits/cafebabecafebabe/status", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"state":"success"}`) }) mux.HandleFunc("/api/v1/repos/unkin/repo/issues/7/comments", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `[{"id":1,"user":{"login":"unkin-agent"}},{"id":2,"user":{"login":"ben"}}]`) }) srv := httptest.NewServer(mux) defer srv.Close() c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()} ref := PRRef{Owner: "unkin", Repo: "repo", Number: 7} st, err := FetchState(c, ref, "unkin-agent") if err != nil { t.Fatalf("FetchState: %v", err) } if st.State != "open" || st.CIStatus != "success" || st.HeadSHA != "cafebabecafebabe" { t.Errorf("state = %+v", st) } if st.NonAgentComments != 1 { t.Errorf("NonAgentComments = %d, want 1 (agent comment excluded)", st.NonAgentComments) } } // A merged PR whose branch was deleted leaves its head commit unreachable, so // the commit-status endpoint 404s. FetchState must still return the (merged) // state rather than failing, otherwise the watch loop never sees the merge. func TestFetchStateToleratesMissingCommit(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/api/v1/repos/unkin/repo/pulls/7", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"number":7,"state":"closed","merged":true,"mergeable":true,"title":"feat","html_url":"u","head":{"sha":"cafebabecafebabe"}}`) }) mux.HandleFunc("/api/v1/repos/unkin/repo/commits/cafebabecafebabe/status", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = io.WriteString(w, `{"message":"not found"}`) }) mux.HandleFunc("/api/v1/repos/unkin/repo/issues/7/comments", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `[]`) }) srv := httptest.NewServer(mux) defer srv.Close() c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()} ref := PRRef{Owner: "unkin", Repo: "repo", Number: 7} st, err := FetchState(c, ref, "unkin-agent") if err != nil { t.Fatalf("FetchState must tolerate a 404 status for a gone commit: %v", err) } if !st.Merged || st.State != "closed" { t.Errorf("merged/state = %t/%q, want true/closed", st.Merged, st.State) } if st.CIStatus != "" { t.Errorf("CIStatus = %q, want empty (no status for a gone commit)", st.CIStatus) } } // A non-404 error from the status endpoint is still fatal: only "commit gone" is // tolerated, not, say, an auth or server failure. func TestFetchStateFailsOnNon404StatusError(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/api/v1/repos/unkin/repo/pulls/7", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"number":7,"state":"open","merged":false,"mergeable":true,"head":{"sha":"cafebabecafebabe"}}`) }) mux.HandleFunc("/api/v1/repos/unkin/repo/commits/cafebabecafebabe/status", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = io.WriteString(w, `{"message":"boom"}`) }) srv := httptest.NewServer(mux) defer srv.Close() c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()} ref := PRRef{Owner: "unkin", Repo: "repo", Number: 7} if _, err := FetchState(c, ref, "unkin-agent"); err == nil { t.Fatal("FetchState should surface a 500 from the status endpoint") } } func TestGiteaAPIError(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/api/v1/repos/unkin/repo/pulls", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = io.WriteString(w, `{"message":"head and base are the same"}`) }) srv := httptest.NewServer(mux) defer srv.Close() c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()} if _, err := c.CreatePR("unkin/repo", CreatePROptions{Base: "main", Head: "main", Title: "x"}); err == nil { t.Fatal("expected error on 422") } }