Skip to content
quarry Artifact [53874b0b90]
Login

Artifact [53874b0b90]

Artifact 53874b0b906cb52304a3224e8bcb0cbf551a352dd0f13d61f4c78d9fa15ce7b0:


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 &registrar{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)
	}
	// The quota counts the deed, not reach: these are repositories dan
	// OWNS. An admin grant on somebody else's repository is asserted
	// below not to draw on the allowance.
	for _, name := range []string{"existing-1", "existing-2"} {
		if err := rg.odb.AddRepo(name, t.TempDir(), "", oltp.Public); err != nil {
			t.Fatal(err)
		}
		if err := rg.odb.SetOwner(name, "dan"); err != nil {
			t.Fatal(err)
		}
	}
	if err := rg.odb.AddRepo("someone-elses", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	if err := rg.odb.SetOwner("someone-elses", "grace"); err != nil {
		t.Fatal(err)
	}
	if err := rg.odb.Grant("dan", "someone-elses", 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(), "owns 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.AddRepo(name, t.TempDir(), "", oltp.Public); err != nil {
			t.Fatal(err)
		}
		if err := rg.odb.SetOwner(name, "boss"); 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)
	}
}

// Creating a repository is the act of owning one, so the deed is
// recorded at creation rather than inferred later from capabilities,
// and the namespace starts as the creator's own handle: the personal
// case is meant to need no arranging.
//
// This exercises claim directly rather than through Add. Add reaches
// Register, which needs a serving node this helper does not build --
// the same reason the tests above only cover Add's early refusals.
func TestClaimRecordsTheDeedAndThePersonalNamespace(t *testing.T) {
	rg := newTestRegistrar(t, 25)
	if err := rg.odb.AddRepo("mine", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	rg.claim(api.RepoSpec{Name: "mine", Author: "dan"})
	owner, err := rg.odb.OwnerOf("mine")
	if err != nil {
		t.Fatal(err)
	}
	ns, err := rg.odb.NamespaceOf("mine")
	if err != nil {
		t.Fatal(err)
	}
	if owner != "dan" || ns != "dan" {
		t.Fatalf("owner/namespace = %q/%q, want dan/dan", owner, ns)
	}
	by, at, err := rg.odb.NamespacePlacement("mine")
	if err != nil {
		t.Fatal(err)
	}
	if by != "dan" || at == "" {
		t.Fatalf("placement = %q at %q, want dan at a recorded time", by, at)
	}
}

// A store an operator serves on behalf of nobody keeps an empty deed
// rather than being attributed to whoever ran the command.
func TestClaimWithNoAuthorLeavesTheRepositoryForgeHeld(t *testing.T) {
	rg := newTestRegistrar(t, 25)
	if err := rg.odb.AddRepo("served", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	rg.claim(api.RepoSpec{Name: "served"})
	owner, err := rg.odb.OwnerOf("served")
	if err != nil {
		t.Fatal(err)
	}
	if owner != "" {
		t.Fatalf("owner = %q, want empty -- nobody claimed this store", owner)
	}
}

// The third direction of the name-pool rule. oltp refuses an account or
// team named after a repository; this refuses the reverse, and all
// three together are what make a path's first segment unambiguous.
func TestAddRefusesANameANamespaceHolds(t *testing.T) {
	rg := newTestRegistrar(t, 25)
	if err := rg.odb.AddAccount("ada", "pw12345678", false); err != nil {
		t.Fatal(err)
	}
	if err := rg.odb.AddTeam("hoppers", "Hoppers", "", "ada"); err != nil {
		t.Fatal(err)
	}
	for _, tc := range []struct{ name, why string }{
		{"ada", "an account's name is where its repositories appear"},
		{"hoppers", "a team's name is where its repositories appear"},
	} {
		_, err := rg.Add(api.RepoSpec{Name: tc.name, Author: "ada", Path: t.TempDir(), Create: true})
		if err == nil {
			t.Errorf("FAIL: a repository took %q, which %s", tc.name, tc.why)
			continue
		}
		if !strings.Contains(err.Error(), "cannot take it") {
			t.Errorf("FAIL: %q refused for the wrong reason: %v", tc.name, err)
			continue
		}
		t.Logf("ok: a repository cannot take %q", tc.name)
	}
	// A name nobody holds is NOT refused here: it passes this check and
	// goes on to Register, which this harness cannot build — the same
	// limit the other Add tests work within, so they all assert
	// refusals rather than successes.
}

// A private repository could not be cloned or synced by ANYONE,
// including the account that owns it: SyncReadGate asked MayMirror of
// every caller, and MayMirror answers no for every private repository
// by construction. "May other people keep a copy" had quietly become
// "may anyone read this at all", so the owner was refused their own
// history (b112b205).
func TestOwnerCanSyncTheirOwnPrivateRepository(t *testing.T) {
	rg := newTestRegistrar(t, 25)
	const pw = "pw12345678"
	if err := rg.odb.AddAccount("owner", pw, false); err != nil {
		t.Fatal(err)
	}
	if err := rg.odb.AddRepo("vault", t.TempDir(), "", oltp.Private); err != nil {
		t.Fatal(err)
	}
	// Created private blanks the categories, so nothing is granted by
	// default — standing on a private repository is explicit, which is
	// the whole point of it. Give the owner theirs.
	if err := rg.odb.SetCaps("owner", "vault", "o"); err != nil {
		t.Fatal(err)
	}
	rg.refreshPrivate()

	anon := httptest.NewRequest(http.MethodGet, "/xfer/vault", nil)
	if rg.SyncReadGate(anon, "vault", "") {
		t.Error("a private repository answered yes to a stranger")
	}

	owned := httptest.NewRequest(http.MethodGet, "/xfer/vault", nil)
	owned.SetBasicAuth("owner", pw)
	if !rg.SyncReadGate(owned, "vault", "") {
		t.Error("the owner was refused a clone of their own private repository")
	}

	// A signed-in account with no standing on the repository is still
	// a stranger to it: identifying yourself is not the same as being
	// admitted, and CanRead is what decides that.
	if err := rg.odb.AddAccount("passerby", pw, false); err != nil {
		t.Fatal(err)
	}
	other := httptest.NewRequest(http.MethodGet, "/xfer/vault", nil)
	other.SetBasicAuth("passerby", pw)
	if rg.SyncReadGate(other, "vault", "") {
		t.Error("an unrelated signed-in account was let into a private repository")
	}

	// Wrong password is not identification. This must fall back to the
	// anonymous answer rather than to the authenticated one.
	bad := httptest.NewRequest(http.MethodGet, "/xfer/vault", nil)
	bad.SetBasicAuth("owner", "not-the-password")
	if rg.SyncReadGate(bad, "vault", "") {
		t.Error("a bad password was treated as the owner")
	}

	// THE CASE THIS GATE EXISTED TO REFUSE AND SHOULD NOT HAVE.
	//
	// A sync client does not send HTTP Basic: fossil and zit
	// authenticate with the login card, inside the request body. Every
	// case above uses Basic, which a browser and curl send and no sync
	// client does — so the gate admitted the shapes nobody syncs with
	// and refused every real one. `zit clone` of a private repository
	// answered 404 for its owner exactly as it did for a stranger
	// (b112b205). xfer verifies the card against the repository's
	// shared secret and passes the name it proved.
	card := httptest.NewRequest(http.MethodGet, "/xfer/vault", nil)
	if !rg.SyncReadGate(card, "vault", "owner") {
		t.Error("a verified sync login from the owner was refused: " +
			"a private repository stays unclonable by the people it admits")
	}

	// The login is identity, not admission. Someone the repository does
	// not admit is still refused, however well they proved who they are.
	if rg.SyncReadGate(card, "vault", "passerby") {
		t.Error("a verified login from an account with no standing was let in")
	}
}

// mirrorable=false still refuses a stranger on a public repository —
// that flag's actual job, and the case the owner fix must not widen.
func TestMirrorableStillRefusesStrangers(t *testing.T) {
	rg := newTestRegistrar(t, 25)
	const pw = "pw12345678"
	if err := rg.odb.AddAccount("keeper", pw, false); 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)
	}
	rg.refreshPrivate()
	anon := httptest.NewRequest(http.MethodGet, "/xfer/closed", nil)
	if rg.SyncReadGate(anon, "closed", "") {
		t.Error("mirroring is off, yet a stranger was allowed to clone")
	}
}