package adminbootstrap_test import ( "context" "encoding/json" "io" "net/http" "strings" "testing" "git.d-ma.be/mathias/gitea-mcp/internal/adminbootstrap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // This exists to close dispatch#67 fix #2's blocker: minting a scoped Gitea // token needs basic auth (POST /users/{u}/tokens and POST /admin/users both // reject token auth outright, confirmed live against git.d-ma.be — an // existing token cannot mint another token, by design). A sandboxed agent // session has no interactive password, so this package is meant to run // locally, where a real admin password is available just-in-time (1Password, // `op run`), and to do the whole bootstrap in one auditable call rather than // leaving it as ad hoc curl. // // Design choice, made explicit rather than silently assumed: this tool // ALWAYS (re)sets the target account's password via admin basic auth before // minting, whether the account is new or already existed. That is safe and // expected for a dedicated service/bot account (its password is a mechanism // to reach the token endpoint, never a human login credential) and would be // dangerous pointed at a real person's account by mistake -- which is why // Request.ConfirmServiceAccount exists and Run refuses to proceed without it. type call struct { method, path string body map[string]any authUser string // basic-auth username used for this call, "" if token/none authPass string } type fakeTransport struct { t *testing.T responses map[string]fakeResponse // key: "METHOD path" calls []call } type fakeResponse struct { status int body string } func (f *fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) { var body map[string]any if req.Body != nil { raw, _ := io.ReadAll(req.Body) if len(raw) > 0 { require.NoError(f.t, json.Unmarshal(raw, &body)) } } user, pass, _ := req.BasicAuth() f.calls = append(f.calls, call{method: req.Method, path: req.URL.Path, body: body, authUser: user, authPass: pass}) key := req.Method + " " + req.URL.Path r, ok := f.responses[key] if !ok { f.t.Fatalf("unscripted request: %s (have: %v)", key, keys(f.responses)) } return &http.Response{ StatusCode: r.status, Body: io.NopCloser(strings.NewReader(r.body)), Header: make(http.Header), }, nil } func keys(m map[string]fakeResponse) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } return out } // fakeSecretStore records what would have been written to 1Password, without // ever touching it -- and, critically, asserts the token value itself never // leaks into anything but this one call. type fakeSecretStore struct { stored map[string]string } func (f *fakeSecretStore) Store(_ context.Context, name, value string) (string, error) { if f.stored == nil { f.stored = map[string]string{} } f.stored[name] = value return "op://AgentSecrets/" + name + "/token", nil } func newBootstrapper(t *testing.T, responses map[string]fakeResponse) (adminbootstrap.Bootstrapper, *fakeTransport, *fakeSecretStore) { ft := &fakeTransport{t: t, responses: responses} fs := &fakeSecretStore{} return adminbootstrap.Bootstrapper{ BaseURL: "http://fake.gitea", AdminUsername: "mathias", AdminPassword: "admin-pass-not-a-real-secret", HTTP: &http.Client{Transport: ft}, Secrets: fs, }, ft, fs } func TestRun_CreatesNewAccountMintsTokenAndStoresIt(t *testing.T) { b, ft, fs := newBootstrapper(t, map[string]fakeResponse{ "GET /api/v1/users/dispatch-bot": {status: 404, body: `{}`}, "POST /api/v1/admin/users": {status: 201, body: `{"login":"dispatch-bot"}`}, "POST /api/v1/users/dispatch-bot/tokens": {status: 201, body: `{"sha1":"tok_abc123"}`}, }) result, err := b.Run(context.Background(), adminbootstrap.Request{ Username: "dispatch-bot", Email: "dispatch@d-ma.be", Scopes: []string{"read:repository", "read:issue"}, TokenName: "dispatch-ci-readonly", SecretName: "gitea-dispatch-bot-ci-readonly", ConfirmServiceAccount: true, }) require.NoError(t, err) assert.True(t, result.Created) assert.Equal(t, "op://AgentSecrets/gitea-dispatch-bot-ci-readonly/token", result.SecretRef) assert.Equal(t, "tok_abc123", fs.stored["gitea-dispatch-bot-ci-readonly"]) // The account-creation call must use the ADMIN's basic auth, never the // fresh account's own (unknown at that point) password. create := findCall(t, ft.calls, "POST", "/api/v1/admin/users") assert.Equal(t, "mathias", create.authUser) assert.Equal(t, "dispatch-bot", create.body["username"]) assert.Equal(t, "dispatch@d-ma.be", create.body["email"]) assert.Equal(t, false, create.body["must_change_password"]) // The token-mint call must use basic auth AS THE NEW ACCOUNT -- Gitea // requires this specifically (an admin token cannot mint on someone // else's behalf) -- with the password this package generated and set, // never the admin's own password. mint := findCall(t, ft.calls, "POST", "/api/v1/users/dispatch-bot/tokens") assert.Equal(t, "dispatch-bot", mint.authUser) assert.NotEqual(t, "admin-pass-not-a-real-secret", mint.authPass) assert.NotEmpty(t, mint.authPass) assert.Equal(t, "dispatch-ci-readonly", mint.body["name"]) assert.ElementsMatch(t, []any{"read:repository", "read:issue"}, mint.body["scopes"]) } func TestRun_ExistingAccountGetsPasswordResetNotRecreated(t *testing.T) { b, ft, _ := newBootstrapper(t, map[string]fakeResponse{ "GET /api/v1/users/dispatch-bot": {status: 200, body: `{"login":"dispatch-bot"}`}, "PATCH /api/v1/admin/users/dispatch-bot": {status: 200, body: `{}`}, "POST /api/v1/users/dispatch-bot/tokens": {status: 201, body: `{"sha1":"tok_new"}`}, }) result, err := b.Run(context.Background(), adminbootstrap.Request{ Username: "dispatch-bot", Email: "dispatch@d-ma.be", Scopes: []string{"read:repository"}, TokenName: "dispatch-ci-readonly", SecretName: "gitea-dispatch-bot-ci-readonly", ConfirmServiceAccount: true, }) require.NoError(t, err) assert.False(t, result.Created, "must report the account as pre-existing, not newly created") // Must PATCH (reset password), never POST (create) for an existing account. assert.Empty(t, filterCalls(ft.calls, "POST", "/api/v1/admin/users")) patch := findCall(t, ft.calls, "PATCH", "/api/v1/admin/users/dispatch-bot") assert.Equal(t, "mathias", patch.authUser, "the reset must be done as the real admin") assert.NotEmpty(t, patch.body["password"]) } func TestRun_RefusesWithoutExplicitServiceAccountConfirmation(t *testing.T) { // Guard against pointing this at a real person's account by accident -- // this tool resets whatever password the target account has. b, ft, _ := newBootstrapper(t, map[string]fakeResponse{}) _, err := b.Run(context.Background(), adminbootstrap.Request{ Username: "mathias", Email: "mthbqv@gmail.com", Scopes: []string{"read:repository"}, TokenName: "whatever", SecretName: "whatever", // ConfirmServiceAccount left false/unset }) require.Error(t, err) assert.Contains(t, err.Error(), "ConfirmServiceAccount") assert.Empty(t, ft.calls, "must not make any request at all without confirmation") } func TestRun_GrantsReadOnlyCollaboratorAccessViaAdminAuth(t *testing.T) { b, ft, _ := newBootstrapper(t, map[string]fakeResponse{ "GET /api/v1/users/dispatch-bot": {status: 404, body: `{}`}, "POST /api/v1/admin/users": {status: 201, body: `{}`}, "POST /api/v1/users/dispatch-bot/tokens": {status: 201, body: `{"sha1":"tok"}`}, "PUT /api/v1/repos/mathias/cobalt-dingo/collaborators/dispatch-bot": {status: 204, body: ``}, }) result, err := b.Run(context.Background(), adminbootstrap.Request{ Username: "dispatch-bot", Email: "dispatch@d-ma.be", Scopes: []string{"read:repository"}, TokenName: "dispatch-ci-readonly", SecretName: "gitea-dispatch-bot-ci-readonly", GrantReadRepos: []adminbootstrap.RepoRef{{Owner: "mathias", Name: "cobalt-dingo"}}, ConfirmServiceAccount: true, }) require.NoError(t, err) assert.Equal(t, []string{"mathias/cobalt-dingo"}, result.GrantedRepos) grant := findCall(t, ft.calls, "PUT", "/api/v1/repos/mathias/cobalt-dingo/collaborators/dispatch-bot") assert.Equal(t, "mathias", grant.authUser, "granting collaborator access is an admin action, not the new account's own") assert.Equal(t, "read", grant.body["permission"]) } func TestRun_FailsClosedOnUpstreamError(t *testing.T) { b, _, fs := newBootstrapper(t, map[string]fakeResponse{ "GET /api/v1/users/dispatch-bot": {status: 404, body: `{}`}, "POST /api/v1/admin/users": {status: 500, body: `{"message":"internal error"}`}, }) _, err := b.Run(context.Background(), adminbootstrap.Request{ Username: "dispatch-bot", Email: "dispatch@d-ma.be", Scopes: []string{"read:repository"}, TokenName: "x", SecretName: "x", ConfirmServiceAccount: true, }) require.Error(t, err) assert.Empty(t, fs.stored, "nothing must be written to the secret store if the flow didn't complete") } func findCall(t *testing.T, calls []call, method, path string) call { t.Helper() for _, c := range calls { if c.method == method && c.path == path { return c } } t.Fatalf("no call found: %s %s (have: %+v)", method, path, calls) return call{} } func filterCalls(calls []call, method, path string) []call { var out []call for _, c := range calls { if c.method == method && c.path == path { out = append(out, c) } } return out }