package api
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/uv"
)
// A sync token ceilinged to y (RFC 0021) signs a stock fossil client's
// unversioned push, and the same token signing a check-in is refused
// whole: the login card resolves to y without i, and the gate moved to
// the cards.
func TestAYOnlyTokenPushesUnversionedButNotCheckIns(t *testing.T) {
g := newUVFossilRig(t, 1<<20)
// The forge's own credential store behind the front, in place of the
// rig's password fake.
g.db.SyncProjectCode = func(string) (string, bool) { return g.code, true }
if err := g.db.AddAccount("pub", "pw-pub-unused", false); err != nil {
t.Fatal(err)
}
if err := g.db.SetCaps("pub", "test", "oiy"); err != nil {
t.Fatal(err)
}
token, _, err := g.db.MintToken("pub", oltp.TokenSpec{Name: "release", Scopes: []string{oltp.ScopeSync}, Ceiling: "y"})
if err != nil {
t.Fatal(err)
}
g.front.SetAuth(g.db)
withToken := strings.Replace(g.base, "http://", "http://pub:"+token+"@", 1)
a := g.clone("a")
g.uvAdd(a, "release.tar", "bytes\n", "2026-01-01 00:00:00")
out, err := g.fossil(filepath.Dir(a), "sync", "-u", withToken, "-R", a)
o, inBucket := g.objects.has(uv.Key("test", "release.tar"))
check(t, err == nil && inBucket && string(o.body) == "bytes\n",
"a y-only token publishes an unversioned file",
"uv push with a y-only token: err %v, in bucket %v\n%s", err, inBucket, out)
co := t.TempDir()
g.must(co, "settings", "autosync", "off", "-R", a)
g.must(co, "open", a, "--nosync")
if err := os.WriteFile(filepath.Join(co, "NEW.md"), []byte("by a uv token\n"), 0o644); err != nil {
t.Fatal(err)
}
g.must(co, "add", "NEW.md")
g.fossil(co, "commit", "-m", "by a uv token", "--no-warnings")
out, _ = g.fossil(co, "push", withToken, "--once")
check(t, !strings.Contains(g.timeline(), "by a uv token") && strings.Contains(out, "unversioned files only"),
"the same token pushing a check-in is refused whole",
"check-in push with a y-only token:\n%s", out)
}
// Two sync tokens on one account are two sessions: a stock fossil push
// signed with either lands, and revoking one leaves the other pushing.
// This is the concurrent-sessions case that one-token-per-account broke.
func TestTwoSyncTokensPushAndRevokingOneLeavesTheOther(t *testing.T) {
g := newUVFossilRig(t, 1<<20)
g.db.SyncProjectCode = func(string) (string, bool) { return g.code, true }
if err := g.db.AddAccount("dev", "pw-dev-unused", false); err != nil {
t.Fatal(err)
}
if err := g.db.SetCaps("dev", "test", "oi"); err != nil {
t.Fatal(err)
}
one, tokOne, err := g.db.MintToken("dev", oltp.TokenSpec{Name: "session 1", Scopes: []string{oltp.ScopeSync}})
if err != nil {
t.Fatal(err)
}
two, _, err := g.db.MintToken("dev", oltp.TokenSpec{Name: "session 2", Scopes: []string{oltp.ScopeSync}})
if err != nil {
t.Fatal(err)
}
g.front.SetAuth(g.db)
as := func(token string) string { return strings.Replace(g.base, "http://", "http://dev:"+token+"@", 1) }
co := t.TempDir()
a := filepath.Join(t.TempDir(), "a.fossil")
g.must(filepath.Dir(a), "clone", as(one), a)
g.must(co, "settings", "autosync", "off", "-R", a)
g.must(co, "open", a, "--nosync")
commit := func(msg string) {
t.Helper()
// A file per commit: same size and second would read as unchanged.
name := strings.ReplaceAll(msg, " ", "-") + ".md"
if err := os.WriteFile(filepath.Join(co, name), []byte(msg+"\n"), 0o644); err != nil {
t.Fatal(err)
}
g.must(co, "add", name)
g.must(co, "commit", "-m", msg, "--no-warnings")
}
commit("pushed with one")
out, err := g.fossil(co, "push", as(one), "--once")
check(t, err == nil && strings.Contains(g.timeline(), "pushed with one"),
"a push signed with the first token lands", "push with token one: %v\n%s", err, out)
commit("pushed with two")
out, err = g.fossil(co, "push", as(two), "--once")
check(t, err == nil && strings.Contains(g.timeline(), "pushed with two"),
"a push signed with the second token lands", "push with token two: %v\n%s", err, out)
if err := g.db.RevokeToken("dev", tokOne.ID); err != nil {
t.Fatal(err)
}
commit("after the revoke")
out, _ = g.fossil(co, "push", as(one), "--once")
check(t, !strings.Contains(g.timeline(), "after the revoke"),
"the revoked token's push is refused", "push with a revoked token landed:\n%s", out)
out, err = g.fossil(co, "push", as(two), "--once")
check(t, err == nil && strings.Contains(g.timeline(), "after the revoke"),
"the other token still pushes", "push with the surviving token: %v\n%s", err, out)
}
// A RUN TOKEN PUBLISHES WHILE ITS RUN LIVES AND NOT AFTER (RFC 0021
// §runs). A stock fossil client is told the login run-<id> and the
// token as its password and needs nothing else: the front reads the
// secret from the run's row. y alone pushes a uv card, never a
// check-in; the run's close, or a cancel, ends it at once.
func TestARunTokenPublishesWhileItsRunLives(t *testing.T) {
g := newUVFossilRig(t, 1<<20)
g.db.SyncProjectCode = func(string) (string, bool) { return g.code, true }
for _, id := range []string{"r1", "r2"} {
if err := g.db.AddCIRun(oltp.CIRun{RunID: id, Repo: "test", Checkin: "c", Workflow: "w", Pipeline: "ci/release.yml", Status: "running"}); err != nil {
t.Fatal(err)
}
}
mint := func(run string) string {
login, value, _, err := g.db.MintRunToken(oltp.RunTokenSpec{Run: run, Repo: "test", Job: "publish", Letters: "y", Expires: time.Now().Add(time.Hour)})
if err != nil {
t.Fatal(err)
}
return strings.Replace(g.base, "http://", "http://"+login+":"+value+"@", 1)
}
g.front.SetAuth(g.db)
as := mint("r1")
a := g.clone("a")
g.uvAdd(a, "release.tar", "bytes\n", "2026-01-01 00:00:00")
out, err := g.fossil(filepath.Dir(a), "sync", "-u", as, "-R", a)
o, inBucket := g.objects.has(uv.Key("test", "release.tar"))
check(t, err == nil && inBucket && string(o.body) == "bytes\n",
"a y run token publishes an unversioned file",
"uv push with a run token: err %v, in bucket %v\n%s", err, inBucket, out)
co := t.TempDir()
g.must(co, "settings", "autosync", "off", "-R", a)
g.must(co, "open", a, "--nosync")
if err := os.WriteFile(filepath.Join(co, "NEW.md"), []byte("by a run token\n"), 0o644); err != nil {
t.Fatal(err)
}
g.must(co, "add", "NEW.md")
g.fossil(co, "commit", "-m", "by a run token", "--no-warnings")
out, _ = g.fossil(co, "push", as, "--once")
check(t, !strings.Contains(g.timeline(), "by a run token") && strings.Contains(out, "unversioned files only"),
"the run token pushing a check-in is refused whole",
"check-in push with a uv: write run token:\n%s", out)
if err := g.db.CompleteCIRun("r1", "succeeded", ""); err != nil {
t.Fatal(err)
}
g.uvAdd(a, "late.tar", "late\n", "2026-01-02 00:00:00")
out, _ = g.fossil(filepath.Dir(a), "sync", "-u", as, "-R", a)
_, late := g.objects.has(uv.Key("test", "late.tar"))
check(t, !late && strings.Contains(out, "is over"), "the run token is refused once its run closed, saying so",
"uv push after the run closed landed:\n%s", out)
as = mint("r2")
if err := g.db.RevokeRunTokens("r2"); err != nil { // the cancel's revoke
t.Fatal(err)
}
out, _ = g.fossil(filepath.Dir(a), "sync", "-u", as, "-R", a)
_, late = g.objects.has(uv.Key("test", "late.tar"))
check(t, !late && strings.Contains(out, "is over"), "the run token is refused once its run is cancelled, saying so",
"uv push after the cancel landed:\n%s", out)
}