package api
import (
"os"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/uv"
)
// Stock fossil answers an anonymous `sync -u` against a repository it
// will not take pushes on with "pull only - not authorized to push",
// pulls what is new, and exits 0. The front used to refuse the whole
// request on the push card fossil sends with every sync (quarry #324).
const pullOnlyNote = "pull only - not authorized to push"
// serverCheckIn adds a check-in to the repository the front serves.
func (g *uvFossilRig) serverCheckIn(name, content, msg string) {
g.t.Helper()
if err := os.WriteFile(filepath.Join(g.origin, name), []byte(content), 0o644); err != nil {
g.t.Fatal(err)
}
g.zit("add", name)
g.zit("commit", "-m", msg)
}
func TestAnonymousSyncPullsAndPushesNothing(t *testing.T) {
g := newUVFossilRig(t, 1<<20)
a := g.clone("a")
g.uvAdd(a, "old.txt", "old\n", "2026-01-01 00:00:00")
g.must(filepath.Dir(a), "sync", "-u", g.as("pub"), "-R", a)
b := g.clone("b", "-u")
// The server moves on: a new check-in and a new unversioned file.
g.serverCheckIn("NEWS.md", "news\n", "server moved on")
g.uvAdd(a, "new.txt", "new\n", "2026-01-02 00:00:00")
g.must(filepath.Dir(a), "sync", "-u", g.as("pub"), "-R", a)
// The reader holds an unversioned file of its own, which it must
// not be able to publish.
g.uvAdd(b, "mine.txt", "mine\n", "2026-01-03 00:00:00")
out, err := g.fossil(filepath.Dir(b), "sync", "-u", g.base, "-R", b)
check(t, err == nil, "an anonymous sync -u exits 0", "anonymous sync -u: %v\n%s", err, out)
check(t, strings.Contains(out, pullOnlyNote), "the reader is told it may only pull", "no pull-only note:\n%s", out)
tl := g.must(filepath.Dir(b), "timeline", "-R", b)
check(t, strings.Contains(tl, "server moved on"), "the new check-in was pulled", "timeline of the reader:\n%s", tl)
got, _ := g.fossil(filepath.Dir(b), "uv", "cat", "new.txt", "-R", b)
check(t, got == "new\n", "the new unversioned file was pulled", "uv cat new.txt = %q", got)
_, pushed := g.objects.has(uv.Key("test", "mine.txt"))
_, row := g.row("mine.txt")
check(t, !pushed && !row, "the reader's own unversioned file was not published", "bucket %v, catalog %v", pushed, row)
}
// A write-bearing card in an anonymous request refuses the whole
// request, the pull in it too.
func TestAnonymousRequestWithAFileCardIsRefusedWhole(t *testing.T) {
g := newUVFossilRig(t, 1<<20)
for _, card := range []string{"config /project 4\nabcd\n", "cfile " + strings.Repeat("a", 40) + " 4\nabcd\n",
"uvfile x.txt 1767225600 " + uv.Hash([]byte("x")) + " 1 0\nx"} {
reply := g.exchange("", "", "push x "+g.code+"\npull x "+g.code+"\n"+card)
check(t, strings.Contains(reply, "error anonymous\\spush\\sis\\snot\\saccepted") && !strings.Contains(reply, "message "),
"an anonymous request with "+strings.Fields(card)[0]+" is refused whole",
"%s request:\n%s", strings.Fields(card)[0], reply)
}
// And with nothing to write it is a pull, told so.
reply := g.exchange("", "", "push x "+g.code+"\npull x "+g.code+"\n")
check(t, !strings.Contains(reply, "error ") && strings.Contains(reply, "message pull\\sonly"),
"an anonymous push card alone is answered as a pull", "reply:\n%s", reply)
}
// A signed-in reader whose account holds no check-in letter is
// answered as stock fossil answers one without push capability.
func TestSignedInReaderWithoutCheckInPulls(t *testing.T) {
g := newUVFossilRigWith(t, 1<<20, forgeFront)
if err := g.db.AddAccount("rdr", "rdrpw", false); err != nil {
t.Fatal(err)
}
if err := g.db.SetCaps("rdr", "test", "o"); err != nil {
t.Fatal(err)
}
b := g.clone("b")
g.serverCheckIn("NEWS.md", "news\n", "server moved on")
url := strings.Replace(g.base, "http://", "http://rdr:rdrpw@", 1)
out, err := g.fossil(filepath.Dir(b), "sync", url, "-R", b)
check(t, err == nil && strings.Contains(out, pullOnlyNote), "a signed-in reader without i syncs pull only", "reader sync: %v\n%s", err, out)
check(t, strings.Contains(g.must(filepath.Dir(b), "timeline", "-R", b), "server moved on"), "and pulls the new check-in", "not pulled")
}