Monorepo for Tangled
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957package spindle
import ( "context" "database/sql" "fmt" "io" "log/slog" "path/filepath" "testing" "time"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/rbac" "tangled.org/core/spindle/db" "tangled.org/core/spindle/secrets")
func seedTapDB(t *testing.T, path string) { t.Helper() tdb, err := sql.Open("sqlite3", path) if err != nil { t.Fatalf("open tap db: %v", err) } defer tdb.Close() if _, err := tdb.Exec(` create table repos ( did text primary key, state text not null default 'pending', status text not null default 'active', handle text default '', rev text default '', prev_data text default '', error_msg text default '', retry_count integer not null default 0, retry_after integer not null default 0 ); create table repo_records ( did text not null, collection text not null, rkey text not null, cid text not null, primary key (did, collection, rkey) ); `); err != nil { t.Fatalf("create tap tables: %v", err) }}
func tapRepoState(t *testing.T, path, did string) string { t.Helper() tdb, err := sql.Open("sqlite3", path) if err != nil { t.Fatalf("open tap db: %v", err) } defer tdb.Close() var state string if err := tdb.QueryRow(`select state from repos where did = ?`, did).Scan(&state); err != nil { t.Fatalf("query state for %s: %v", did, err) } return state}
func tapRecordCount(t *testing.T, path string) int { t.Helper() tdb, err := sql.Open("sqlite3", path) if err != nil { t.Fatalf("open tap db: %v", err) } defer tdb.Close() var n int if err := tdb.QueryRow(`select count(*) from repo_records`).Scan(&n); err != nil { t.Fatalf("count repo_records: %v", err) } return n}
func newTestSpindleDB(t *testing.T) (*db.DB, *rbac.Enforcer) { t.Helper() p := filepath.Join(t.TempDir(), "spindle.db") d, err := db.Make(context.Background(), p) if err != nil { t.Fatalf("db.Make: %v", err) } t.Cleanup(func() { d.Close() }) e, err := rbac.NewEnforcer(p) if err != nil { t.Fatalf("rbac.NewEnforcer: %v", err) } e.E.EnableAutoSave(true) return d, e}
func newTestVault(t *testing.T) *secrets.SqliteManager { t.Helper() vault, err := secrets.NewSQLiteManager(filepath.Join(t.TempDir(), "vault.db")) if err != nil { t.Fatalf("vault.New: %v", err) } return vault}
func mustAddSecret(t *testing.T, vault secrets.Manager, repo, key, value string, createdAt time.Time, by string) { t.Helper() err := vault.AddSecret(context.Background(), secrets.UnlockedSecret{ Repo: secrets.RepoIdentifier(repo), Key: key, Value: value, CreatedAt: createdAt, CreatedBy: syntax.DID(by), }) if err != nil { t.Fatalf("AddSecret(%s/%s): %v", repo, key, err) }}
func mustAddCollab(t *testing.T, d *db.DB, owner, rkey, subject, repoDid string) { t.Helper() if err := d.AddRepoCollaborator(db.RepoCollaborator{ OwnerDid: syntax.DID(owner), Rkey: syntax.RecordKey(rkey), Subject: syntax.DID(subject), RepoDid: syntax.DID(repoDid), }); err != nil { t.Fatalf("AddRepoCollaborator(%s): %v", rkey, err) }}
func TestMigrateLegacyRepoSecrets_NameCandidate(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t) vault := newTestVault(t)
owner := syntax.DID("did:plc:akshay") repoDid := syntax.DID("did:plc:boltless") displayName := "myrepo" rkey := syntax.RecordKey("3kspindlerkey00a")
created := time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC) oldNameKey := owner.String() + "/" + displayName
mustAddSecret(t, vault, oldNameKey, "API_KEY", "alpha", created, owner.String()) mustAddSecret(t, vault, oldNameKey, "DB_PASSWORD", "bravo", created.Add(1*time.Hour), owner.String())
migrateLegacyRepoSecrets(ctx, d, vault, logger, owner, displayName, rkey, repoDid)
copied, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(repoDid)) if err != nil { t.Fatalf("GetSecretsUnlocked(new): %v", err) } if len(copied) != 2 { t.Fatalf("expected 2 secrets under repo_did key, got %d", len(copied)) }
want := map[string]struct { value string createdAt time.Time }{ "API_KEY": {"alpha", created}, "DB_PASSWORD": {"bravo", created.Add(1 * time.Hour)}, } for _, s := range copied { w, ok := want[s.Key] if !ok { t.Errorf("unexpected key %q under %s", s.Key, repoDid) continue } if s.Value != w.value { t.Errorf("%s: value got %q, want %q", s.Key, s.Value, w.value) } if !s.CreatedAt.Equal(w.createdAt) { t.Errorf("%s: CreatedAt got %s, want %s", s.Key, s.CreatedAt, w.createdAt) } if string(s.Repo) != repoDid.String() { t.Errorf("%s: Repo got %s, want %s", s.Key, s.Repo, repoDid) } }
orig, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(oldNameKey)) if err != nil { t.Fatalf("GetSecretsUnlocked(old): %v", err) } if len(orig) != 2 { t.Errorf("expected old-key secrets preserved, got %d", len(orig)) }
migrateLegacyRepoSecrets(ctx, d, vault, logger, owner, displayName, rkey, repoDid) again, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(repoDid)) if err != nil { t.Fatalf("GetSecretsUnlocked(new) after re-run: %v", err) } if len(again) != 2 { t.Errorf("re-run should not duplicate or drop secrets, got %d", len(again)) }
var marked int if err := d.QueryRow( `select count(*) from migrations where name = ?`, "legacy-secret-copy:"+repoDid.String()+":"+rkey.String(), ).Scan(&marked); err != nil { t.Fatalf("query migrations: %v", err) } if marked != 1 { t.Errorf("expected per-repo flag recorded exactly once, got %d", marked) }}
func TestMigrateLegacyRepoSecrets_RkeyCandidate(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t) vault := newTestVault(t)
owner := syntax.DID("did:plc:akshay") repoDid := syntax.DID("did:plc:boltless") displayName := "myrepo" rkey := syntax.RecordKey("3kspindlerkey00a")
created := time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC) oldRkeyKey := owner.String() + "/" + rkey.String()
mustAddSecret(t, vault, oldRkeyKey, "API_KEY", "alpha", created, owner.String()) mustAddSecret(t, vault, oldRkeyKey, "DB_PASSWORD", "bravo", created, owner.String())
migrateLegacyRepoSecrets(ctx, d, vault, logger, owner, displayName, rkey, repoDid)
got, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(repoDid)) if err != nil { t.Fatalf("GetSecretsUnlocked: %v", err) } if len(got) != 2 { t.Fatalf("expected 2 secrets copied via rkey candidate, got %d", len(got)) }}
func TestMigrateLegacyRepoSecrets_BothCandidates(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t) vault := newTestVault(t)
owner := syntax.DID("did:plc:akshay") repoDid := syntax.DID("did:plc:boltless") displayName := "myrepo" rkey := syntax.RecordKey("3kspindlerkey00a")
created := time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC) oldNameKey := owner.String() + "/" + displayName oldRkeyKey := owner.String() + "/" + rkey.String()
mustAddSecret(t, vault, oldNameKey, "FROM_NAME", "n", created, owner.String()) mustAddSecret(t, vault, oldRkeyKey, "FROM_RKEY", "r", created, owner.String())
migrateLegacyRepoSecrets(ctx, d, vault, logger, owner, displayName, rkey, repoDid)
got, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(repoDid)) if err != nil { t.Fatalf("GetSecretsUnlocked: %v", err) } if len(got) != 2 { t.Fatalf("expected 2 secrets merged from both candidates, got %d", len(got)) } seen := map[string]string{} for _, s := range got { seen[s.Key] = s.Value } if seen["FROM_NAME"] != "n" { t.Errorf("FROM_NAME missing or wrong value: %q", seen["FROM_NAME"]) } if seen["FROM_RKEY"] != "r" { t.Errorf("FROM_RKEY missing or wrong value: %q", seen["FROM_RKEY"]) }}
func TestMigrateLegacyRepoSecrets_PreExistingTakesPriority(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t) vault := newTestVault(t)
owner := syntax.DID("did:plc:akshay") repoDid := syntax.DID("did:plc:boltless") displayName := "myrepo" rkey := syntax.RecordKey("3kspindlerkey00a")
created := time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC) oldKey := owner.String() + "/" + displayName
mustAddSecret(t, vault, oldKey, "API_KEY", "alpha", created, owner.String()) mustAddSecret(t, vault, oldKey, "DB_PASSWORD", "bravo", created, owner.String()) mustAddSecret(t, vault, repoDid.String(), "API_KEY", "pre-existing", created.Add(-24*time.Hour), owner.String())
migrateLegacyRepoSecrets(ctx, d, vault, logger, owner, displayName, rkey, repoDid)
got, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(repoDid)) if err != nil { t.Fatalf("GetSecretsUnlocked: %v", err) } if len(got) != 2 { t.Fatalf("expected 2 secrets under repo_did key, got %d", len(got)) } for _, s := range got { if s.Key == "API_KEY" && s.Value != "pre-existing" { t.Errorf("API_KEY should preserve pre-existing value, got %q", s.Value) } if s.Key == "DB_PASSWORD" && s.Value != "bravo" { t.Errorf("DB_PASSWORD should be copied, got %q", s.Value) } }}
func TestMigrateLegacyRepoSecrets_EmptyName(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t) vault := newTestVault(t)
owner := syntax.DID("did:plc:akshay") repoDid := syntax.DID("did:plc:boltless") rkey := syntax.RecordKey("3kspindlerkey00a")
created := time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC) oldRkeyKey := owner.String() + "/" + rkey.String() mustAddSecret(t, vault, oldRkeyKey, "API_KEY", "alpha", created, owner.String())
migrateLegacyRepoSecrets(ctx, d, vault, logger, owner, "", rkey, repoDid)
got, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(repoDid)) if err != nil { t.Fatalf("GetSecretsUnlocked: %v", err) } if len(got) != 1 { t.Errorf("expected 1 secret via rkey candidate when name empty, got %d", len(got)) }}
func TestMigrateLegacyRepoSecrets_BothEmpty(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t) vault := newTestVault(t)
owner := syntax.DID("did:plc:akshay") repoDid := syntax.DID("did:plc:boltless")
migrateLegacyRepoSecrets(ctx, d, vault, logger, owner, "", "", repoDid)
got, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(repoDid)) if err != nil { t.Fatalf("GetSecretsUnlocked: %v", err) } if len(got) != 0 { t.Errorf("expected no work when both name and rkey empty, got %d secrets", len(got)) }
var marked int if err := d.QueryRow( `select count(*) from migrations where name like ?`, "legacy-secret-copy:"+repoDid.String()+":%", ).Scan(&marked); err != nil { t.Fatalf("query migrations: %v", err) } if marked != 0 { t.Errorf("empty inputs should not record flag, got %d", marked) }}
func TestMigrateLegacyRepoCasbin_NameCandidate(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, e := newTestSpindleDB(t)
if err := e.AddSpindle(rbacDomain); err != nil { t.Fatalf("AddSpindle: %v", err) }
owner := "did:plc:akshay" repoDid := "did:plc:boltless" displayName := "myrepo" rkey := "3kspindlerkey00a" collab := "did:plc:limpet" oldNameKey := owner + "/" + displayName oldRkeyKey := owner + "/" + rkey
mustAddCollab(t, d, owner, "3kcollabrkey0001", collab, repoDid)
if err := e.AddRepo(owner, rbacDomain, oldNameKey); err != nil { t.Fatalf("seed AddRepo at Name key: %v", err) } if err := e.AddCollaborator(collab, rbacDomain, oldNameKey); err != nil { t.Fatalf("seed AddCollaborator at Name key: %v", err) }
migrateLegacyRepoCasbin(ctx, d, e, logger, syntax.DID(owner), displayName, syntax.RecordKey(rkey), syntax.DID(repoDid))
if got, err := e.IsSettingsAllowed(owner, rbacDomain, repoDid); err != nil || !got { t.Errorf("owner should have settings at new repoDid key, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(collab, rbacDomain, repoDid); err != nil || !got { t.Errorf("collab should have settings at new repoDid key, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(owner, rbacDomain, oldNameKey); err != nil || got { t.Errorf("owner Name-keyed policy should be removed, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(collab, rbacDomain, oldNameKey); err != nil || got { t.Errorf("collab Name-keyed policy should be removed, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(owner, rbacDomain, oldRkeyKey); err != nil || got { t.Errorf("owner rkey-keyed policy should be absent (never added), allowed=%v err=%v", got, err) }
migrateLegacyRepoCasbin(ctx, d, e, logger, syntax.DID(owner), displayName, syntax.RecordKey(rkey), syntax.DID(repoDid))
if got, err := e.IsSettingsAllowed(collab, rbacDomain, repoDid); err != nil || !got { t.Errorf("collab settings still expected after idempotent re-run, allowed=%v err=%v", got, err) }
var marked int if err := d.QueryRow( `select count(*) from migrations where name = ?`, "legacy-casbin-rekey:"+repoDid+":"+rkey, ).Scan(&marked); err != nil { t.Fatalf("query migrations: %v", err) } if marked != 1 { t.Errorf("expected per-repo flag recorded exactly once, got %d", marked) }}
func TestMigrateLegacyRepoCasbin_RkeyCandidate(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, e := newTestSpindleDB(t)
if err := e.AddSpindle(rbacDomain); err != nil { t.Fatalf("AddSpindle: %v", err) }
owner := "did:plc:akshay" repoDid := "did:plc:boltless" displayName := "myrepo" rkey := "3kspindlerkey00a" collab := "did:plc:limpet" oldRkeyKey := owner + "/" + rkey
mustAddCollab(t, d, owner, "3kcollabrkey0001", collab, repoDid)
if err := e.AddRepo(owner, rbacDomain, oldRkeyKey); err != nil { t.Fatalf("seed AddRepo at rkey: %v", err) } if err := e.AddCollaborator(collab, rbacDomain, oldRkeyKey); err != nil { t.Fatalf("seed AddCollaborator at rkey: %v", err) }
migrateLegacyRepoCasbin(ctx, d, e, logger, syntax.DID(owner), displayName, syntax.RecordKey(rkey), syntax.DID(repoDid))
if got, err := e.IsSettingsAllowed(owner, rbacDomain, repoDid); err != nil || !got { t.Errorf("owner should have settings at new repoDid key, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(collab, rbacDomain, repoDid); err != nil || !got { t.Errorf("collab should have settings at new repoDid key, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(owner, rbacDomain, oldRkeyKey); err != nil || got { t.Errorf("owner rkey-keyed policy should be removed, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(collab, rbacDomain, oldRkeyKey); err != nil || got { t.Errorf("collab rkey-keyed policy should be removed, allowed=%v err=%v", got, err) }}
func TestMigrateLegacyRepoCasbin_BothCandidates(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, e := newTestSpindleDB(t)
if err := e.AddSpindle(rbacDomain); err != nil { t.Fatalf("AddSpindle: %v", err) }
owner := "did:plc:akshay" repoDid := "did:plc:boltless" displayName := "myrepo" rkey := "3kspindlerkey00a" collab := "did:plc:limpet" oldNameKey := owner + "/" + displayName oldRkeyKey := owner + "/" + rkey
mustAddCollab(t, d, owner, "3kcollabrkey0001", collab, repoDid)
if err := e.AddRepo(owner, rbacDomain, oldNameKey); err != nil { t.Fatalf("seed AddRepo at Name key: %v", err) } if err := e.AddRepo(owner, rbacDomain, oldRkeyKey); err != nil { t.Fatalf("seed AddRepo at rkey: %v", err) } if err := e.AddCollaborator(collab, rbacDomain, oldNameKey); err != nil { t.Fatalf("seed AddCollaborator at Name key: %v", err) } if err := e.AddCollaborator(collab, rbacDomain, oldRkeyKey); err != nil { t.Fatalf("seed AddCollaborator at rkey: %v", err) }
migrateLegacyRepoCasbin(ctx, d, e, logger, syntax.DID(owner), displayName, syntax.RecordKey(rkey), syntax.DID(repoDid))
if got, err := e.IsSettingsAllowed(owner, rbacDomain, oldNameKey); err != nil || got { t.Errorf("owner Name-keyed policy should be removed, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(owner, rbacDomain, oldRkeyKey); err != nil || got { t.Errorf("owner rkey-keyed policy should be removed, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(collab, rbacDomain, oldNameKey); err != nil || got { t.Errorf("collab Name-keyed policy should be removed, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(collab, rbacDomain, oldRkeyKey); err != nil || got { t.Errorf("collab rkey-keyed policy should be removed, allowed=%v err=%v", got, err) }}
func TestMigrateLegacyRepoCasbin_BothEmpty(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, e := newTestSpindleDB(t)
if err := e.AddSpindle(rbacDomain); err != nil { t.Fatalf("AddSpindle: %v", err) }
owner := syntax.DID("did:plc:akshay") repoDid := syntax.DID("did:plc:boltless")
migrateLegacyRepoCasbin(ctx, d, e, logger, owner, "", "", repoDid)
var marked int if err := d.QueryRow( `select count(*) from migrations where name like ?`, "legacy-casbin-rekey:"+repoDid.String()+":%", ).Scan(&marked); err != nil { t.Fatalf("query migrations: %v", err) } if marked != 0 { t.Errorf("empty inputs should not record flag, got %d", marked) }}
func TestNudgeTapForResync(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t)
tapPath := filepath.Join(t.TempDir(), "tap.db") seedTapDB(t, tapPath)
tdb, err := sql.Open("sqlite3", tapPath) if err != nil { t.Fatalf("open tap db: %v", err) } if _, err := tdb.Exec(`insert into repos (did, state) values ('did:plc:akshay', 'active'), ('did:plc:boltless', 'error'), ('did:plc:limpet', 'pending') `); err != nil { t.Fatalf("seed repos: %v", err) } if _, err := tdb.Exec(`insert into repo_records (did, collection, rkey, cid) values ('did:plc:akshay', 'sh.tangled.repo', '3kspindlerkey00a', 'bafyone'), ('did:plc:boltless', 'sh.tangled.repo', '3kspindlerkey00b', 'bafytwo') `); err != nil { t.Fatalf("seed records: %v", err) } tdb.Close()
if err := nudgeTapForResync(ctx, d, tapPath, logger); err != nil { t.Fatalf("nudgeTapForResync: %v", err) }
if got := tapRecordCount(t, tapPath); got != 0 { t.Errorf("expected repo_records cleared, got %d", got) } if got := tapRepoState(t, tapPath, "did:plc:akshay"); got != "desynchronized" { t.Errorf("active should flip to desynchronized, got %s", got) } if got := tapRepoState(t, tapPath, "did:plc:boltless"); got != "desynchronized" { t.Errorf("error should flip to desynchronized, got %s", got) } if got := tapRepoState(t, tapPath, "did:plc:limpet"); got != "pending" { t.Errorf("pending should not be touched, got %s", got) }
tdb2, err := sql.Open("sqlite3", tapPath) if err != nil { t.Fatalf("reopen tap db: %v", err) } if _, err := tdb2.Exec(`update repos set state = 'active' where did = 'did:plc:akshay'`); err != nil { t.Fatalf("reseed: %v", err) } tdb2.Close()
if err := nudgeTapForResync(ctx, d, tapPath, logger); err != nil { t.Fatalf("nudgeTapForResync second run: %v", err) } if got := tapRepoState(t, tapPath, "did:plc:akshay"); got != "active" { t.Errorf("idempotent re-run should not touch state, got %s", got) }
var marked int if err := d.QueryRow( `select count(*) from migrations where name = ?`, "force-tap-repo-resync-v1", ).Scan(&marked); err != nil { t.Fatalf("query migrations: %v", err) } if marked != 1 { t.Errorf("expected flag recorded exactly once, got %d", marked) }}
func TestNudgeTapForResync_MissingDB(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t)
missing := filepath.Join(t.TempDir(), "absent.db")
if err := nudgeTapForResync(ctx, d, missing, logger); err != nil { t.Fatalf("missing tap db should succeed: %v", err) }
var marked int if err := d.QueryRow( `select count(*) from migrations where name = ?`, "force-tap-repo-resync-v1", ).Scan(&marked); err != nil { t.Fatalf("query migrations: %v", err) } if marked != 1 { t.Errorf("expected flag recorded even when tap db absent, got %d", marked) }}
func TestNudgeTapForResync_EmptyPath(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t)
if err := nudgeTapForResync(ctx, d, "", logger); err == nil { t.Errorf("expected error for empty tap db path") }
var marked int if err := d.QueryRow( `select count(*) from migrations where name = ?`, "force-tap-repo-resync-v1", ).Scan(&marked); err != nil { t.Fatalf("query migrations: %v", err) } if marked != 0 { t.Errorf("empty path should not mark flag, got %d", marked) }}
func TestRunStartupMigrations_NonEmbedSkipsTapNudge(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t)
if err := runStartupMigrations(ctx, d, false, "", logger); err != nil { t.Fatalf("non-embed should not error on empty path: %v", err) }
var marked int if err := d.QueryRow( `select count(*) from migrations where name = ?`, "force-tap-repo-resync-v1", ).Scan(&marked); err != nil { t.Fatalf("query migrations: %v", err) } if marked != 0 { t.Errorf("non-embed mode should skip tap nudge flag, got %d", marked) }}
func TestCleanupOrphanRepos_DeletesWhenSiblingExists(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t)
owner := "did:plc:akshay" if _, err := d.Exec(`insert into repos (knot, owner, rkey, repo_did, created_at) values ('k', ?, 'legacy_name', null, null), ('k', ?, '3kspindlerkey00a', 'did:plc:boltless', '2024-01-01T00:00:00Z')`, owner, owner); err != nil { t.Fatalf("seed: %v", err) }
if err := cleanupOrphanRepos(ctx, d, logger); err != nil { t.Fatalf("cleanupOrphanRepos: %v", err) }
var nullCount int if err := d.QueryRow(`select count(*) from repos where repo_did is null`).Scan(&nullCount); err != nil { t.Fatalf("null count: %v", err) } if nullCount != 0 { t.Errorf("orphan should be deleted when sibling exists, got %d remaining", nullCount) }
var sibCount int if err := d.QueryRow(`select count(*) from repos where repo_did is not null`).Scan(&sibCount); err != nil { t.Fatalf("sibling count: %v", err) } if sibCount != 1 { t.Errorf("sibling row should be preserved, got %d", sibCount) }}
func TestCleanupOrphanRepos_KeepsWhenAlone(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t)
owner := "did:plc:akshay" if _, err := d.Exec(`insert into repos (knot, owner, rkey, repo_did, created_at) values ('k', ?, 'legacy_name', null, null)`, owner); err != nil { t.Fatalf("seed: %v", err) }
if err := cleanupOrphanRepos(ctx, d, logger); err != nil { t.Fatalf("cleanupOrphanRepos: %v", err) }
var remaining int if err := d.QueryRow(`select count(*) from repos where owner = ?`, owner).Scan(&remaining); err != nil { t.Fatalf("count: %v", err) } if remaining != 1 { t.Errorf("orphan with no sibling should be kept (preserves owner registration), got %d", remaining) }}
func TestCleanupOrphanRepos_PerOwnerScope(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t)
ownerA := "did:plc:akshay" ownerB := "did:plc:limpet" if _, err := d.Exec(`insert into repos (knot, owner, rkey, repo_did, created_at) values ('k', ?, 'legacy_a', null, null), ('k', ?, '3krealkey', 'did:plc:boltless', '2024-01-01T00:00:00Z'), ('k', ?, 'legacy_b', null, null)`, ownerA, ownerA, ownerB); err != nil { t.Fatalf("seed: %v", err) }
if err := cleanupOrphanRepos(ctx, d, logger); err != nil { t.Fatalf("cleanupOrphanRepos: %v", err) }
var ownerARows, ownerBRows int if err := d.QueryRow(`select count(*) from repos where owner = ?`, ownerA).Scan(&ownerARows); err != nil { t.Fatalf("count A: %v", err) } if ownerARows != 1 { t.Errorf("ownerA: orphan should be deleted (sibling exists), expected 1 row, got %d", ownerARows) } if err := d.QueryRow(`select count(*) from repos where owner = ?`, ownerB).Scan(&ownerBRows); err != nil { t.Fatalf("count B: %v", err) } if ownerBRows != 1 { t.Errorf("ownerB: orphan should be kept (no sibling), expected 1 row, got %d", ownerBRows) }}
func TestCleanupOrphanRepos_EmptyStringRepoDid(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, _ := newTestSpindleDB(t)
owner := "did:plc:akshay" if _, err := d.Exec(`insert into repos (knot, owner, rkey, repo_did, created_at) values ('k', ?, 'legacy_empty', '', null), ('k', ?, '3krealkey', 'did:plc:boltless', '2024-01-01T00:00:00Z')`, owner, owner); err != nil { t.Fatalf("seed: %v", err) }
if err := cleanupOrphanRepos(ctx, d, logger); err != nil { t.Fatalf("cleanupOrphanRepos: %v", err) }
var emptyCount int if err := d.QueryRow(`select count(*) from repos where coalesce(repo_did, '') = ''`).Scan(&emptyCount); err != nil { t.Fatalf("empty count: %v", err) } if emptyCount != 0 { t.Errorf("empty-string repo_did orphan should be deleted when sibling exists, got %d remaining", emptyCount) }}
func TestMigrateLegacyRepoCasbin_MultipleCollabsAllRekeyed(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, e := newTestSpindleDB(t)
if err := e.AddSpindle(rbacDomain); err != nil { t.Fatalf("AddSpindle: %v", err) }
owner := "did:plc:akshay" repoDid := "did:plc:boltless" displayName := "myrepo" rkey := "3kspindlerkey00a" oldNameKey := owner + "/" + displayName collabs := []string{"did:plc:limpet", "did:plc:nautilus", "did:plc:whelk", "did:plc:cuttle"}
var addCollabRows func(rest []string, idx int) addCollabRows = func(rest []string, idx int) { if len(rest) == 0 { return } mustAddCollab(t, d, owner, fmt.Sprintf("3kcollabrkey%04d", idx), rest[0], repoDid) addCollabRows(rest[1:], idx+1) } addCollabRows(collabs, 0)
if err := e.AddRepo(owner, rbacDomain, oldNameKey); err != nil { t.Fatalf("seed owner: %v", err) } var seedAll func(rest []string) error seedAll = func(rest []string) error { if len(rest) == 0 { return nil } if err := e.AddCollaborator(rest[0], rbacDomain, oldNameKey); err != nil { return err } return seedAll(rest[1:]) } if err := seedAll(collabs); err != nil { t.Fatalf("seed collab policies: %v", err) }
migrateLegacyRepoCasbin(ctx, d, e, logger, syntax.DID(owner), displayName, syntax.RecordKey(rkey), syntax.DID(repoDid))
var assertEach func(rest []string) assertEach = func(rest []string) { if len(rest) == 0 { return } c := rest[0] if got, err := e.IsSettingsAllowed(c, rbacDomain, repoDid); err != nil || !got { t.Errorf("collab %s should have settings at repoDid, allowed=%v err=%v", c, got, err) } if got, err := e.IsPushAllowed(c, rbacDomain, repoDid); err != nil || !got { t.Errorf("collab %s should have push at repoDid, allowed=%v err=%v", c, got, err) } if got, err := e.IsSettingsAllowed(c, rbacDomain, oldNameKey); err != nil || got { t.Errorf("collab %s old policy should be wiped, allowed=%v err=%v", c, got, err) } assertEach(rest[1:]) } assertEach(collabs)}
func TestMigrateLegacyRepoCasbin_RenameSiblingsEachWiped(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, e := newTestSpindleDB(t)
if err := e.AddSpindle(rbacDomain); err != nil { t.Fatalf("AddSpindle: %v", err) }
owner := syntax.DID("did:plc:akshay") repoDid := syntax.DID("did:plc:di4gol2smljyj6gjnjdu5qrg") siblings := []string{"pre-rename-life", "i-renamed-this", "post-rename-rename", "post-rename-renamed-again"}
var seedAll func(rest []string) error seedAll = func(rest []string) error { if len(rest) == 0 { return nil } if err := e.AddRepo(owner.String(), rbacDomain, owner.String()+"/"+rest[0]); err != nil { return err } return seedAll(rest[1:]) } if err := seedAll(siblings); err != nil { t.Fatalf("seed siblings: %v", err) }
var run func(rest []string) run = func(rest []string) { if len(rest) == 0 { return } migrateLegacyRepoCasbin(ctx, d, e, logger, owner, "", syntax.RecordKey(rest[0]), repoDid) run(rest[1:]) } run(siblings)
var assertWiped func(rest []string) assertWiped = func(rest []string) { if len(rest) == 0 { return } key := owner.String() + "/" + rest[0] if got, err := e.IsSettingsAllowed(owner.String(), rbacDomain, key); err != nil || got { t.Errorf("rename sibling %s should be wiped, allowed=%v err=%v", rest[0], got, err) } assertWiped(rest[1:]) } assertWiped(siblings)
if got, err := e.IsSettingsAllowed(owner.String(), rbacDomain, repoDid.String()); err != nil || !got { t.Errorf("owner should retain settings at repoDid, allowed=%v err=%v", got, err) }}
func TestMigrateLegacyRepoCasbin_StrandedCollabWiped(t *testing.T) { ctx := context.Background() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, e := newTestSpindleDB(t)
if err := e.AddSpindle(rbacDomain); err != nil { t.Fatalf("AddSpindle: %v", err) }
owner := "did:plc:akshay" repoDid := "did:plc:boltless" displayName := "myrepo" rkey := "3kspindlerkey00a" strandedCollab := "did:plc:nautilus" oldNameKey := owner + "/" + displayName
if err := e.AddRepo(owner, rbacDomain, oldNameKey); err != nil { t.Fatalf("seed AddRepo at Name key: %v", err) } if err := e.AddCollaborator(strandedCollab, rbacDomain, oldNameKey); err != nil { t.Fatalf("seed stranded collab at Name key: %v", err) }
migrateLegacyRepoCasbin(ctx, d, e, logger, syntax.DID(owner), displayName, syntax.RecordKey(rkey), syntax.DID(repoDid))
if got, err := e.IsSettingsAllowed(strandedCollab, rbacDomain, oldNameKey); err != nil || got { t.Errorf("stranded collab should be wiped from old key, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(owner, rbacDomain, oldNameKey); err != nil || got { t.Errorf("owner old policy should be wiped, allowed=%v err=%v", got, err) } if got, err := e.IsSettingsAllowed(owner, rbacDomain, repoDid); err != nil || !got { t.Errorf("owner should have settings at new repoDid key, allowed=%v err=%v", got, err) }}