26cd05e961
agentpr always read gitea/creds/unkin-agent from a bare const, so a service like repospawner could not run it as its own Gitea identity. - Replace the GiteaCredsPath const with a function: GITEA_CREDS_PATH when set, otherwise gitea/creds/<AGENT_LOGIN>. Unset env still resolves to gitea/creds/unkin-agent, so existing callers are unchanged. - Thread the creds path through fetchGiteaToken/readGiteaCreds instead of reading a package-level const, and report it in the error messages. - Make agentpr's help text login-agnostic and document both variables.
234 lines
8.5 KiB
Go
234 lines
8.5 KiB
Go
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")
|
|
}
|
|
}
|