package api
import (
"os"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/uv"
"craftdesign.group/quarry/internal/xfer"
)
// 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) {
// The forge's own credential store behind the front, in place of the
// rig's password fake.
g := newUVFossilRigAuth(t, 1<<20, forgeAuth)
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)
}
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 := newUVFossilRigAuth(t, 1<<20, forgeAuth)
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)
}
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)
}
// forgeAuth puts the forge's own token store behind the rig's front.
func forgeAuth(g *uvFossilRig) xfer.Auth {
g.db.SyncProjectCode = func(string) (string, bool) { return g.code, true }
return g.db
}