package oltp
import (
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/xfer"
)
// runDB is a forge with a private repository, a public one, and a run
// still going on the private one.
func runDB(t *testing.T) *DB {
t.Helper()
db := tokenDB(t, "pub")
if err := db.AddRepo("priv", t.TempDir(), "", "private"); err != nil {
t.Fatal(err)
}
if err := db.AddCIRun(CIRun{RunID: "r1", Repo: "priv", Checkin: "c1", Workflow: "w", Pipeline: "ci/release.yml", Status: "running"}); err != nil {
t.Fatal(err)
}
return db
}
func mintRun(t *testing.T, db *DB, run, repo, job, letters string) (string, string, Token) {
t.Helper()
login, value, tok, err := db.MintRunToken(RunTokenSpec{Run: run, Repo: repo, Job: job, Letters: letters,
Expires: time.Now().Add(time.Hour)})
if err != nil {
t.Fatal(err)
}
return login, value, tok
}
// A RUN TOKEN IS A TOKEN ROW WITH NO ACCOUNT, ITS LOGIN run-<id>.
func TestARunTokenHasItsOwnLoginAndShape(t *testing.T) {
db := runDB(t)
login, value, tok := mintRun(t, db, "r1", "priv", "publish", "y")
if login != "run-"+tok.ID || !strings.HasPrefix(value, "qrt_"+tok.ID+"_") || !LooksLikeToken(value) {
t.Errorf("FAIL: login %q value %q id %q; want run-<id> and qrt_<id>_…", login, value, tok.ID)
}
var account, run, verifier, secret, scope, repos, ceiling string
var sealed int
if err := db.db.QueryRow(`SELECT account, run, verifier, sealed, sync_secret, scope, repos, ceiling FROM token WHERE id = ?`, tok.ID).
Scan(&account, &run, &verifier, &sealed, &secret, &scope, &repos, &ceiling); err != nil {
t.Fatal(err)
}
if account != "" || run != "r1" || sealed != 0 || verifier != hashToken(value) || repos != "priv" || ceiling != "goy" {
t.Errorf("FAIL: row account=%q run=%q sealed=%d repos=%q ceiling=%q; want no account, run r1, hashed, priv, goy (y and the read letters)", account, run, sealed, repos, ceiling)
}
if scope != "sync,api" {
t.Errorf("FAIL: scope %q; want sync,api for a y token", scope)
}
if want := xfer.SharedSecret("CODE-priv", login, value); secret != want {
t.Errorf("FAIL: sync_secret is not the one repository's derived secret")
}
_, _, readOnly := mintRun(t, db, "r1", "priv", "test", "go")
var s2, sc2 string
db.db.QueryRow(`SELECT sync_secret, scope FROM token WHERE id = ?`, readOnly.ID).Scan(&s2, &sc2)
if s2 != "" || sc2 != "sync,api" {
t.Errorf("FAIL: a read-only run token kept sync_secret %q, scope %q; want none, and sync,api for the Basic round", s2, sc2)
}
t.Log("ok: run-<id> login, qrt_ value, hashed, one repository, a push secret only with i or y")
}
// The login resolves at every door by its name alone, to the run's
// letters on its one repository and the public's elsewhere.
func TestARunTokenResolvesByItsLogin(t *testing.T) {
db := runDB(t)
login, value, tok := mintRun(t, db, "r1", "priv", "publish", "y")
if _, ok := db.ResolveToken(login, value, ScopeAPI); !ok {
t.Errorf("FAIL: the run token was refused at the API door")
}
if _, ok := db.ResolveToken(login, value, ScopeWeb); ok {
t.Errorf("FAIL: a run token opened the web door")
}
if _, ok := db.ResolveToken("run-"+tok.ID+"x", value, ScopeAPI); ok {
t.Errorf("FAIL: a run token resolved under another login")
}
if _, ok := db.ResolveToken("someone", value, ScopeAPI); ok {
t.Errorf("FAIL: a run token resolved for an account")
}
if got := db.EffectiveCaps(login, "priv"); !got.HasAny("y") || !got.HasAny("g") || !got.HasAny("o") || got.HasAny("i") {
t.Errorf("FAIL: caps on priv = %q; want y with the read letters, no i", got)
}
if got := db.EffectiveCaps(login, "pub"); got.HasAny("y") {
t.Errorf("FAIL: caps on another repository = %q; want the public's only", got)
}
creds := db.Secrets("priv", login)
if len(creds) != 1 || creds[0].CheckIn || !creds[0].Publish || creds[0].Token != tok.ID {
t.Errorf("FAIL: Secrets = %+v; want one unversioned-only credential naming the token", creds)
}
if len(db.Secrets("pub", login)) != 0 {
t.Errorf("FAIL: the run token signs for a repository it does not name")
}
if _, ok := db.LiveToken(login, tok.ID); !ok {
t.Errorf("FAIL: LiveToken does not find the run token")
}
t.Log("ok: resolved by login at the api door, not the web; letters on its repository only; a y-only xfer credential")
}
// CLOSE, CANCEL, EXPIRY AND THE STARTUP SWEEP ALL END IT.
func TestARunTokenEndsWithItsRun(t *testing.T) {
db := runDB(t)
login, value, tok := mintRun(t, db, "r1", "priv", "publish", "y")
if err := db.CompleteCIRun("r1", "succeeded", ""); err != nil {
t.Fatal(err)
}
if _, ok := db.ResolveToken(login, value, ScopeAPI); ok {
t.Errorf("FAIL: the token outlived its run's close")
}
if len(db.Secrets("priv", login)) != 0 || db.EffectiveCaps(login, "priv").HasAny("y") {
t.Errorf("FAIL: the closed run's login still signs or holds y")
}
var secret, revoked string
db.db.QueryRow(`SELECT sync_secret, revoked FROM token WHERE id = ?`, tok.ID).Scan(&secret, &revoked)
if secret != "" || revoked == "" {
t.Errorf("FAIL: after close sync_secret=%q revoked=%q; want cleared and stamped", secret, revoked)
}
toks, err := db.RunTokens("r1")
if err != nil || len(toks) != 1 || toks[0].ID != tok.ID || toks[0].Revoked == "" || toks[0].Ceiling != "goy" {
t.Errorf("FAIL: RunTokens = %+v, %v; want the revoked row, its letters kept", toks, err)
}
if _, _, _, err := db.MintRunToken(RunTokenSpec{Run: "r1", Repo: "priv", Job: "late", Letters: "y", Expires: time.Now().Add(time.Hour)}); err == nil {
t.Errorf("FAIL: a token was minted for a run already over")
}
// A cancel by hand, and the forge ending a run it never ran.
db.AddCIRun(CIRun{RunID: "r2", Repo: "priv", Checkin: "c2", Workflow: "w", Pipeline: "p", Status: "running"})
l2, v2, _ := mintRun(t, db, "r2", "priv", "publish", "y")
if err := db.RevokeRunTokens("r2"); err != nil {
t.Fatal(err)
}
if _, ok := db.ResolveToken(l2, v2, ScopeAPI); ok {
t.Errorf("FAIL: the token outlived its run's cancel")
}
db.AddCIRun(CIRun{RunID: "r3", Repo: "priv", Checkin: "c3", Workflow: "w", Pipeline: "p", Status: "running"})
l3, v3, _ := mintRun(t, db, "r3", "priv", "publish", "y")
if _, err := db.EndCIRun("r3", "cancelled", "no worker"); err != nil {
t.Fatal(err)
}
if _, ok := db.ResolveToken(l3, v3, ScopeAPI); ok {
t.Errorf("FAIL: the token outlived its run's expiry")
}
// The sweep: a run gone from the work list, by whatever path,
// takes its token with it; a live run keeps its own.
db.AddCIRun(CIRun{RunID: "r4", Repo: "priv", Checkin: "c4", Workflow: "w", Pipeline: "p", Status: "running"})
db.AddCIRun(CIRun{RunID: "r5", Repo: "priv", Checkin: "c5", Workflow: "w", Pipeline: "p", Status: "running"})
l4, v4, _ := mintRun(t, db, "r4", "priv", "publish", "y")
l5, v5, _ := mintRun(t, db, "r5", "priv", "publish", "y")
db.w.Exec(`UPDATE ci_run SET status = 'failed' WHERE run_id = 'r4'`)
if n, err := db.SweepRunTokens(); err != nil || n != 1 {
t.Errorf("FAIL: the sweep revoked %d, %v; want 1", n, err)
}
if _, ok := db.ResolveToken(l4, v4, ScopeAPI); ok {
t.Errorf("FAIL: the sweep left a finished run's token live")
}
if _, ok := db.ResolveToken(l5, v5, ScopeAPI); !ok {
t.Errorf("FAIL: the sweep took a live run's token")
}
t.Log("ok: close, cancel, expiry and the sweep revoke; the row and its letters stay")
}
// A retried job's second token replaces its first.
func TestAJobHoldsOneLiveRunToken(t *testing.T) {
db := runDB(t)
l1, v1, _ := mintRun(t, db, "r1", "priv", "publish", "y")
l2, v2, _ := mintRun(t, db, "r1", "priv", "publish", "y")
if _, ok := db.ResolveToken(l1, v1, ScopeAPI); ok {
t.Errorf("FAIL: the first token of a job stayed live beside the second")
}
if _, ok := db.ResolveToken(l2, v2, ScopeAPI); !ok {
t.Errorf("FAIL: the second token was refused")
}
t.Log("ok: one live token per job of a run")
}
// ENVIRONMENTS CARRY allowed AND A REQUIRED STARTER LETTER.
func TestEnvironmentsCarryAllowedAndAStarterLetter(t *testing.T) {
db := runDB(t)
p, err := db.EnvironmentPolicy("priv", "")
if err != nil || p.Exists || p.Allowed != DefaultAllowed || p.Starter != "" {
t.Errorf("FAIL: the default policy = %+v, %v; want allowed %q and no starter letter", p, err, DefaultAllowed)
}
if err := db.SetEnvironment("priv", "release", nil, "dan"); err != nil {
t.Fatal(err)
}
if err := db.SetEnvironmentTokens("priv", "release", "y", "a", "dan"); err != nil {
t.Fatal(err)
}
p, err = db.EnvironmentPolicy("priv", "release")
if err != nil || !p.Exists || p.Allowed != "y" || p.Starter != "a" {
t.Errorf("FAIL: release = %+v, %v; want allowed y, starter a", p, err)
}
envs, _ := db.Environments("priv")
if len(envs) != 1 || envs[0].Allowed != "y" || envs[0].Starter != "a" {
t.Errorf("FAIL: Environments = %+v; want release listing allowed and starter", envs)
}
if err := db.SetEnvironmentTokens("priv", "release", "ys", "", "dan"); err == nil {
t.Errorf("FAIL: allowed took s")
}
if err := db.SetEnvironmentTokens("priv", "release", "y", "ab", "dan"); err == nil {
t.Errorf("FAIL: a starter requirement of two letters was taken")
}
if err := db.SetEnvironmentTokens("priv", "nope", "y", "", "dan"); err == nil {
t.Errorf("FAIL: allowed was set on an environment that does not exist")
}
// The default policy is the "" row, made on first use.
if err := db.SetEnvironmentTokens("priv", "", "go", "", "dan"); err != nil {
t.Fatal(err)
}
if p, _ := db.EnvironmentPolicy("priv", ""); p.Allowed != "go" {
t.Errorf("FAIL: the default policy's allowed = %q; want go", p.Allowed)
}
t.Log("ok: allowed defaults to o g 8, a starter letter is optional and one letter, the default policy is settable")
}
// A run's start is kept with it: event, starter, the jobs' grants.
func TestARunKeepsItsStart(t *testing.T) {
db := runDB(t)
if err := db.RecordCIRunStart("r1", "tag", "tagger", map[string]RunJobGrant{"publish": {Environment: "release", Letters: "y", Timeout: 20}}); err != nil {
t.Fatal(err)
}
ev, starter, jobs, err := db.CIRunStart("r1")
if err != nil || ev != "tag" || starter != "tagger" || jobs["publish"] != (RunJobGrant{Environment: "release", Letters: "y", Timeout: 20}) {
t.Errorf("FAIL: CIRunStart = %q %q %+v %v", ev, starter, jobs, err)
}
t.Log("ok: the start is recorded beside the run")
}