package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http/httptest"
"path/filepath"
"strings"
"sync"
"testing"
"craftdesign.group/quarry/internal/ci"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
type logRow struct{ repo, actor, via, field, was, now string }
type settingRecorder struct {
mu sync.Mutex
rows []logRow
}
func (s *settingRecorder) LogSetting(repo, actor, via, field, was, now string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.rows = append(s.rows, logRow{repo, actor, via, field, was, now})
return nil
}
type workerEngine struct {
mu sync.Mutex
tokens []ci.WorkerToken
revoked []string
groups []string
mints int
}
func (e *workerEngine) WorkerTokens() ([]ci.WorkerToken, error) {
e.mu.Lock()
defer e.mu.Unlock()
return append([]ci.WorkerToken(nil), e.tokens...), nil
}
func (e *workerEngine) MintWorkerToken(label string, prefixes, groups []string) (ci.WorkerToken, error) {
e.mu.Lock()
defer e.mu.Unlock()
e.groups = groups
e.mints++
t := ci.WorkerToken{ID: fmt.Sprintf("new-%d", e.mints), Token: "s3cret-bearer-value", Label: label, WorkerGroups: groups,
TaskTypePrefixes: prefixes, CreatedAt: "2026-11-12T10:00:00Z"}
stored := t
stored.Token = "" // the engine never lists a bearer
e.tokens = append(e.tokens, stored)
return t, nil
}
func (e *workerEngine) RevokeWorkerToken(id string) error {
e.mu.Lock()
defer e.mu.Unlock()
e.revoked = append(e.revoked, id)
return nil
}
func workerTokenRegistry(t *testing.T, eng *workerEngine) *ops.Registry {
t.Helper()
r := ops.NewRegistry()
caps := func(account, repo string) CapSet {
if account == "dan" {
return oltp.CapSet("a")
}
return oltp.CapSet("3")
}
if err := RegisterAll(r, Deps{Version: "v", WorkerTokens: eng, CapsFor: caps,
Repos: NewRepos(map[string]*Repo{"alpha": {Index: wikiIndex(t)}, "beta": {Index: wikiIndex(t)}})}); err != nil {
t.Fatal(err)
}
return r
}
// A mint answers with the value once; the list never carries it nor
// anything that could be replayed; and the token is scoped to the
// repository's own group.
func TestWorkerTokenMintAnswersOnceAndListNeverShowsIt(t *testing.T) {
eng := &workerEngine{}
r := workerTokenRegistry(t, eng)
dan := ops.WithUser(context.Background(), "dan")
out, err := r.Invoke(dan, "ci.worker.token.new", workerTokenNewIn{Repo: "alpha", Label: "runner"})
if err != nil {
t.Fatal(err)
}
o := out.(workerTokenNewOut)
if o.Value != "s3cret-bearer-value" || o.Token.ID != "new-1" || o.Token.Label != "runner" {
t.Errorf("FAIL: the mint answered %+v", o)
}
if len(eng.groups) != 1 || eng.groups[0] != ci.GroupFor("alpha") {
t.Errorf("FAIL: scoped to %v, want only alpha's group", eng.groups)
}
if body, _ := json.Marshal(o.Token); strings.Contains(string(body), "s3cret") {
t.Errorf("FAIL: the token row carries the value: %s", body)
}
list, err := r.Invoke(dan, "ci.worker.token.list", workerTokenListIn{Repo: "alpha"})
if err != nil {
t.Fatal(err)
}
body, _ := json.Marshal(list)
if strings.Contains(string(body), "s3cret") || strings.Contains(string(body), "value") {
t.Errorf("FAIL: the list carries a value: %s", body)
}
if rows := list.(workerTokenListOut).Tokens; len(rows) != 1 || rows[0].ID != "new-1" {
t.Errorf("FAIL: the list is %+v", rows)
}
t.Log("ok")
}
// An account without the repository's admin letter is refused all three.
func TestWorkerTokenOpsRefuseANonAdmin(t *testing.T) {
eng := &workerEngine{tokens: []ci.WorkerToken{{ID: "own", WorkerGroups: []string{ci.GroupFor("alpha")}}}}
r := workerTokenRegistry(t, eng)
eve := ops.WithUser(context.Background(), "eve")
if _, err := r.Invoke(eve, "ci.worker.token.new", workerTokenNewIn{Repo: "alpha"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a non-admin minted: %v", err)
}
if _, err := r.Invoke(eve, "ci.worker.token.list", workerTokenListIn{Repo: "alpha"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a non-admin listed: %v", err)
}
if _, err := r.Invoke(eve, "ci.worker.token.revoke", workerTokenRevokeIn{Repo: "alpha", ID: "own"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a non-admin revoked: %v", err)
}
if len(eng.revoked) != 0 || eng.groups != nil {
t.Errorf("FAIL: a refused call reached the engine: revoked %v groups %v", eng.revoked, eng.groups)
}
t.Log("ok")
}
// Revoke finds only a token this repository's list shows; another
// repository's, a fleet-wide one and an unknown id are all not found.
func TestWorkerTokenRevokeIsScopedToTheRepository(t *testing.T) {
eng := &workerEngine{tokens: []ci.WorkerToken{
{ID: "own", WorkerGroups: []string{ci.GroupFor("alpha")}},
{ID: "other", WorkerGroups: []string{ci.GroupFor("beta")}},
{ID: "fleet", TaskTypePrefixes: []string{"x"}},
}}
r := workerTokenRegistry(t, eng)
dan := ops.WithUser(context.Background(), "dan")
for _, id := range []string{"other", "fleet", "nope"} {
if _, err := r.Invoke(dan, "ci.worker.token.revoke", workerTokenRevokeIn{Repo: "alpha", ID: id}); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: revoking %q from alpha: %v, want not found", id, err)
}
}
if len(eng.revoked) != 0 {
t.Fatalf("FAIL: the engine revoked %v for a token not alpha's", eng.revoked)
}
if _, err := r.Invoke(dan, "ci.worker.token.revoke", workerTokenRevokeIn{Repo: "alpha", ID: "own"}); err != nil {
t.Fatal(err)
}
if len(eng.revoked) != 1 || eng.revoked[0] != "own" {
t.Errorf("FAIL: revoked %v, want only own", eng.revoked)
}
t.Log("ok")
}
// No engine, no door: not found.
func TestWorkerTokenOpsWithoutAnEngineAreNotFound(t *testing.T) {
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"alpha": {Index: wikiIndex(t)}})}); err != nil {
t.Fatal(err)
}
if _, err := r.Invoke(context.Background(), "ci.worker.token.new", workerTokenNewIn{Repo: "alpha"}); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: %v", err)
}
// And a repository that is not served: refused before the engine.
r = workerTokenRegistry(t, &workerEngine{})
if _, err := r.Invoke(ops.WithUser(context.Background(), "dan"), "ci.worker.token.new", workerTokenNewIn{Repo: "gone"}); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: minting on an unserved repository: %v", err)
}
t.Log("ok")
}
// Mint and revoke are written to the repository's settings log with the
// actor and the token's label and id, never its value; revoking one that
// is already revoked answers without the engine and without a second row.
func TestWorkerTokenMintAndRevokeAreLogged(t *testing.T) {
eng := &workerEngine{}
rec := &settingRecorder{}
r := ops.NewRegistry()
caps := func(string, string) CapSet { return oltp.CapSet("a") }
if err := RegisterAll(r, Deps{Version: "v", WorkerTokens: eng, SettingLog: rec, CapsFor: caps,
Repos: NewRepos(map[string]*Repo{"alpha": {Index: wikiIndex(t)}})}); err != nil {
t.Fatal(err)
}
dan := ops.WithUser(context.Background(), "dan")
if _, err := r.Invoke(dan, "ci.worker.token.new", workerTokenNewIn{Repo: "alpha", Label: "runner"}); err != nil {
t.Fatal(err)
}
if len(rec.rows) != 1 || rec.rows[0].actor != "dan" || rec.rows[0].repo != "alpha" || rec.rows[0].field != "worker-token" ||
!strings.Contains(rec.rows[0].now, "runner") || !strings.Contains(rec.rows[0].now, "new-1") {
t.Fatalf("FAIL: the mint's row: %+v", rec.rows)
}
if _, err := r.Invoke(dan, "ci.worker.token.revoke", workerTokenRevokeIn{Repo: "alpha", ID: "new-1"}); err != nil {
t.Fatal(err)
}
if len(rec.rows) != 2 || rec.rows[1].actor != "dan" || rec.rows[1].now != "revoked" || !strings.Contains(rec.rows[1].was, "new-1") {
t.Fatalf("FAIL: the revoke's row: %+v", rec.rows)
}
for _, row := range rec.rows {
if strings.Contains(row.was+row.now, "s3cret") {
t.Errorf("FAIL: a row carries the value: %+v", row)
}
}
// The engine lists the token as revoked now.
eng.mu.Lock()
eng.tokens[0].RevokedAt = "2026-11-12T11:00:00Z"
eng.mu.Unlock()
if _, err := r.Invoke(dan, "ci.worker.token.revoke", workerTokenRevokeIn{Repo: "alpha", ID: "new-1"}); err != nil {
t.Fatal(err)
}
if len(eng.revoked) != 1 || len(rec.rows) != 2 {
t.Errorf("FAIL: revoking a revoked token reached the engine %d times and wrote %d rows", len(eng.revoked), len(rec.rows))
}
t.Log("ok")
}
// An agent does not mint a worker token; asking its person with
// Quarry-Ask files a request, and once the person approves, the token is
// minted as them. The agent still lists and revokes with `a`.
func TestAnAgentAsksItsPersonForAWorkerToken(t *testing.T) {
eng := &workerEngine{}
rec := &settingRecorder{}
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
for _, who := range []string{"dmestas", "jva"} {
if err := db.AddAccount(who, "a-long-enough-password", who == "dmestas"); err != nil {
t.Fatal(err)
}
}
if err := db.AddAccount("jva-bot", "a-long-enough-password", false); err != nil {
t.Fatal(err)
}
if err := db.SetOperator("jva-bot", "jva"); err != nil {
t.Fatal(err)
}
holds := func(string, string) CapSet { return oltp.CapSet("a") }
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", WorkerTokens: eng, SettingLog: rec,
Repos: NewRepos(map[string]*Repo{"test": {Index: wikiIndex(t), Code: "code-test"}}),
Accounts: adminAdapter{acctAdapter{db}}, SignOffs: testSignOffs{db}, CapsFor: holds, SecretBox: testBox(t)}); err != nil {
t.Fatal(err)
}
if _, err := r.Invoke(asUser("jva-bot"), "ci.worker.token.new", workerTokenNewIn{Repo: "test", Label: "ci"}); !errors.Is(err, ops.ErrForbidden) {
t.Fatalf("FAIL: an agent minted: %v", err)
}
if eng.mints != 0 {
t.Fatal("FAIL: a refused agent reached the engine")
}
if _, err := r.Invoke(asUser("jva-bot"), "ci.worker.token.list", workerTokenListIn{Repo: "test"}); err != nil {
t.Errorf("FAIL: an agent could not list: %v", err)
}
h, err := RouterWithCaps(r, "v", holds, nil)
if err != nil {
t.Fatal(err)
}
ctx := ops.WithToken(ops.WithUser(context.Background(), "jva-bot"), ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return oltp.CapSet("a") }})
req := httptest.NewRequest("POST", "/api/v1/r/test/ci/worker-tokens", strings.NewReader(`{"label":"ci"}`)).WithContext(ctx)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Quarry-Ask", "a runner for the repository")
rec2 := httptest.NewRecorder()
h.ServeHTTP(rec2, req)
if rec2.Code != 202 {
t.Fatalf("FAIL: asking answered %d: %s", rec2.Code, rec2.Body)
}
var asked struct {
SignOff SignOffRequest `json:"signoff"`
}
if err := json.Unmarshal(rec2.Body.Bytes(), &asked); err != nil || asked.SignOff.ID == "" || asked.SignOff.Owner != "jva" || asked.SignOff.State != "pending" {
t.Fatalf("FAIL: the request: %s", rec2.Body)
}
if eng.mints != 0 {
t.Fatal("FAIL: the ask minted before the person approved")
}
if _, err := r.Invoke(asUser("jva"), "signoff.decide", signOffDecideIn{ID: asked.SignOff.ID, Approve: true}); err != nil {
t.Fatalf("FAIL: approving: %v", err)
}
if eng.mints != 1 {
t.Fatalf("FAIL: approving minted %d tokens, want 1", eng.mints)
}
if len(rec.rows) != 1 || rec.rows[0].actor != "jva" {
t.Errorf("FAIL: the mint was not logged as the person: %+v", rec.rows)
} else if !strings.Contains(rec.rows[0].now, "asked by jva-bot") || !strings.Contains(rec.rows[0].now, asked.SignOff.ID) {
t.Errorf("FAIL: the row does not name the asking agent and request: %q", rec.rows[0].now)
}
t.Log("ok")
}