package api
import (
"context"
"errors"
"path/filepath"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/zitcli"
)
// A RUN TOKEN GOES ONLY WHERE THE ENVIRONMENT'S RULES SAY (RFC 0021 §runs).
//
// From the forge's own records alone: the run's start (event, starter,
// the job's declaration), the environment's branch policy, allowed and
// starter letter, the check-in's branch. Never a merge's.
func TestARunTokenGoesWhereTheRulesSay(t *testing.T) {
wait := runStartWait
runStartWait = 50 * time.Millisecond
t.Cleanup(func() { runStartWait = wait })
ix, trunkTip, devTip := closeIndex(t) // trunk is protected below, dev is not
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
db.SyncProjectCode = func(repo string) (string, bool) { return "CODE-" + repo, true }
for name, vis := range map[string]string{"test": "private", "pub": "public"} {
if err := db.AddRepo(name, filepath.Join(t.TempDir(), name), "", vis); err != nil {
t.Fatal(err)
}
}
for user, caps := range map[string]string{"steward": "a", "dev": "i", "alice": "a"} {
if err := db.AddAccount(user, "pw-irrelevant", false); err != nil {
t.Fatal(err)
}
if err := db.SetCaps(user, "test", caps); err != nil {
t.Fatal(err)
}
}
d := Deps{Version: "v",
Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: &zitcli.Client{}}, "pub": {Index: ix, Zit: &zitcli.Client{}}}),
Protect: stubProtect{rules: []oltp.Protection{{Branch: "trunk", Owners: []string{"alice"}}}},
Secrets: db,
}
r := ops.NewRegistry()
if err := RegisterAll(r, d); err != nil {
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "alice")
for _, in := range []ciEnvironmentSetIn{
{Repo: "test", Name: "release", Allowed: ptr("y"), Starter: ptr("a")},
{Repo: "test", Name: "narrow", Allowed: ptr("o g")},
{Repo: "test", Name: "anybranch", Branches: []string{"*"}},
} {
if _, err := r.Invoke(ctx, "ci.environment.set", in); err != nil {
t.Fatalf("FAIL: ci.environment.set %+v: %v", in, err)
}
}
if p, _ := db.EnvironmentPolicy("test", "release"); p.Allowed != "y" || p.Starter != "a" {
t.Errorf("FAIL: ci.environment.set left release at %+v; want allowed y, starter a", p)
}
n := 0
run := func(repo, checkin, pair, event, starter string, jobs map[string]oltp.RunJobGrant) string {
n++
id := "run" + string(rune('a'+n))
if err := db.AddCIRun(oltp.CIRun{RunID: id, Repo: repo, Checkin: checkin, Pair: pair, Workflow: "w", Pipeline: "ci/release.yml", Status: "running"}); err != nil {
t.Fatal(err)
}
if err := db.RecordCIRunStart(id, event, starter, jobs); err != nil {
t.Fatal(err)
}
return id
}
publish := map[string]oltp.RunJobGrant{"publish": {Environment: "release", Letters: "y"}}
letters := func(env map[string]string) string { return string(db.EffectiveCaps(env["QUARRY_USER"], "test")) }
// A tagger holding a and not y gets y: no starter intersection.
id := run("test", trunkTip, "", "tag", "steward", publish)
env, note, err := d.DeliverRunToken("test", id+".publish")
if err != nil || env["QUARRY_USER"] == "" || !strings.HasPrefix(env["QUARRY_TOKEN"], "qrt_") {
t.Fatalf("FAIL: a tag by a steward on trunk got %v, %q, %v; want a token", env, note, err)
}
if got := letters(env); !strings.Contains(got, "y") || strings.Contains(got, "i") {
t.Errorf("FAIL: the release token holds %q; want y with the read letters and nothing else", got)
}
if toks, _ := db.RunTokens(id); len(toks) != 1 || toks[0].Ceiling != "goy" || toks[0].Name != "publish" {
t.Errorf("FAIL: the run's tokens %+v; want one for publish carrying goy", toks)
}
out, err := r.Invoke(ctx, "ci.run.tokens", ciRunTokensIn{Repo: "test", RunID: id})
if err != nil {
t.Fatalf("FAIL: ci.run.tokens: %v", err)
}
if got := out.(ciRunTokensOut).Tokens; len(got) != 1 || got[0].Job != "publish" || got[0].Letters != "goy" || got[0].Revoked != "" {
t.Errorf("FAIL: ci.run.tokens = %+v; want publish carrying goy, live", got)
}
if strings.Contains(asJSON(t, out), "qrt_") || strings.Contains(asJSON(t, out), env["QUARRY_TOKEN"]) {
t.Errorf("FAIL: ci.run.tokens answered with a value: %s", asJSON(t, out))
}
// A tagger without a: the run goes on, nothing is minted, the note
// names the letter and the person.
id = run("test", trunkTip, "", "tag", "dev", publish)
env, note, err = d.DeliverRunToken("test", id+".publish")
if err != nil || len(env) != 0 || !strings.Contains(note, "dev") || !strings.Contains(note, "a") {
t.Errorf("FAIL: a tag by dev got %v, %q, %v; want no token and a note naming dev and a", env, note, err)
}
// An unprotected branch, and a landing's merge: nothing.
id = run("test", devTip, "", "push", "steward", publish)
if env, note, _ = d.DeliverRunToken("test", id+".publish"); len(env) != 0 || !strings.Contains(note, "dev") {
t.Errorf("FAIL: a push on dev got %v, %q; want no token and the branch named", env, note)
}
id = run("test", "pair:"+devTip+":"+trunkTip, "pair:"+devTip+":"+trunkTip, "landing", "steward", publish)
if env, note, _ = d.DeliverRunToken("test", id+".publish"); len(env) != 0 || note == "" {
t.Errorf("FAIL: a landing's merge got %v, %q; want no token, said why", env, note)
}
// A schedule has no starter: refused where one is required, minted
// where none is.
id = run("test", trunkTip, "", "schedule", "", publish)
if env, note, _ = d.DeliverRunToken("test", id+".publish"); len(env) != 0 || !strings.Contains(note, "no starter") {
t.Errorf("FAIL: a scheduled release got %v, %q; want no token, said why", env, note)
}
id = run("test", trunkTip, "", "schedule", "", map[string]oltp.RunJobGrant{"nightly": {Letters: "go"}})
if env, note, _ = d.DeliverRunToken("test", id+".nightly"); env["QUARRY_TOKEN"] == "" {
t.Errorf("FAIL: a scheduled job under the default policy got %v, %q; want a read token on the private repository", env, note)
}
// allowed narrower than the declaration wins.
id = run("test", trunkTip, "", "push", "dev", map[string]oltp.RunJobGrant{"build": {Environment: "narrow", Letters: "gio"}})
env, note, _ = d.DeliverRunToken("test", id+".build")
if toks, _ := db.RunTokens(id); env["QUARRY_TOKEN"] == "" || len(toks) != 1 || toks[0].Ceiling != "go" {
t.Errorf("FAIL: contents: write in an environment allowing o g gave %+v (%q); want go", toks, note)
}
// Nothing past the public on a public repository: nothing minted, nothing said.
id = run("pub", trunkTip, "", "push", "dev", map[string]oltp.RunJobGrant{"test": {Letters: "go"}})
if env, note, _ = d.DeliverRunToken("pub", id+".test"); len(env) != 0 || note != "" {
t.Errorf("FAIL: contents: read on a public repository got %v, %q; want nothing at all", env, note)
}
// A job the run's start does not name is minted nothing.
if env, _, _ = d.DeliverRunToken("test", id+".other"); len(env) != 0 {
t.Errorf("FAIL: an unrecorded job got %v", env)
}
// A re-run asked by a holder of a gets the release token.
id = run("test", trunkTip, "", "workflow_dispatch", "steward", publish)
if env, note, _ = d.DeliverRunToken("test", id+".publish"); env["QUARRY_TOKEN"] == "" {
t.Errorf("FAIL: a re-run by a steward got %v, %q; want a token", env, note)
}
// A run started by a run token's login never mints, whatever it holds.
id = run("test", trunkTip, "", "workflow_dispatch", "run-abc", map[string]oltp.RunJobGrant{"nightly": {Letters: "go"}})
if env, note, _ = d.DeliverRunToken("test", id+".nightly"); len(env) != 0 || note == "" {
t.Errorf("FAIL: a run started by a run login got %v, %q; want nothing, said why", env, note)
}
// A run the forge has no record of is told so, after a short wait.
if env, note, _ = d.DeliverRunToken("test", "forged.publish"); len(env) != 0 || !strings.Contains(note, "no record") {
t.Errorf("FAIL: a forged task got %v, %q; want nothing and a note", env, note)
}
// A run whose start lands while its job waits is minted for.
late := "runlate"
go func() {
time.Sleep(20 * time.Millisecond)
db.AddCIRun(oltp.CIRun{RunID: late, Repo: "test", Checkin: trunkTip, Workflow: "w", Pipeline: "p", Status: "running"})
db.RecordCIRunStart(late, "push", "steward", map[string]oltp.RunJobGrant{"nightly": {Letters: "go"}})
}()
runStartWait = 2 * time.Second
if env, note, _ = d.DeliverRunToken("test", late+".nightly"); env["QUARRY_TOKEN"] == "" {
t.Errorf("FAIL: a run recorded while its job asked got %v, %q; want a token", env, note)
}
// ci.trigger is refused to a run login.
if _, err := r.Invoke(ops.WithUser(context.Background(), "run-abc"), "ci.trigger", ciTriggerIn{Repo: "test"}); err == nil || !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: ci.trigger by a run login answered %v; want forbidden", err)
}
t.Log("ok: protected branch and environment rules, the starter letter, schedules, allowed, the public floor")
}
func ptr(s string) *string { return &s }