package main
import (
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/xfer"
)
// syncFront is the sync lane as serve wires it: the fronter over a real
// zit store, the forge's token store as its credentials, and the
// registrar's read gate. vault is private; jan holds o, i and y on it.
type syncFront struct {
rg *registrar
base string
home string
}
func newSyncFront(t *testing.T) syncFront {
t.Helper()
for _, bin := range []string{"zit", "fossil"} {
if _, err := exec.LookPath(bin); err != nil {
if os.Getenv("CI") == "true" {
t.Fatalf("FAIL: %s not on PATH under CI", bin)
}
t.Skip(bin + " not on PATH")
}
}
store := t.TempDir()
run := func(args ...string) {
t.Helper()
cmd := exec.Command("zit", args...)
cmd.Dir = store
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("zit %v: %v\n%s", args, err, out)
}
}
run("init")
if err := os.WriteFile(filepath.Join(store, "README.md"), []byte("# vault\n"), 0o644); err != nil {
t.Fatal(err)
}
run("add", "README.md")
run("commit", "-m", "first")
code, err := xfer.ProjectCode(store)
if err != nil {
t.Fatal(err)
}
rg := newTestRegistrar(t, 25)
db := rg.odb
db.SyncProjectCode = func(string) (string, bool) { return code, true }
if err := db.AddAccount("jan", "pw-jan-12345", false); err != nil {
t.Fatal(err)
}
for _, repo := range []string{"vault", "other"} {
if err := db.AddRepo(repo, t.TempDir(), "", oltp.Private); err != nil {
t.Fatal(err)
}
if err := db.SetCaps("jan", repo, "oiy"); err != nil {
t.Fatal(err)
}
}
rg.refreshPrivate()
rg.doors = &tokenDoors{db: db}
f := xfer.New()
t.Cleanup(f.Close)
f.Register("vault", store)
f.SetAuth(db)
f.SetLoginLimit(rg.doors)
f.SetReadGate(rg.SyncReadGate)
mux := http.NewServeMux()
mux.Handle("POST /r/{repo}", f)
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return syncFront{rg: rg, base: srv.URL + "/r/vault", home: t.TempDir()}
}
func (s syncFront) as(token string) string {
return strings.Replace(s.base, "http://", "http://jan:"+token+"@", 1)
}
func (s syncFront) fossil(dir string, args ...string) (string, error) {
cmd := exec.Command("fossil", args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "HOME="+s.home, "FOSSIL_HOME="+s.home, "FOSSIL_USER=jan")
out, err := cmd.CombinedOutput()
return string(out), err
}
func (s syncFront) mint(t *testing.T, spec oltp.TokenSpec) string {
t.Helper()
spec.Name, spec.Scopes = "t", []string{oltp.ScopeSync}
v, _, err := s.rg.odb.MintToken("jan", spec)
if err != nil {
t.Fatal(err)
}
return v
}
// A token's repositories and ceiling bind the sync lane's reads as they
// bind its pushes, on the Basic round and on every login card after it.
func TestATokensScopeBindsFossilReads(t *testing.T) {
s := newSyncFront(t)
full := s.mint(t, oltp.TokenSpec{})
elsewhere := s.mint(t, oltp.TokenSpec{Repos: []string{"other"}})
uvOnly := s.mint(t, oltp.TokenSpec{Ceiling: "y"})
dir := t.TempDir()
out, err := s.fossil(dir, "clone", "-B", "jan:"+elsewhere, s.as(elsewhere), filepath.Join(dir, "no.fossil"))
if err == nil {
t.Errorf("FAIL: a clone with a token naming only another repository succeeded:\n%s", out)
} else {
t.Log("ok: a token naming another repository cannot clone")
}
out, err = s.fossil(dir, "clone", "-B", "jan:"+uvOnly, s.as(uvOnly), filepath.Join(dir, "uv.fossil"))
if err == nil {
t.Errorf("FAIL: a clone with a token ceilinged to y succeeded:\n%s", out)
} else {
t.Log("ok: a token ceilinged to y cannot clone a private repository")
}
repo := filepath.Join(dir, "a.fossil")
if out, err := s.fossil(dir, "clone", "-B", "jan:"+full, s.as(full), repo); err != nil {
t.Fatalf("FAIL: a clone with the full token: %v\n%s", err, out)
}
// A pull from the existing clone signs a login card from the first
// round: the card verifies (the token holds y), and the read must
// still be the token's, not the account's.
out, err = s.fossil(dir, "pull", s.as(uvOnly), "-R", repo, "--once")
if err == nil {
t.Errorf("FAIL: a pull signed by a y-only token read a private repository:\n%s", out)
} else {
t.Log("ok: a login card signed by a y-only token reads nothing")
}
if out, err := s.fossil(dir, "pull", s.as(full), "-R", repo, "--once"); err != nil {
t.Errorf("FAIL: a pull with the full token: %v\n%s", err, out)
}
}