d78644d173
agentpr manages PRs/comments/whoami as unkin-agent (Vault AppRole -> gitea creds -> Gitea API), fixing tea's post-as-Ben default. watchpr polls PRs and alerts only on merge/close, human comment, CI failure, or lost mergeability. - cobra multi-binary layout mirroring node-lookup (cmd/ + internal/) - Makefile (build, patch|minor|major, completions, rpm), nfpm RPM with both binaries + bash/zsh/fish completions, woodpecker CI publishing to rpm-internal - unit tests for parsing, meaningful-change detection, and the Vault+Gitea client
180 lines
5.9 KiB
Go
180 lines
5.9 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// fakeVault serves the AppRole login and gitea creds endpoints.
|
|
func fakeVault(t *testing.T, wantRoleID, 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/"+GiteaCredsPath, 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-abc")
|
|
defer srv.Close()
|
|
|
|
tok, err := fetchGiteaToken(srv.URL, "role-xyz")
|
|
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"); 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)
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|