package oltp
import (
"path/filepath"
"testing"
"time"
)
// reopen closes and reopens the registry at the same path, so the
// migration list runs a second time against rows already in it. That
// is the only honest way to exercise a backfill: the fresh schema
// already has the columns, so the UPDATE has to be what fills them.
func reopen(t *testing.T, db *DB, path string) *DB {
t.Helper()
if err := db.Close(); err != nil {
t.Fatal(err)
}
next, err := Open(path)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { next.Close() })
return next
}
func TestOwnerAndNamespaceRoundTrip(t *testing.T) {
db := testDB(t)
if err := db.AddRepo("proj", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
owner, err := db.OwnerOf("proj")
if err != nil {
t.Fatal(err)
}
if owner != "" {
t.Fatalf("a fresh repository starts forge-held, got owner %q", owner)
}
if err := db.SetOwner("proj", "ada"); err != nil {
t.Fatal(err)
}
at := time.Date(2026, 9, 13, 1, 2, 3, 0, time.UTC)
if err := db.SetNamespace("proj", "hoppers", "grace", at); err != nil {
t.Fatal(err)
}
owner, err = db.OwnerOf("proj")
if err != nil {
t.Fatal(err)
}
ns, err := db.NamespaceOf("proj")
if err != nil {
t.Fatal(err)
}
// The deed and the appearance are separate facts: placing a
// repository under a team must not change who holds it.
if owner != "ada" || ns != "hoppers" {
t.Fatalf("owner/namespace = %q/%q, want ada/hoppers", owner, ns)
}
by, when, err := db.NamespacePlacement("proj")
if err != nil {
t.Fatal(err)
}
if by != "grace" || when != "2026-09-13T01:02:03Z" {
t.Fatalf("placement = %q at %q, want grace at 2026-09-13T01:02:03Z", by, when)
}
}
func TestOwnerOfUnknownRepoIsEmptyNotAnError(t *testing.T) {
db := testDB(t)
owner, err := db.OwnerOf("nope")
if err != nil || owner != "" {
t.Fatalf("OwnerOf(unknown) = %q, %v; want empty and no error", owner, err)
}
}
func TestBackfillAdoptsTheSoleAdmin(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := db.AddRepo("solo", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetCaps("ada", "solo", "a"); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
owner, err := db.OwnerOf("solo")
if err != nil {
t.Fatal(err)
}
if owner != "ada" {
t.Fatalf("owner = %q, want ada — one admin is an unambiguous owner", owner)
}
ns, err := db.NamespaceOf("solo")
if err != nil {
t.Fatal(err)
}
if ns != "ada" {
t.Fatalf("namespace = %q, want ada — with no team, a repository appears under its owner", ns)
}
}
// The case this rule exists for. On the production forge the agent
// account that imported every mirror holds `a` on all of them, and the
// human owner was granted `a` afterwards. Any rule that picks one of
// two admins — earliest, latest, alphabetical — can hand the deed to
// the wrong account silently. Declining is the only safe answer.
func TestBackfillDeclinesWhenTwoAccountsHoldAdmin(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := db.AddRepo("shared", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetCaps("importer", "shared", "a"); err != nil {
t.Fatal(err)
}
if err := db.SetCaps("ada", "shared", "a"); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
owner, err := db.OwnerOf("shared")
if err != nil {
t.Fatal(err)
}
if owner != "" {
t.Fatalf("owner = %q, want empty — two admins is ambiguous and must be left to a person", owner)
}
}
func TestBackfillIgnoresNonAdminGrants(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := db.AddRepo("readonly", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetCaps("lou", "readonly", "u3bBC78"); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
owner, err := db.OwnerOf("readonly")
if err != nil {
t.Fatal(err)
}
if owner != "" {
t.Fatalf("owner = %q, want empty — a reader is not an owner", owner)
}
}
func TestBackfillLeavesAnExistingOwnerAlone(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := db.AddRepo("held", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("held", "ada"); err != nil {
t.Fatal(err)
}
// A later admin grant to somebody else must not re-derive the deed.
if err := db.SetCaps("grace", "held", "a"); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
owner, err := db.OwnerOf("held")
if err != nil {
t.Fatal(err)
}
if owner != "ada" {
t.Fatalf("owner = %q, want ada — the backfill adopts only the unclaimed", owner)
}
}
func TestReposOwnedByIsOwnershipNotReach(t *testing.T) {
db := testDB(t)
for _, n := range []string{"one", "two", "other"} {
if err := db.AddRepo(n, t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
}
if err := db.SetOwner("one", "ada"); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("two", "ada"); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("other", "grace"); err != nil {
t.Fatal(err)
}
// Admin on somebody else's repository is reach, not ownership.
if err := db.SetCaps("ada", "other", "a"); err != nil {
t.Fatal(err)
}
got, err := db.ReposOwnedBy("ada")
if err != nil {
t.Fatal(err)
}
if len(got) != 2 || got[0] != "one" || got[1] != "two" {
t.Fatalf("ReposOwnedBy(ada) = %v, want [one two]", got)
}
n, err := db.OwnedRepoCount("ada")
if err != nil {
t.Fatal(err)
}
if n != 2 {
t.Fatalf("OwnedRepoCount(ada) = %d, want 2 — the quota counts what you own, not what you can reach", n)
}
}
// Load-bearing for the team union in EffectiveCaps. Team capabilities
// are resolved by matching team_member.team against the repository's
// namespace, so if a team could take a name an account already holds,
// a personal namespace would start matching a team's grants. The two
// name kinds share one pool and refuse each other in both directions;
// this asserts that rather than trusting it.
func TestTeamAndAccountNamesCannotCollide(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("ada", "correct horse battery staple", false); err != nil {
t.Fatal(err)
}
if err := db.AddTeam("hoppers", "Hoppers", "", "ada"); err != nil {
t.Fatal(err)
}
if err := db.AddTeam("ada", "Not Ada", "", "ada"); err == nil {
t.Fatal("a team took a name an account already holds; a personal namespace would then match team grants")
}
if err := db.AddAccount("hoppers", "correct horse battery staple", false); err == nil {
t.Fatal("an account took a name a team already holds; the same collision, from the other side")
}
}
// A tracking store is something the forge serves, not something a
// person is hosting here, and its sole admin is usually the operator
// account that ran the import. Adopting that account would hand it the
// deed to code it merely copied.
func TestBackfillDoesNotAdoptAMirror(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := db.AddRepo("upstream", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetMirror("upstream", "https://example.invalid/r/upstream", time.Hour); err != nil {
t.Fatal(err)
}
if err := db.SetCaps("importer", "upstream", "a"); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
owner, err := db.OwnerOf("upstream")
if err != nil {
t.Fatal(err)
}
if owner != "" {
t.Fatalf("owner = %q, want empty — a mirror stays forge-held", owner)
}
}
// The migration that released wrongly-adopted mirrors has been removed,
// and this is the interaction that made keeping it unsafe.
//
// It identified rows to release by namespace_by being empty. That now
// also means "the segment fell back to its owner", which is what
// leaving a team produces -- so a claimed mirror that had been in a
// team would have been quietly un-owned by a migration on the next
// restart, and nobody would have connected the two.
func TestLeavingATeamDoesNotUnownAMirror(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := db.AddAccount("ada", "correct horse battery staple", false); err != nil {
t.Fatal(err)
}
if err := db.AddTeam("hoppers", "Hoppers", "", "ada"); err != nil {
t.Fatal(err)
}
if err := db.AddRepo("tracked", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetMirror("tracked", "https://example.invalid/r/tracked", time.Hour); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("tracked", "ada"); err != nil {
t.Fatal(err)
}
if err := db.SetTeamRepo("hoppers", "tracked", "ada"); err != nil {
t.Fatal(err)
}
if err := db.RemoveTeamRepo("tracked"); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
owner, err := db.OwnerOf("tracked")
if err != nil {
t.Fatal(err)
}
if owner != "ada" {
t.Fatalf("owner = %q, want ada -- a restart must not take a claimed mirror away", owner)
}
}
// team_repo decides which team holds a repository; repo.namespace is
// where it appears. They were written in different places, so a
// repository could sit in a team while its path still named the
// previous holder. The two now move together.
func TestNamespaceFollowsTeamPlacement(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("ada", "correct horse battery staple", false); err != nil {
t.Fatal(err)
}
if err := db.AddTeam("hoppers", "Hoppers", "", "ada"); err != nil {
t.Fatal(err)
}
if err := db.AddRepo("proj", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("proj", "ada"); err != nil {
t.Fatal(err)
}
if err := db.SetNamespace("proj", "ada", "ada", time.Now()); err != nil {
t.Fatal(err)
}
if err := db.SetTeamRepo("hoppers", "proj", "ada"); err != nil {
t.Fatal(err)
}
ns, err := db.NamespaceOf("proj")
if err != nil {
t.Fatal(err)
}
if ns != "hoppers" {
t.Fatalf("namespace = %q, want hoppers — a repository in a team appears under the team", ns)
}
// The deed never moves. That is what lets a team be dissolved
// without putting anybody's code at risk.
owner, err := db.OwnerOf("proj")
if err != nil {
t.Fatal(err)
}
if owner != "ada" {
t.Fatalf("owner = %q, want ada — placing a repository in a team must not move its deed", owner)
}
by, _, err := db.NamespacePlacement("proj")
if err != nil {
t.Fatal(err)
}
if by != "ada" {
t.Fatalf("placement recorded %q, want ada", by)
}
if err := db.RemoveTeamRepo("proj"); err != nil {
t.Fatal(err)
}
ns, err = db.NamespaceOf("proj")
if err != nil {
t.Fatal(err)
}
if ns != "ada" {
t.Fatalf("namespace = %q, want ada — out of the team, it appears under its owner again", ns)
}
}
// A forge-held repository has no owner to fall back to, so leaving a
// team leaves it with no namespace rather than stranding the team's
// name on a repository the team no longer holds.
func TestLeavingATeamWithNoOwnerLeavesNoNamespace(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("ada", "correct horse battery staple", false); err != nil {
t.Fatal(err)
}
if err := db.AddTeam("hoppers", "Hoppers", "", "ada"); err != nil {
t.Fatal(err)
}
if err := db.AddRepo("served", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetTeamRepo("hoppers", "served", "ada"); err != nil {
t.Fatal(err)
}
if err := db.RemoveTeamRepo("served"); err != nil {
t.Fatal(err)
}
ns, err := db.NamespaceOf("served")
if err != nil {
t.Fatal(err)
}
if ns != "" {
t.Fatalf("namespace = %q, want empty — nobody holds this store", ns)
}
}
// A path segment that disagrees with the team holding the repository is
// the path gone stale, never the other way round, so a reopen re-seats
// it. This is the state rows were left in before SetTeamRepo wrote both.
func TestReopenReseatsAStalePathSegment(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := db.AddAccount("ada", "correct horse battery staple", false); err != nil {
t.Fatal(err)
}
if err := db.AddTeam("hoppers", "Hoppers", "", "ada"); err != nil {
t.Fatal(err)
}
if err := db.AddRepo("proj", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("proj", "ada"); err != nil {
t.Fatal(err)
}
if err := db.SetTeamRepo("hoppers", "proj", "ada"); err != nil {
t.Fatal(err)
}
// Force the stale shape the old code produced: team_repo says the
// team, the path still names the previous holder.
if err := db.SetNamespace("proj", "ada", "ada", time.Now()); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
ns, err := db.NamespaceOf("proj")
if err != nil {
t.Fatal(err)
}
if ns != "hoppers" {
t.Fatalf("namespace = %q, want hoppers — the team table is the authority", ns)
}
}
// Dissolving a team must not strand its repositories' paths. The team
// held the path, never the deed, which is the whole reason dissolution
// is safe -- and it is only true if the path actually falls back.
func TestDissolvingATeamReturnsItsRepositoriesToTheirOwners(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("ada", "correct horse battery staple", false); err != nil {
t.Fatal(err)
}
if err := db.AddTeam("hoppers", "Hoppers", "", "ada"); err != nil {
t.Fatal(err)
}
for _, n := range []string{"one", "two"} {
if err := db.AddRepo(n, t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetOwner(n, "ada"); err != nil {
t.Fatal(err)
}
if err := db.SetTeamRepo("hoppers", n, "ada"); err != nil {
t.Fatal(err)
}
}
if err := db.DeleteTeam("hoppers"); err != nil {
t.Fatal(err)
}
for _, n := range []string{"one", "two"} {
ns, err := db.NamespaceOf(n)
if err != nil {
t.Fatal(err)
}
if ns != "ada" {
t.Fatalf("%s namespace = %q, want ada — a dissolved team must not keep naming the path", n, ns)
}
owner, err := db.OwnerOf(n)
if err != nil {
t.Fatal(err)
}
if owner != "ada" {
t.Fatalf("%s owner = %q, want ada — dissolving a team must not touch the deed", n, owner)
}
by, _, err := db.NamespacePlacement(n)
if err != nil {
t.Fatal(err)
}
if by != "" {
t.Fatalf("%s placement = %q, want empty — nobody put it where it now is", n, by)
}
}
}
// A forge carrying the damage a dissolve used to leave repairs itself
// on the next start. The re-seat above cannot see these rows: they are
// no longer in any team, which is exactly why they were unrecoverable.
func TestReopenRepairsASegmentNamingADeadTeam(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := db.AddAccount("ada", "correct horse battery staple", false); err != nil {
t.Fatal(err)
}
if err := db.AddRepo("proj", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("proj", "ada"); err != nil {
t.Fatal(err)
}
// The exact shape the old dissolve left: a segment naming a team
// that does not exist, and no team_repo row to find it by.
if err := db.SetNamespace("proj", "dissolved-team", "ada", time.Now()); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
ns, err := db.NamespaceOf("proj")
if err != nil {
t.Fatal(err)
}
if ns != "ada" {
t.Fatalf("namespace = %q, want ada", ns)
}
}
// The repair must never touch a repository sitting in its OWNER's
// namespace, which is the ordinary personal case.
//
// This once asserted that any live account name was left alone, on the
// reasoning that a segment naming a live account could not be a dead
// team. That was the too-narrow rule rather than the invariant, and it
// is what let a handed-over repository keep the previous owner's
// handle: also a live account, and also wrong.
func TestRepairLeavesTheOwnersOwnNamespaceAlone(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
for _, who := range []string{"ada", "grace"} {
if err := db.AddAccount(who, "correct horse battery staple", false); err != nil {
t.Fatal(err)
}
}
if err := db.AddRepo("proj", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("proj", "ada"); err != nil {
t.Fatal(err)
}
// In its owner's own namespace, which is where a repository with no
// team belongs.
if err := db.SetNamespace("proj", "ada", "ada", time.Now()); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
ns, err := db.NamespaceOf("proj")
if err != nil {
t.Fatal(err)
}
if ns != "ada" {
t.Fatalf("namespace = %q, want ada — a repository in its owner's namespace is where it belongs", ns)
}
}
// The case that actually occurred, and that the first version of the
// repair missed: a repository changed hands and kept the PREVIOUS
// owner's handle. That is a live account, so a repair looking for a
// segment naming no live team and no live account left it alone.
//
// The rule is the invariant instead: a namespace is the team holding
// the repository, or the owner's handle, and nothing else.
func TestReopenRepairsANamespaceNamingSomebodyElse(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
for _, who := range []string{"keeper", "heir"} {
if err := db.AddAccount(who, "correct horse battery staple", false); err != nil {
t.Fatal(err)
}
}
if err := db.AddRepo("handed", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("handed", "heir"); err != nil {
t.Fatal(err)
}
if err := db.SetNamespace("handed", "keeper", "keeper", time.Now()); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
ns, err := db.NamespaceOf("handed")
if err != nil {
t.Fatal(err)
}
if ns != "heir" {
t.Fatalf("namespace = %q, want heir — a repository appears under the person who holds it", ns)
}
}
// A repository genuinely under a team keeps the team's name, which is
// the one case where the namespace is not the owner.
func TestRepairLeavesATeamsNamespaceAlone(t *testing.T) {
path := filepath.Join(t.TempDir(), "oltp.db")
db, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := db.AddAccount("ada", "correct horse battery staple", false); err != nil {
t.Fatal(err)
}
if err := db.AddTeam("hoppers", "Hoppers", "", "ada"); err != nil {
t.Fatal(err)
}
if err := db.AddRepo("shared", t.TempDir(), "", Public); err != nil {
t.Fatal(err)
}
if err := db.SetOwner("shared", "ada"); err != nil {
t.Fatal(err)
}
if err := db.SetTeamRepo("hoppers", "shared", "ada"); err != nil {
t.Fatal(err)
}
db = reopen(t, db, path)
ns, err := db.NamespaceOf("shared")
if err != nil {
t.Fatal(err)
}
if ns != "hoppers" {
t.Fatalf("namespace = %q, want hoppers", ns)
}
}