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)
}
}