Monorepo for Tangled
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862package appview
import ( "context" "database/sql" "encoding/json" "errors" "log/slog" "net/url" "path/filepath" "testing"
"github.com/bluesky-social/indigo/atproto/syntax" jmodels "github.com/bluesky-social/jetstream/pkg/models" "tangled.org/core/api/tangled" "tangled.org/core/appview/db" "tangled.org/core/appview/models" "tangled.org/core/appview/notify" "tangled.org/core/orm" "tangled.org/core/rbac" "tangled.org/core/repoverify")
func mustKnotURL(t *testing.T, raw string) *url.URL { t.Helper() u, err := repoverify.ParseKnotEndpoint(raw, true) if err != nil { t.Fatalf("ParseKnotEndpoint(%q): %v", raw, err) } return u}
func acceptOwner(t *testing.T, e *jmodels.Event) repoverify.Verifier { t.Helper() knot := mustKnotURL(t, "https://knot.example") return func(_ context.Context, repoDid repoverify.RepoDid) (repoverify.Result, error) { return repoverify.Result{ RepoDid: repoDid, OwnerDid: repoverify.OwnerDid(e.Did), KnotURL: knot, }, nil }}
func stubVerifier(result repoverify.Result, err error) repoverify.Verifier { return func(_ context.Context, _ repoverify.RepoDid) (repoverify.Result, error) { return result, err }}
type spyNotifier struct { notify.BaseNotifier creates int deletes int renames int}
func (s *spyNotifier) NewRepo(_ context.Context, _ *models.Repo) { s.creates++ }func (s *spyNotifier) DeleteRepo(_ context.Context, _ *models.Repo) { s.deletes++ }func (s *spyNotifier) RenameRepo(_ context.Context, _ syntax.DID, _, _ *models.Repo) { s.renames++}
func newTestIngester(t *testing.T) (*Ingester, *spyNotifier) { t.Helper() path := filepath.Join(t.TempDir(), "test.db") d, err := db.Make(context.Background(), path) if err != nil { t.Fatalf("db.Make: %v", err) } t.Cleanup(func() { d.Close() }) enforcer, err := rbac.NewEnforcer(path) if err != nil { t.Fatalf("rbac.NewEnforcer: %v", err) }
spy := &spyNotifier{} ing := &Ingester{ Db: d, Enforcer: enforcer, Logger: slog.New(slog.DiscardHandler), Notifier: spy, } return ing, spy}
func withVerifier(ing *Ingester, v repoverify.Verifier) *Ingester { ing.Verifier = v return ing}
func ingestAcceptingOwner(t *testing.T, ing *Ingester, e *jmodels.Event) error { t.Helper() ing.Verifier = acceptOwner(t, e) return ing.ingestRepo(context.Background(), e, ing.Logger)}
func seedRepoRow(t *testing.T, ing *Ingester, did, knot, name, rkey, repoDid string) *models.Repo { t.Helper() tx, err := ing.Db.Begin() if err != nil { t.Fatalf("Begin: %v", err) } repo := &models.Repo{ Did: did, Name: name, Knot: knot, Rkey: rkey, RepoDid: repoDid, } if err := db.AddRepo(tx, repo); err != nil { t.Fatalf("AddRepo: %v", err) } if err := tx.Commit(); err != nil { t.Fatalf("Commit: %v", err) } return repo}
func ptr[T any](v T) *T { return &v }
func makeEvent(t *testing.T, op string, did, rkey string, record tangled.Repo) *jmodels.Event { t.Helper() raw, err := json.Marshal(record) if err != nil { t.Fatalf("marshal record: %v", err) } return &jmodels.Event{ Did: did, Kind: jmodels.EventKindCommit, Commit: &jmodels.Commit{ Operation: op, Collection: tangled.RepoNSID, RKey: rkey, Record: raw, }, }}
func makeDeleteEvent(did, rkey string) *jmodels.Event { return &jmodels.Event{ Did: did, Kind: jmodels.EventKindCommit, Commit: &jmodels.Commit{ Operation: jmodels.CommitOperationDelete, Collection: tangled.RepoNSID, RKey: rkey, }, }}
func loadRepo(t *testing.T, ing *Ingester, did, rkey string) *models.Repo { t.Helper() r, err := db.GetRepo(ing.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", rkey), ) if err != nil { t.Fatalf("GetRepo: %v", err) } return r}
func assertRepoOwnerPermissions(t *testing.T, ing *Ingester, owner, knot, repo string) { t.Helper() for _, perm := range []string{"repo:settings", "repo:push", "repo:owner"} { ok, err := ing.Enforcer.E.Enforce(owner, knot, repo, perm) if err != nil { t.Fatalf("Enforce(%q): %v", perm, err) } if !ok { t.Fatalf("owner missing %s permission for %s", perm, repo) } }}
func assertNoRepoPolicies(t *testing.T, ing *Ingester, knot, repo string) { t.Helper() for _, perm := range []string{"repo:settings", "repo:push", "repo:owner", "repo:delete", "repo:invite", "repo:collaborator"} { policies, err := ing.Enforcer.E.GetFilteredPolicy(1, knot, repo, perm) if err != nil { t.Fatalf("GetFilteredPolicy(%q): %v", perm, err) } if len(policies) != 0 { t.Fatalf("expected no %s policies for %s, got %v", perm, repo, policies) } }}
func TestIngestRepo_CreateInsertsNewRow(t *testing.T) { ing, spy := newTestIngester(t)
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "myrepo", tangled.Repo{ Knot: "knot.example", Name: ptr("MyRepo"), Description: ptr("a test repo"), RepoDid: ptr("did:plc:repo1"), })
if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
r := loadRepo(t, ing, "did:plc:akshay", "myrepo") if r.Name != "MyRepo" { t.Errorf("name = %q, want %q", r.Name, "MyRepo") } if r.Description != "a test repo" { t.Errorf("description = %q", r.Description) } if r.RepoDid != "did:plc:repo1" { t.Errorf("repoDid = %q", r.RepoDid) } if spy.creates != 1 { t.Errorf("NewRepo called %d times, want 1", spy.creates) } assertRepoOwnerPermissions(t, ing, "did:plc:akshay", "knot.example", "did:plc:repo1")}
func TestIngestRepo_CreateSkipsIfRowExists(t *testing.T) { ing, spy := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "myrepo", "myrepo", "did:plc:repo1")
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "myrepo", tangled.Repo{ Knot: "knot.example", Name: ptr("myrepo"), RepoDid: ptr("did:plc:repo1"), })
if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) } if spy.creates != 0 { t.Errorf("row already exists, NewRepo should not be called but was called %d times", spy.creates) } assertRepoOwnerPermissions(t, ing, "did:plc:akshay", "knot.example", "did:plc:repo1")}
func TestIngestRepo_CreateCascadesRename(t *testing.T) { ing, spy := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "oldname", "oldname", "did:plc:repo1")
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "newname", tangled.Repo{ Knot: "knot.example", Name: ptr("NewName"), RepoDid: ptr("did:plc:repo1"), })
if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
_, err := db.GetRepo(ing.Db, orm.FilterEq("did", "did:plc:akshay"), orm.FilterEq("rkey", "oldname"), ) if !errors.Is(err, sql.ErrNoRows) { t.Errorf("old rkey row should be gone, got err = %v", err) }
r := loadRepo(t, ing, "did:plc:akshay", "newname") if r.Name != "NewName" { t.Errorf("name = %q, want %q", r.Name, "NewName") } if r.RepoDid != "did:plc:repo1" { t.Errorf("repoDid = %q", r.RepoDid) }
hint, err := db.LookupRepoRename(ing.Db, "did:plc:akshay", "oldname") if err != nil { t.Fatalf("LookupRepoRename: %v", err) } if hint == nil { t.Fatal("expected rename history, got nil") }
if spy.renames != 1 { t.Errorf("RenameRepo called %d times, want 1", spy.renames) } if spy.creates != 0 { t.Errorf("rename should not create: NewRepo called %d times, want 0", spy.creates) }}
func TestIngestRepo_CreateNoRepoDidSkipped(t *testing.T) { ing, spy := newTestIngester(t)
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "myrepo", tangled.Repo{ Knot: "knot.example", Name: ptr("myrepo"), })
if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) } if spy.creates != 0 { t.Errorf("NewRepo called %d times, want 0", spy.creates) }}
func TestIngestRepo_UpdateMetadata(t *testing.T) { ing, _ := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "foo", "foo", "did:plc:repo1")
e := makeEvent(t, jmodels.CommitOperationUpdate, "did:plc:akshay", "foo", tangled.Repo{ Knot: "knot.example", Name: ptr("foo"), Description: ptr("updated description"), Website: ptr("https://example.com"), Topics: []string{"go", "test"}, RepoDid: ptr("did:plc:repo1"), })
if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
r := loadRepo(t, ing, "did:plc:akshay", "foo") if r.Description != "updated description" { t.Errorf("description = %q", r.Description) } if r.Website != "https://example.com" { t.Errorf("website = %q", r.Website) } if got := r.TopicStr(); got != "go test" { t.Errorf("topics = %q", got) }}
func TestIngestRepo_UpdateDisplayName(t *testing.T) { ing, _ := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "foo", "foo", "did:plc:repo1")
e := makeEvent(t, jmodels.CommitOperationUpdate, "did:plc:akshay", "foo", tangled.Repo{ Knot: "knot.example", Name: ptr("Foo"), RepoDid: ptr("did:plc:repo1"), })
if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
r := loadRepo(t, ing, "did:plc:akshay", "foo") if r.Name != "Foo" { t.Errorf("name = %q, want %q", r.Name, "Foo") } if r.Rkey != "foo" { t.Errorf("rkey should be unchanged but got %q, want %q", r.Rkey, "foo") }}
func TestIngestRepo_UpdateNothingChangedNoOp(t *testing.T) { ing, _ := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "foo", "foo", "did:plc:repo1")
e := makeEvent(t, jmodels.CommitOperationUpdate, "did:plc:akshay", "foo", tangled.Repo{ Knot: "knot.example", Name: ptr("foo"), RepoDid: ptr("did:plc:repo1"), })
if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
r := loadRepo(t, ing, "did:plc:akshay", "foo") if r.Name != "foo" { t.Errorf("name = %q, want unchanged %q", r.Name, "foo") }}
func TestIngestRepo_UnknownRowSkipped(t *testing.T) { ops := []string{jmodels.CommitOperationUpdate, jmodels.CommitOperationDelete} for _, op := range ops { t.Run(op, func(t *testing.T) { ing, _ := newTestIngester(t)
var e *jmodels.Event switch op { case jmodels.CommitOperationUpdate: e = makeEvent(t, op, "did:plc:nobody", "ghost", tangled.Repo{ Knot: "knot.example", Name: ptr("ghost"), RepoDid: ptr("did:plc:nope"), }) case jmodels.CommitOperationDelete: e = makeDeleteEvent("did:plc:nobody", "ghost") }
if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) } }) }}
func TestIngestRepo_UpdateNoRepoDidSkipped(t *testing.T) { ing, _ := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "foo", "foo", "did:plc:repo1")
e := makeEvent(t, jmodels.CommitOperationUpdate, "did:plc:akshay", "foo", tangled.Repo{ Knot: "knot.example", Name: ptr("bar"), })
if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
r := loadRepo(t, ing, "did:plc:akshay", "foo") if r.Name != "foo" { t.Errorf("name = %q, want unchanged %q", r.Name, "foo") }}
func TestIngestRepo_DeleteRemovesRow(t *testing.T) { ing, _ := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "foo", "foo", "did:plc:repo1")
e := makeDeleteEvent("did:plc:akshay", "foo") if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
_, err := db.GetRepo(ing.Db, orm.FilterEq("did", "did:plc:akshay"), orm.FilterEq("rkey", "foo"), ) if !errors.Is(err, sql.ErrNoRows) { t.Errorf("expected row to be deleted, got err = %v", err) }}
func TestIngestRepo_DeleteWipesRbac(t *testing.T) { ing, _ := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "foo", "foo", "did:plc:repo1") if err := ing.ensureRepoOwnerPermissions("did:plc:akshay", "knot.example", "did:plc:repo1"); err != nil { t.Fatalf("ensureRepoOwnerPermissions: %v", err) } if err := ing.Enforcer.AddCollaborator("did:plc:boltless", "knot.example", "did:plc:repo1"); err != nil { t.Fatalf("AddCollaborator: %v", err) } if err := ing.Enforcer.E.SavePolicy(); err != nil { t.Fatalf("SavePolicy: %v", err) } assertRepoOwnerPermissions(t, ing, "did:plc:akshay", "knot.example", "did:plc:repo1")
e := makeDeleteEvent("did:plc:akshay", "foo") if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
assertNoRepoPolicies(t, ing, "knot.example", "did:plc:repo1")}
func TestIngestRepo_MalformedRecord(t *testing.T) { ing, _ := newTestIngester(t)
e := &jmodels.Event{ Did: "did:plc:akshay", Kind: jmodels.EventKindCommit, Commit: &jmodels.Commit{ Operation: jmodels.CommitOperationUpdate, Collection: tangled.RepoNSID, RKey: "rkey1", Record: json.RawMessage("{not json"), }, }
if err := ingestAcceptingOwner(t, ing, e); err == nil { t.Errorf("ingestRepo with malformed record: err = nil, want error") }}
func TestIngestRepo_RenameDeleteSequenceNoTornState(t *testing.T) { ing, spy := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "oldname", "oldname", "did:plc:repo1")
if _, err := ing.Db.Exec( `insert into stars (did, rkey, subject_type, subject) values (?, ?, ?, ?)`, "did:plc:boltless", "star1", "repo", "did:plc:repo1", ); err != nil { t.Fatalf("seed star: %v", err) }
createEvt := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "newname", tangled.Repo{ Knot: "knot.example", Name: ptr("NewName"), RepoDid: ptr("did:plc:repo1"), }) if err := ingestAcceptingOwner(t, ing, createEvt); err != nil { t.Fatalf("ingest create: %v", err) }
deleteEvt := makeDeleteEvent("did:plc:akshay", "oldname") if err := ingestAcceptingOwner(t, ing, deleteEvt); err != nil { t.Fatalf("ingest delete: %v", err) }
r := loadRepo(t, ing, "did:plc:akshay", "newname") if r.Name != "NewName" { t.Errorf("name = %q, want %q", r.Name, "NewName") } if r.RepoDid != "did:plc:repo1" { t.Errorf("repoDid = %q, want %q", r.RepoDid, "did:plc:repo1") }
_, err := db.GetRepo(ing.Db, orm.FilterEq("did", "did:plc:akshay"), orm.FilterEq("rkey", "oldname"), ) if !errors.Is(err, sql.ErrNoRows) { t.Errorf("old rkey should be gone, got err = %v", err) }
var starSubject string if err := ing.Db.QueryRow(`select subject from stars where did = ?`, "did:plc:boltless").Scan(&starSubject); err != nil { t.Fatalf("query star: %v", err) } if starSubject != "did:plc:repo1" { t.Errorf("star subject = %q, want %q", starSubject, "did:plc:repo1") }
if spy.renames != 1 { t.Errorf("RenameRepo called %d times, want 1", spy.renames) } if spy.creates != 0 { t.Errorf("rename should not create: NewRepo called %d times, want 0", spy.creates) } if spy.deletes != 0 { t.Errorf("old rkey already gone, DeleteRepo should not be called but was called %d times", spy.deletes) }}
func TestIngestRepo_CreateFallsBackToRkeyForName(t *testing.T) { ing, _ := newTestIngester(t)
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "myrepo", tangled.Repo{ Knot: "knot.example", RepoDid: ptr("did:plc:repo1"), })
if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
r := loadRepo(t, ing, "did:plc:akshay", "myrepo") if r.Name != "myrepo" { t.Errorf("name should fall back to rkey: got %q, want %q", r.Name, "myrepo") }}
func TestIngestRepo_CreateSquatRejected(t *testing.T) { ing, spy := newTestIngester(t)
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:boltless", "squatrepo", tangled.Repo{ Knot: "knot.example", RepoDid: ptr("did:plc:akshays-repo"), })
withVerifier(ing, stubVerifier(repoverify.Result{ RepoDid: "did:plc:akshays-repo", OwnerDid: "did:plc:akshay", KnotURL: mustKnotURL(t, "https://knot.example"), }, nil))
if err := ing.ingestRepo(context.Background(), e, ing.Logger); err != nil { t.Fatalf("ingestRepo: %v", err) }
if _, err := db.GetRepo(ing.Db, orm.FilterEq("did", "did:plc:boltless"), orm.FilterEq("rkey", "squatrepo"), ); !errors.Is(err, sql.ErrNoRows) { t.Fatalf("boltless's squat row should not exist, got err=%v", err) } if spy.creates != 0 { t.Errorf("NewRepo called %d times despite rejection", spy.creates) }}
func TestIngestRepo_CreateHijackExistingRepoRejected(t *testing.T) { ing, spy := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "myrepo", "akshayskey", "did:plc:akshays-repo")
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:boltless", "takeover", tangled.Repo{ Knot: "knot.example", RepoDid: ptr("did:plc:akshays-repo"), })
withVerifier(ing, stubVerifier(repoverify.Result{ RepoDid: "did:plc:akshays-repo", OwnerDid: "did:plc:akshay", KnotURL: mustKnotURL(t, "https://knot.example"), }, nil))
if err := ing.ingestRepo(context.Background(), e, ing.Logger); err != nil { t.Fatalf("ingestRepo: %v", err) }
akshay := loadRepo(t, ing, "did:plc:akshay", "akshayskey") if akshay.Did != "did:plc:akshay" || akshay.Rkey != "akshayskey" { t.Errorf("akshay's row mutated: %+v", akshay) } if spy.renames != 0 { t.Errorf("RenameRepo called %d times despite rejection", spy.renames) }}
func TestIngestRepo_CreateRenameIgnoresRkeyDrift(t *testing.T) { ing, spy := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "oldname", "oldrkey", "did:plc:akshays-repo")
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "newrkey", tangled.Repo{ Knot: "knot.example", Name: ptr("newname"), RepoDid: ptr("did:plc:akshays-repo"), })
withVerifier(ing, stubVerifier(repoverify.Result{ RepoDid: "did:plc:akshays-repo", OwnerDid: "did:plc:akshay", KnotURL: mustKnotURL(t, "https://knot.example"), }, nil))
if err := ing.ingestRepo(context.Background(), e, ing.Logger); err != nil { t.Fatalf("ingestRepo: %v", err) }
r := loadRepo(t, ing, "did:plc:akshay", "newrkey") if r.Name != "newname" { t.Errorf("rename did not apply despite matching owner: name=%q", r.Name) } if spy.renames != 1 { t.Errorf("RenameRepo called %d times, want 1", spy.renames) }}
func TestIngestRepo_CreateVerifierTransientErrorPropagates(t *testing.T) { ing, spy := newTestIngester(t)
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "myrepo", tangled.Repo{ Knot: "knot.example", RepoDid: ptr("did:plc:akshays-repo"), })
withVerifier(ing, stubVerifier(repoverify.Result{}, errors.New("knot unreachable")))
err := ing.ingestRepo(context.Background(), e, ing.Logger) if err == nil { t.Fatalf("expected error on transient verifier failure, got nil") } if spy.creates != 0 { t.Errorf("NewRepo called %d times despite verifier error", spy.creates) }}
func TestIngestRepo_UpdateRejectsOwnerMismatch(t *testing.T) { ing, _ := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "myrepo", "akshayskey", "did:plc:akshays-repo")
e := makeEvent(t, jmodels.CommitOperationUpdate, "did:plc:boltless", "akshayskey", tangled.Repo{ Knot: "knot.example", Description: ptr("boltless hijacks metadata"), RepoDid: ptr("did:plc:akshays-repo"), })
withVerifier(ing, stubVerifier(repoverify.Result{ RepoDid: "did:plc:akshays-repo", OwnerDid: "did:plc:akshay", KnotURL: mustKnotURL(t, "https://knot.example"), }, nil))
if err := ing.ingestRepo(context.Background(), e, ing.Logger); err != nil { t.Fatalf("ingestRepo: %v", err) }
akshay := loadRepo(t, ing, "did:plc:akshay", "akshayskey") if akshay.Description == "boltless hijacks metadata" { t.Errorf("update by non-owner applied: %+v", akshay) }}
func TestIngestRepo_CreateInvalidRepoDidRejected(t *testing.T) { ing, spy := newTestIngester(t)
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "myrepo", tangled.Repo{ Knot: "knot.example", RepoDid: ptr("did:plc:"), })
verifierCalled := false withVerifier(ing, func(_ context.Context, _ repoverify.RepoDid) (repoverify.Result, error) { verifierCalled = true return repoverify.Result{}, nil })
if err := ing.ingestRepo(context.Background(), e, ing.Logger); err != nil { t.Fatalf("ingestRepo: %v", err) } if verifierCalled { t.Errorf("verifier was called with an invalid repoDid") } if spy.creates != 0 { t.Errorf("NewRepo called %d times despite invalid repoDid", spy.creates) }}
func TestIngestRepo_NilVerifierFailsClosed(t *testing.T) { ing, spy := newTestIngester(t)
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "myrepo", tangled.Repo{ Knot: "knot.example", RepoDid: ptr("did:plc:akshays-repo"), })
err := ing.ingestRepo(context.Background(), e, ing.Logger) if err == nil { t.Fatalf("expected error when Verifier is nil, got nil") } if spy.creates != 0 { t.Errorf("NewRepo called %d times despite nil verifier", spy.creates) }}
func TestIngestRepo_CreateRejectsKnotMismatch(t *testing.T) { ing, spy := newTestIngester(t)
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "myrepo", tangled.Repo{ Knot: "evil.example", RepoDid: ptr("did:plc:akshays-repo"), })
withVerifier(ing, stubVerifier(repoverify.Result{ RepoDid: "did:plc:akshays-repo", OwnerDid: "did:plc:akshay", KnotURL: mustKnotURL(t, "https://knot.example"), }, nil))
if err := ing.ingestRepo(context.Background(), e, ing.Logger); err != nil { t.Fatalf("ingestRepo: %v", err) } if _, err := db.GetRepo(ing.Db, orm.FilterEq("did", "did:plc:akshay"), orm.FilterEq("rkey", "myrepo"), ); !errors.Is(err, sql.ErrNoRows) { t.Fatalf("row should not be created for spoofed knot, err=%v", err) } if spy.creates != 0 { t.Errorf("NewRepo called %d times despite knot mismatch", spy.creates) }}
func TestIngestRepo_UpdateRejectsKnotMismatch(t *testing.T) { ing, _ := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "myrepo", "akshayskey", "did:plc:akshays-repo")
e := makeEvent(t, jmodels.CommitOperationUpdate, "did:plc:akshay", "akshayskey", tangled.Repo{ Knot: "evil.example", Description: ptr("redirected clone target"), RepoDid: ptr("did:plc:akshays-repo"), })
withVerifier(ing, stubVerifier(repoverify.Result{ RepoDid: "did:plc:akshays-repo", OwnerDid: "did:plc:akshay", KnotURL: mustKnotURL(t, "https://knot.example"), }, nil))
if err := ing.ingestRepo(context.Background(), e, ing.Logger); err != nil { t.Fatalf("ingestRepo: %v", err) } akshay := loadRepo(t, ing, "did:plc:akshay", "akshayskey") if akshay.Description == "redirected clone target" { t.Errorf("update with spoofed knot applied: %+v", akshay) } if akshay.Knot != "knot.example" { t.Errorf("row knot mutated to %q, want knot.example", akshay.Knot) }}
func TestIngestRepo_UpdateRejectsRepoDidMutation(t *testing.T) { ing, _ := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "myrepo", "akshayskey", "did:plc:akshays-repo")
e := makeEvent(t, jmodels.CommitOperationUpdate, "did:plc:akshay", "akshayskey", tangled.Repo{ Knot: "knot.example", Description: ptr("sneaky repoDid swap"), RepoDid: ptr("did:plc:other-repo"), })
withVerifier(ing, stubVerifier(repoverify.Result{ RepoDid: "did:plc:other-repo", OwnerDid: "did:plc:akshay", KnotURL: mustKnotURL(t, "https://knot.example"), }, nil))
if err := ing.ingestRepo(context.Background(), e, ing.Logger); err != nil { t.Fatalf("ingestRepo: %v", err) } akshay := loadRepo(t, ing, "did:plc:akshay", "akshayskey") if akshay.RepoDid != "did:plc:akshays-repo" { t.Errorf("repoDid mutated to %q, want did:plc:akshays-repo", akshay.RepoDid) } if akshay.Description == "sneaky repoDid swap" { t.Errorf("metadata from repoDid-mutating update applied: %+v", akshay) }}
func renameAliasExists(t *testing.T, ing *Ingester, ownerDid, oldRkey string) bool { t.Helper() var n int if err := ing.Db.QueryRow( `select count(*) from repo_renames where owner_did = ? and old_rkey = ?`, ownerDid, oldRkey, ).Scan(&n); err != nil { t.Fatalf("count repo_renames %q: %v", oldRkey, err) } return n > 0}
func TestIngestRepo_RenameClearsCollidingAlias(t *testing.T) { ing, _ := newTestIngester(t) seedRepoRow(t, ing, "did:plc:akshay", "knot.example", "anemone-old", "anemone-old", "did:plc:anemone") if err := db.RecordRepoRename(ing.Db, "did:plc:akshay", "anemone", "did:plc:anemone"); err != nil { t.Fatalf("RecordRepoRename: %v", err) }
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "3mpxmsvicr2zn", tangled.Repo{ Knot: "knot.example", Name: ptr("anemone"), RepoDid: ptr("did:plc:anemone"), }) if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
if renameAliasExists(t, ing, "did:plc:akshay", "anemone") { t.Error("alias equal to the new live slug must be cleared to avoid a self-redirect loop") } if !renameAliasExists(t, ing, "did:plc:akshay", "anemone-old") { t.Error("alias for the prior slug must be recorded and survive") }}
func TestIngestRepo_InsertClearsCollidingAlias(t *testing.T) { ing, _ := newTestIngester(t) if err := db.RecordRepoRename(ing.Db, "did:plc:akshay", "clam", "did:plc:clams-former-repo"); err != nil { t.Fatalf("RecordRepoRename: %v", err) }
e := makeEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "3mpxmfgowwck3", tangled.Repo{ Knot: "knot.example", Name: ptr("clam"), RepoDid: ptr("did:plc:clam-repo"), }) if err := ingestAcceptingOwner(t, ing, e); err != nil { t.Fatalf("ingestRepo: %v", err) }
if renameAliasExists(t, ing, "did:plc:akshay", "clam") { t.Error("stale alias must be cleared when a live repo claims that slug") }}