package main
import (
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/oltp"
)
// registrar.Add refuses a reserved name or a quota overrun BEFORE it
// resolves a path or touches a store on disk — both checks sit ahead of
// spec.Path resolution — so a bare RepoSpec (no Create, no Clone, no
// Path) reaches them without needing the rest of a running forge (zit,
// events, leases) wired up.
func newTestRegistrar(t *testing.T, quota int) *registrar {
t.Helper()
db, err := oltp.Open(filepath.Join(t.TempDir(), "oltp.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
return ®istrar{odb: db, deps: &api.Deps{Repos: api.NewRepos(nil)}, repoQuota: quota}
}
// A repository named after a route the forge keeps for itself is
// refused before anything else is checked — "new" would sit under /new
// itself, "admin" would read as belonging to the operator — and the
// check is case-insensitive.
func TestAddRefusesReservedNames(t *testing.T) {
rg := newTestRegistrar(t, 25)
if err := rg.odb.AddAccount("dan", "pw12345678", false); err != nil {
t.Fatal(err)
}
for _, name := range []string{"new", "fleet", "admin", "New", "ADMIN"} {
_, err := rg.Add(api.RepoSpec{Name: name, Author: "dan"})
if err == nil || !strings.Contains(err.Error(), "keeps for its own routes") {
t.Errorf("Add(%q) = %v, want a reserved-name refusal", name, err)
}
}
// An ordinary name is untouched by the reserved list: it reaches
// the NEXT check (path resolution) rather than being refused here.
if _, err := rg.Add(api.RepoSpec{Name: "ordinary", Author: "dan"}); err == nil ||
strings.Contains(err.Error(), "keeps for its own routes") {
t.Errorf("an ordinary name was caught by the reserved-name check: %v", err)
}
}
// SyncReadGate is what a `zit clone`/`zit pull` against this forge's
// sync endpoint actually consults. It refuses a repository that is
// private OR not mirrorable, and allows an ordinary public, mirrorable
// one — the same invisible-not-forbidden posture either way (both
// answer as a 404 to the caller, so a stranger cannot tell "not
// mirrorable" from "not there").
func TestSyncReadGateRefusesNonMirrorableRepositories(t *testing.T) {
rg := newTestRegistrar(t, 25)
if err := rg.odb.AddRepo("open", t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
if err := rg.odb.AddRepo("closed", t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
if err := rg.odb.SetRepoMirrorable("closed", false); err != nil {
t.Fatal(err)
}
if err := rg.odb.AddRepo("vault", t.TempDir(), "", oltp.Private); err != nil {
t.Fatal(err)
}
rg.refreshPrivate() // CanRead reads the cached set, not the table directly
req := httptest.NewRequest(http.MethodGet, "/xfer/open", nil)
if !rg.SyncReadGate(req, "open") {
t.Error("an ordinary public, mirrorable repository was refused")
}
if rg.SyncReadGate(req, "closed") {
t.Error("a repository with mirrorable=false was allowed to be cloned")
}
if rg.SyncReadGate(req, "vault") {
t.Error("a private repository was allowed to be cloned anonymously")
}
if rg.SyncReadGate(req, "nope") {
t.Error("an unregistered repository answered yes")
}
}
// The quota refuses the (quota+1)th repository an account would
// administer, and NAMES the number in the refusal. A site admin is
// never subject to it, regardless of how many repositories they hold.
func TestAddEnforcesThePerAccountQuota(t *testing.T) {
rg := newTestRegistrar(t, 2)
if err := rg.odb.AddAccount("dan", "pw12345678", false); err != nil {
t.Fatal(err)
}
if err := rg.odb.Grant("dan", "existing-1", oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
if err := rg.odb.Grant("dan", "existing-2", oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
_, err := rg.Add(api.RepoSpec{Name: "one-too-many", Author: "dan"})
if err == nil || !strings.Contains(err.Error(), "administers 2 repositories") {
t.Fatalf("quota did not refuse (and name the number) for the third repository: %v", err)
}
if err := rg.odb.AddAccount("boss", "pw12345678", true); err != nil {
t.Fatal(err)
}
for _, name := range []string{"existing-3", "existing-4", "existing-5"} {
if err := rg.odb.Grant("boss", name, oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
}
if _, err := rg.Add(api.RepoSpec{Name: "boss-repo", Author: "boss"}); err == nil ||
strings.Contains(err.Error(), "administers") {
t.Fatalf("a site admin was refused by the quota: %v", err)
}
}