package service import ( "errors" "strings" "testing" "sourcecraft.dev/bigbes/sr-ht-spec/authn" ) // The three token calls are owner-only, and these tests run against a database // that cannot be reached: a refusal that needs a query is a refusal that would // have leaked the inventory, or minted the row, before deciding. func TestIssueAgentTokenIsOwnerOnly(t *testing.T) { svc, _ := newService(t) for name, p := range map[string]authn.Principal{ "an agent": {Kind: authn.KindAgent, Owner: "bigbes", Agent: "claude", Session: "s1"}, "anonymous": authn.Anonymous(), "a zero principal": {}, } { token, row, err := svc.IssueAgentToken(t.Context(), p, "another") if !errors.Is(err, ErrForbidden) { t.Errorf("%s was not refused with ErrForbidden: %v", name, err) } if token != "" || row.ID != 0 { t.Errorf("%s got a token back: %q %+v", name, token, row) } } } // TestIssueAgentTokenRefusesAnAgentBeforeMinting is the property revocation // depends on: an agent that could mint would survive having its own credential // revoked, so "revoke the token" would stop being incident response. func TestIssueAgentTokenRefusesAnAgentBeforeMinting(t *testing.T) { svc, _ := newService(t) agent := authn.Principal{Kind: authn.KindAgent, Owner: "bigbes", Agent: "claude", Session: "s1"} _, _, err := svc.IssueAgentToken(t.Context(), agent, "self-issued") if !errors.Is(err, ErrForbidden) { t.Fatalf("an agent minting a token was not refused: %v", err) } if !strings.Contains(err.Error(), "only the instance owner") { t.Errorf("the refusal does not say who may: %v", err) } } func TestListAndRevokeAreOwnerOnly(t *testing.T) { svc, _ := newService(t) agent := authn.Principal{Kind: authn.KindAgent, Owner: "bigbes", Agent: "claude", Session: "s1"} if _, err := svc.ListAgentTokens(t.Context(), agent); !errors.Is(err, ErrForbidden) { t.Errorf("an agent listing tokens was not refused: %v", err) } if err := svc.RevokeAgentToken(t.Context(), agent, 1); !errors.Is(err, ErrForbidden) { t.Errorf("an agent revoking a token was not refused: %v", err) } } // TestRevokeAgentTokenChecksTheIDBeforeTheDatabase keeps a mistyped id from // becoming an UPDATE that matches nothing and reports "not found", which reads // like the token is already gone. func TestRevokeAgentTokenChecksTheIDBeforeTheDatabase(t *testing.T) { svc, _ := newService(t) owner := authn.Principal{Kind: authn.KindOwner, Owner: "bigbes"} for _, id := range []int{0, -1} { if err := svc.RevokeAgentToken(t.Context(), owner, id); !errors.Is(err, ErrInvalid) { t.Errorf("revoking id %d was not refused as invalid: %v", id, err) } } } func TestValidateTokenName(t *testing.T) { got, err := validateTokenName(" claude-code ") if err != nil { t.Fatalf("validateTokenName: %v", err) } if got != "claude-code" { t.Errorf("name = %q want it trimmed", got) } for name, in := range map[string]string{ "empty": "", "only whitespace": " ", "a control character": "claude\x00code", "a newline": "claude\ncode", "longer than the caps": strings.Repeat("x", MaxTokenNameLen+1), } { if _, err := validateTokenName(in); !errors.Is(err, ErrInvalid) { t.Errorf("%s was accepted as a token name", name) } } } // TestIssueAgentTokenValidatesTheNameBeforeMinting keeps a rejected name from // consuming entropy and, more importantly, from leaving a row whose label the // owner cannot read in the listing. func TestIssueAgentTokenValidatesTheNameBeforeMinting(t *testing.T) { svc, _ := newService(t) owner := authn.Principal{Kind: authn.KindOwner, Owner: "bigbes"} if _, _, err := svc.IssueAgentToken(t.Context(), owner, " "); !errors.Is(err, ErrInvalid) { t.Errorf("a blank token name was accepted: %v", err) } }