package agent import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "sync" "testing" ) func TestGiteaCredsPath(t *testing.T) { tests := []struct { name string agentLogin string credsPath string want string }{ {"no env keeps the historical path", "", "", "gitea/creds/unkin-agent"}, {"derived from AGENT_LOGIN", "repospawner", "", "gitea/creds/repospawner"}, {"GITEA_CREDS_PATH beats AGENT_LOGIN", "repospawner", "gitea/creds/someone-else", "gitea/creds/someone-else"}, {"GITEA_CREDS_PATH beats the default", "", "other-gitea/creds/bot", "other-gitea/creds/bot"}, {"override is trimmed for joining onto /v1/", "", " /gitea/creds/bot/ ", "gitea/creds/bot"}, {"blank override falls back to the login", "repospawner", " ", "gitea/creds/repospawner"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv("AGENT_LOGIN", tt.agentLogin) t.Setenv("GITEA_CREDS_PATH", tt.credsPath) if got := GiteaCredsPath(); got != tt.want { t.Errorf("GiteaCredsPath() = %q, want %q", got, tt.want) } }) } } // recordingVault serves the AppRole login plus any creds path under /v1/, // recording which one was read so tests can assert the selected path. type recordingVault struct { mu sync.Mutex credsPath string } func (v *recordingVault) path() string { v.mu.Lock() defer v.mu.Unlock() return v.credsPath } // fakeEstate serves both Vault (/v1/...) and Gitea (/api/v1/...) from one // server, so a test can drive the whole token-then-API flow. func fakeEstate(t *testing.T, giteaToken, login string) (*httptest.Server, *recordingVault) { t.Helper() rec := &recordingVault{} mux := http.NewServeMux() mux.HandleFunc("/v1/auth/approle/login", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, `{"auth":{"client_token":"s.vaulttoken"}}`) }) mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) { rec.mu.Lock() rec.credsPath = strings.TrimPrefix(r.URL.Path, "/v1/") rec.mu.Unlock() _, _ = io.WriteString(w, `{"data":{"token":"`+giteaToken+`"}}`) }) mux.HandleFunc("/api/v1/user", func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("Authorization"); got != "token "+giteaToken { t.Errorf("whoami auth header = %q, want token %s", got, giteaToken) } _, _ = io.WriteString(w, `{"login":"`+login+`","id":7}`) }) mux.HandleFunc("/api/v1/repos/unkin/repo/pulls", func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("Authorization"); got != "token "+giteaToken { t.Errorf("create PR auth header = %q, want token %s", got, giteaToken) } var body CreatePROptions _ = json.NewDecoder(r.Body).Decode(&body) _, _ = io.WriteString(w, `{"number":12,"state":"open","html_url":"`+r.Host+`/pulls/12"}`) }) return httptest.NewServer(mux), rec } // A service that sets AGENT_LOGIN must read its own creds path and act as its // own Gitea identity for both whoami and PR creation. func TestWhoamiAndPRUseSelectedCredsPath(t *testing.T) { srv, rec := fakeEstate(t, "gitea-repospawner", "repospawner") defer srv.Close() t.Setenv("AGENT_LOGIN", "repospawner") t.Setenv("GITEA_CREDS_PATH", "") t.Setenv("VAULT_ADDR", srv.URL) t.Setenv("GITEA_URL", srv.URL) tok, err := fetchGiteaToken(VaultAddr(), RoleID(), GiteaCredsPath()) if err != nil { t.Fatalf("fetchGiteaToken: %v", err) } if got := rec.path(); got != "gitea/creds/repospawner" { t.Errorf("vault read path = %q, want gitea/creds/repospawner", got) } if tok != "gitea-repospawner" { t.Fatalf("token = %q, want gitea-repospawner", tok) } c := NewGiteaClient(tok) u, err := c.Whoami() if err != nil { t.Fatalf("Whoami: %v", err) } if u.Login != "repospawner" { t.Errorf("whoami login = %q, want repospawner", u.Login) } pr, err := c.CreatePR("unkin/repo", CreatePROptions{Base: "main", Head: "feature", Title: "T"}) if err != nil { t.Fatalf("CreatePR: %v", err) } if pr.Number != 12 { t.Errorf("PR number = %d, want 12", pr.Number) } } // GITEA_CREDS_PATH must win even when AGENT_LOGIN names a different identity. func TestCredsPathOverrideBeatsAgentLogin(t *testing.T) { srv, rec := fakeEstate(t, "gitea-override", "someone-else") defer srv.Close() t.Setenv("AGENT_LOGIN", "repospawner") t.Setenv("GITEA_CREDS_PATH", "gitea/creds/someone-else") t.Setenv("VAULT_ADDR", srv.URL) if _, err := fetchGiteaToken(VaultAddr(), RoleID(), GiteaCredsPath()); err != nil { t.Fatalf("fetchGiteaToken: %v", err) } if got := rec.path(); got != "gitea/creds/someone-else" { t.Errorf("vault read path = %q, want gitea/creds/someone-else", got) } } // With no env set the tools must still read the exact path they always did. func TestCredsPathDefaultIsBackwardCompatible(t *testing.T) { srv, rec := fakeEstate(t, "gitea-abc", "unkin-agent") defer srv.Close() t.Setenv("AGENT_LOGIN", "") t.Setenv("GITEA_CREDS_PATH", "") t.Setenv("VAULT_ADDR", srv.URL) if _, err := fetchGiteaToken(VaultAddr(), RoleID(), GiteaCredsPath()); err != nil { t.Fatalf("fetchGiteaToken: %v", err) } if got := rec.path(); got != "gitea/creds/unkin-agent" { t.Errorf("vault read path = %q, want gitea/creds/unkin-agent", got) } }