Something went wrong. Try again.
An ATProto Powered Reputation Network
Something went wrong. Try again.
13 kB · 330 lines
Go
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331package storage
import ( "context" "database/sql" "errors" "path/filepath" "provenance/internal/domain" "provenance/internal/trust" "testing" "time")
const listDate = "2026-10-09T00:00:00Z"
func listFixture() domain.ListSnapshot { const uri = "at://did:plc:owner/app.bsky.graph.list/mod" return domain.ListSnapshot{ Blocks: []domain.ListRecord{{Author: "did:plc:a", Source: domain.ListBlockCollection, RecordKey: "subscription", ListURI: uri, CreatedAt: listDate}}, Lists: []domain.ListData{{URI: uri, TimeUS: 100, Records: []domain.ListRecord{ {Author: "did:plc:owner", Source: domain.ListCollection, RecordKey: "mod", ListURI: uri, Purpose: domain.ModerationList, CreatedAt: listDate}, {Author: "did:plc:owner", Source: domain.ListItemCollection, RecordKey: "member", ListURI: uri, Subject: "did:plc:b", CreatedAt: listDate}, }}}, }}
func TestBlockedListStorage(t *testing.T) { ctx := context.Background() s, err := OpenWithSoft(filepath.Join(t.TempDir(), "lists.db"), domain.SoftParticipantLists) if err != nil { t.Fatal(err) } defer s.Close() snapshot := listFixture() author := snapshot.Blocks[0].Author if err := s.Join(ctx, author); err != nil { t.Fatal(err) } follow := domain.Vouch{Author: author, Subject: "did:plc:b", Source: domain.FollowCollection, RecordKey: "follow", Score: .5} if err := s.ReplaceWithLists(ctx, author, []domain.Vouch{follow}, snapshot, time.UnixMicro(100)); err != nil { t.Fatal(err) } graph := func(tag string, mode domain.Mode) []domain.Vouch { t.Helper() rows, err := s.Graph(ctx, tag, mode, 100) if err != nil { t.Fatal(err) } return trust.Select(rows, tag, mode) } if got := graph("", domain.Default); len(got) != 1 || got[0].Score != -.5 || got[0].Source != domain.ListBlockCollection || got[0].ListURI != snapshot.Lists[0].URI || got[0].ItemKey != "member" { t.Fatalf("list edge: %+v", got) } if got := graph("work", domain.Tagged); len(got) != 0 { t.Fatalf("tag-only: %+v", got) } if got := graph("work", domain.Fallback); len(got) != 1 || got[0].Score != -.5 { t.Fatalf("fallback: %+v", got) } if _, err := s.Participant(ctx, "did:plc:owner"); !errors.Is(err, sql.ErrNoRows) { t.Fatalf("owner enrolled: %v", err) } // Explicit zero scores override list blocks in both default and topic fallback queries. for index, tag := range []string{"", "work"} { v := domain.Vouch{Author: author, Subject: follow.Subject, Tag: tag, Score: 0} key := domain.RecordKey(v.Subject, tag) stamp := int64(101 + 2*index) if err := s.Apply(ctx, "explicit", domain.Change{Kind: domain.RecordChange, DID: author, RecordKey: key, Vouch: &v, TimeUS: stamp}); err != nil { t.Fatal(err) } mode := domain.Default if tag != "" { mode = domain.Fallback } if got := graph(tag, mode); len(got) != 1 || got[0].Score != 0 || got[0].Source != domain.Collection { t.Fatalf("explicit precedence: %+v", got) } if err := s.Apply(ctx, "explicit", domain.Change{Kind: domain.RecordChange, DID: author, RecordKey: key, TimeUS: stamp + 1}); err != nil { t.Fatal(err) } } apply := func(r *domain.ListRecord, author, source, key string, stamp int64) { t.Helper() if err := s.Apply(ctx, "lists", domain.Change{Kind: domain.RecordChange, DID: author, Source: source, RecordKey: key, ListRecord: r, TimeUS: stamp}); err != nil { t.Fatal(err) } } // A duplicate membership cannot multiply the negative assertion. duplicate := snapshot.Lists[0].Records[1] duplicate.RecordKey = "member-2" apply(&duplicate, duplicate.Author, duplicate.Source, duplicate.RecordKey, 110) if got := graph("", domain.Default); len(got) != 1 { t.Fatalf("duplicate members: %+v", got) } rows, revision, err := s.TrustGraph(ctx, "", domain.Default, 100) if err != nil || len(rows) < 1 { t.Fatal(err) } score := -.5 deps := []trust.Dependency{{Author: author, Source: domain.ListBlockCollection, RecordKey: "subscription"}, {Author: duplicate.Author, Source: domain.ListItemCollection, RecordKey: "member"}, {Author: duplicate.Author, Source: domain.ListCollection, RecordKey: "mod"}} now := time.UnixMicro(1000) if saved, err := s.SaveTrust(ctx, "list-cache", trust.Result{Status: trust.CalculatedStatus, Score: &score}, deps, revision, now.Add(time.Hour)); !saved || err != nil { t.Fatalf("save: %v %v", saved, err) } apply(nil, duplicate.Author, duplicate.Source, "member-2", 120) if _, err := s.CachedTrust(ctx, "list-cache", now); err != nil { t.Fatalf("unselected membership invalidation: %v", err) } apply(nil, duplicate.Author, duplicate.Source, "member", 130) if _, err := s.CachedTrust(ctx, "list-cache", now); !errors.Is(err, sql.ErrNoRows) { t.Fatalf("selected membership retained cache: %v", err) } apply(&snapshot.Lists[0].Records[1], duplicate.Author, duplicate.Source, "member", 125) if got := graph("", domain.Default); len(got) != 1 || got[0].Score != .5 { t.Fatalf("membership replay: %+v", got) } apply(&snapshot.Lists[0].Records[1], duplicate.Author, duplicate.Source, "member", 140) apply(nil, author, domain.ListBlockCollection, "subscription", 150) if got := graph("", domain.Default); len(got) != 1 || got[0].Score != .5 { t.Fatalf("unblocked list: %+v", got) } apply(&snapshot.Blocks[0], author, domain.ListBlockCollection, "subscription", 160) apply(nil, duplicate.Author, domain.ListCollection, "mod", 170) if got := graph("", domain.Default); len(got) != 1 || got[0].Score != .5 { t.Fatalf("deleted list: %+v", got) } apply(&snapshot.Lists[0].Records[0], duplicate.Author, domain.ListCollection, "mod", 165) if got := graph("", domain.Default); len(got) != 1 || got[0].Score != .5 { t.Fatalf("list deletion replay: %+v", got) }}
func TestListScopeAndSnapshots(t *testing.T) { ctx := context.Background() for _, mode := range []domain.SoftMode{domain.SoftDisabled, domain.SoftParticipants, domain.SoftAllAuthors, domain.SoftParticipantLists, domain.SoftAllAuthorLists} { s, err := OpenWithSoft(filepath.Join(t.TempDir(), "scope.db"), mode) if err != nil { t.Fatal(err) } snapshot := listFixture() block := snapshot.Blocks[0] data := snapshot.Lists[0] if err := s.Apply(ctx, "stream", domain.Change{Kind: domain.RecordChange, DID: block.Author, Source: block.Source, RecordKey: block.RecordKey, ListRecord: &block, ListData: &data, TimeUS: 200}); err != nil { t.Fatal(err) } rows, err := s.Incoming(ctx, "did:plc:b", "", domain.Default, 100) if err != nil { t.Fatal(err) } want := 0 if mode == domain.SoftAllAuthorLists { want = 1 } if len(rows) != want { t.Fatalf("scope %v: %+v", mode, rows) } s.Close() }}
func TestListSnapshotRollback(t *testing.T) { ctx := context.Background() s, err := OpenWithSoft(filepath.Join(t.TempDir(), "rollback.db"), domain.SoftParticipantLists) if err != nil { t.Fatal(err) } defer s.Close() snapshot := listFixture() if err := s.Join(ctx, "did:plc:a"); err != nil { t.Fatal(err) } if err := s.ReplaceWithLists(ctx, "did:plc:a", nil, snapshot, time.UnixMicro(100)); err != nil { t.Fatal(err) } snapshot.Lists[0].Records[1].Author = "did:plc:forged" if err := s.ReplaceWithLists(ctx, "did:plc:a", nil, snapshot, time.UnixMicro(200)); err == nil { t.Fatal("forged list member accepted") } rows, err := s.Incoming(ctx, "did:plc:b", "", domain.Default, 100) if err != nil || len(rows) != 1 { t.Fatalf("rollback lost member: %+v %v", rows, err) } snapshot.Lists = nil if err := s.ReplaceWithLists(ctx, "did:plc:a", nil, snapshot, time.UnixMicro(200)); err == nil { t.Fatal("incomplete list snapshot accepted") }}
func TestMovedListMember(t *testing.T) { ctx := context.Background() s, err := OpenWithSoft(filepath.Join(t.TempDir(), "move.db"), domain.SoftParticipantLists) if err != nil { t.Fatal(err) } defer s.Close() first := listFixture() if err := s.Join(ctx, "did:plc:a"); err != nil { t.Fatal(err) } if err := s.ReplaceWithLists(ctx, "did:plc:a", nil, first, time.UnixMicro(100)); err != nil { t.Fatal(err) } const secondURI = "at://did:plc:owner/app.bsky.graph.list/second" second := domain.ListSnapshot{Blocks: []domain.ListRecord{{Author: "did:plc:c", Source: domain.ListBlockCollection, RecordKey: "block", ListURI: secondURI, CreatedAt: listDate}}, Lists: []domain.ListData{{URI: secondURI, TimeUS: 300, Records: []domain.ListRecord{{Author: "did:plc:owner", Source: domain.ListCollection, RecordKey: "second", ListURI: secondURI, Purpose: domain.ModerationList, CreatedAt: listDate}}}}} if err := s.Join(ctx, "did:plc:c"); err != nil { t.Fatal(err) } if err := s.ReplaceWithLists(ctx, "did:plc:c", nil, second, time.UnixMicro(300)); err != nil { t.Fatal(err) } moved := first.Lists[0].Records[1] moved.ListURI = secondURI if err := s.Apply(ctx, "stream", domain.Change{Kind: domain.RecordChange, DID: moved.Author, Source: moved.Source, RecordKey: moved.RecordKey, ListRecord: &moved, TimeUS: 200}); err != nil { t.Fatal(err) } rows, err := s.Incoming(ctx, moved.Subject, "", domain.Default, 100) if err != nil || len(rows) != 0 { t.Fatalf("stale membership after move: %+v %v", rows, err) }}
func TestUnknownListDeletion(t *testing.T) { ctx := context.Background() s, err := OpenWithSoft(filepath.Join(t.TempDir(), "replay.db"), domain.SoftParticipantLists) if err != nil { t.Fatal(err) } defer s.Close() snapshot := listFixture() if err := s.Join(ctx, "did:plc:a"); err != nil { t.Fatal(err) } if err := s.ReplaceWithLists(ctx, "did:plc:a", nil, snapshot, time.UnixMicro(100)); err != nil { t.Fatal(err) } item := snapshot.Lists[0].Records[1] item.RecordKey, item.Subject = "unseen", "did:plc:unseen" if err := s.Apply(ctx, "stream", domain.Change{Kind: domain.RecordChange, DID: item.Author, Source: item.Source, RecordKey: item.RecordKey, TimeUS: 300}); err != nil { t.Fatal(err) } if err := s.Apply(ctx, "stream", domain.Change{Kind: domain.RecordChange, DID: item.Author, Source: item.Source, RecordKey: item.RecordKey, ListRecord: &item, TimeUS: 200}); err != nil { t.Fatal(err) } rows, err := s.Incoming(ctx, item.Subject, "", domain.Default, 100) if err != nil || len(rows) != 0 { t.Fatalf("unknown deletion lost its tombstone: %+v %v", rows, err) }}
func TestListDependencies(t *testing.T) { ctx := context.Background() for _, source := range []string{domain.ListCollection, domain.ListBlockCollection} { t.Run(source, func(t *testing.T) { s, err := OpenWithSoft(filepath.Join(t.TempDir(), "deps.db"), domain.SoftParticipantLists) if err != nil { t.Fatal(err) } defer s.Close() snapshot := listFixture() if err := s.Join(ctx, "did:plc:a"); err != nil { t.Fatal(err) } if err := s.ReplaceWithLists(ctx, "did:plc:a", nil, snapshot, time.UnixMicro(100)); err != nil { t.Fatal(err) } _, revision, err := s.TrustGraph(ctx, "", domain.Default, 100) if err != nil { t.Fatal(err) } now := time.UnixMicro(1000) record := snapshot.Blocks[0] if source == domain.ListCollection { record = snapshot.Lists[0].Records[0] } score := -.5 if saved, err := s.SaveTrust(ctx, "path", trust.Result{Status: trust.CalculatedStatus, Score: &score}, []trust.Dependency{{Author: record.Author, Source: record.Source, RecordKey: record.RecordKey}}, revision, now.Add(time.Hour)); !saved || err != nil { t.Fatalf("cache: %v %v", saved, err) } snapshot.Lists[0].TimeUS = 200 if err := s.ReplaceWithLists(ctx, "did:plc:a", nil, snapshot, time.UnixMicro(200)); err != nil { t.Fatal(err) } if _, err := s.CachedTrust(ctx, "path", now); err != nil { t.Fatalf("unchanged snapshot invalidated path: %v", err) } if err := s.Apply(ctx, "stream", domain.Change{Kind: domain.RecordChange, DID: record.Author, Source: record.Source, RecordKey: record.RecordKey, TimeUS: 300}); err != nil { t.Fatal(err) } if _, err := s.CachedTrust(ctx, "path", now); !errors.Is(err, sql.ErrNoRows) { t.Fatalf("list dependency retained cache: %v", err) } }) }}
func TestListOwnerLifecycle(t *testing.T) { ctx := context.Background() s, err := OpenWithSoft(filepath.Join(t.TempDir(), "lifecycle.db"), domain.SoftParticipantLists) if err != nil { t.Fatal(err) } defer s.Close() snapshot := listFixture() if err := s.Join(ctx, "did:plc:a"); err != nil { t.Fatal(err) } if err := s.ReplaceWithLists(ctx, "did:plc:a", nil, snapshot, time.UnixMicro(100)); err != nil { t.Fatal(err) } for _, change := range []domain.Change{ {Kind: domain.AccountChange, DID: "did:plc:owner", Active: false, TimeUS: 200}, {Kind: domain.AccountChange, DID: "did:plc:owner", Active: true, TimeUS: 300}, {Kind: domain.AccountChange, DID: "did:plc:owner", Active: false, Status: domain.DeletedAccount, TimeUS: 400}, {Kind: domain.AccountChange, DID: "did:plc:owner", Active: true, TimeUS: 500}, {Kind: domain.RecordChange, DID: "did:plc:owner", Source: domain.ListCollection, RecordKey: "mod", ListRecord: &snapshot.Lists[0].Records[0], TimeUS: 350}, {Kind: domain.RecordChange, DID: "did:plc:owner", Source: domain.ListItemCollection, RecordKey: "member", ListRecord: &snapshot.Lists[0].Records[1], TimeUS: 360}, } { if err := s.Apply(ctx, "stream", change); err != nil { t.Fatal(err) } rows, err := s.Incoming(ctx, "did:plc:b", "", domain.Default, 100) want := 0 if change.TimeUS == 300 { want = 1 } if err != nil || len(rows) != want { t.Fatalf("lifecycle %v: %+v %v", change.TimeUS, rows, err) } }}