Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

tangled-core / knotserver / ingester_repo_test.go
6.2 kB 223 lines
1package knotserver 2 3import ( 4 "context" 5 "encoding/json" 6 "log/slog" 7 "sync" 8 "testing" 9 10 jsmodels "github.com/bluesky-social/jetstream/pkg/models" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/config" 13 "tangled.org/core/knotserver/db" 14 "tangled.org/core/log" 15) 16 17type logRecord struct { 18 Level slog.Level 19 Msg string 20 Attrs map[string]any 21} 22 23type capturingHandler struct { 24 mu *sync.Mutex 25 records *[]logRecord 26 attrs []slog.Attr 27} 28 29func newCapturingHandler() *capturingHandler { 30 return &capturingHandler{ 31 mu: &sync.Mutex{}, 32 records: &[]logRecord{}, 33 } 34} 35 36func (h *capturingHandler) Enabled(_ context.Context, _ slog.Level) bool { return true } 37 38func (h *capturingHandler) Handle(_ context.Context, r slog.Record) error { 39 rec := logRecord{Level: r.Level, Msg: r.Message, Attrs: map[string]any{}} 40 for _, a := range h.attrs { 41 rec.Attrs[a.Key] = a.Value.Any() 42 } 43 r.Attrs(func(a slog.Attr) bool { 44 rec.Attrs[a.Key] = a.Value.Any() 45 return true 46 }) 47 h.mu.Lock() 48 *h.records = append(*h.records, rec) 49 h.mu.Unlock() 50 return nil 51} 52 53func (h *capturingHandler) WithAttrs(attrs []slog.Attr) slog.Handler { 54 merged := make([]slog.Attr, 0, len(h.attrs)+len(attrs)) 55 merged = append(merged, h.attrs...) 56 merged = append(merged, attrs...) 57 return &capturingHandler{mu: h.mu, records: h.records, attrs: merged} 58} 59 60func (h *capturingHandler) WithGroup(string) slog.Handler { 61 panic("capturingHandler: WithGroup not supported") 62} 63 64func (h *capturingHandler) snapshot() []logRecord { 65 h.mu.Lock() 66 defer h.mu.Unlock() 67 out := make([]logRecord, len(*h.records)) 68 copy(out, *h.records) 69 return out 70} 71 72func newProcessRepoFixture(t *testing.T) (*Knot, context.Context, *capturingHandler) { 73 t.Helper() 74 d := newTestKnotDB(t) 75 cap := newCapturingHandler() 76 l := slog.New(cap) 77 ctx := log.IntoContext(context.Background(), l) 78 79 c := &config.Config{ 80 Server: config.Server{Hostname: "knot.example"}, 81 } 82 return &Knot{ 83 c: c, 84 db: d, 85 l: l, 86 }, ctx, cap 87} 88 89func repoEvent(t *testing.T, authorDid, rkey, rev string, record tangled.Repo, op string) *jsmodels.Event { 90 t.Helper() 91 raw, err := json.Marshal(record) 92 if err != nil { 93 t.Fatalf("marshal record: %v", err) 94 } 95 return &jsmodels.Event{ 96 Did: authorDid, 97 Kind: jsmodels.EventKindCommit, 98 Commit: &jsmodels.Commit{ 99 Operation: op, 100 Collection: tangled.RepoNSID, 101 RKey: rkey, 102 Rev: rev, 103 Record: raw, 104 }, 105 } 106} 107 108func ptr(s string) *string { return &s } 109 110func TestProcessRepo_CreateRegistersAlias(t *testing.T) { 111 h, ctx, _ := newProcessRepoFixture(t) 112 if err := h.db.StoreRepoKey("did:plc:repo1", []byte("k"), "did:plc:akshay", "foo"); err != nil { 113 t.Fatalf("StoreRepoKey: %v", err) 114 } 115 116 ev := repoEvent(t, "did:plc:akshay", "bar", "3laaaaaaaaaab", tangled.Repo{ 117 Knot: "knot.example", 118 RepoDid: ptr("did:plc:repo1"), 119 }, jsmodels.CommitOperationCreate) 120 if err := h.processRepo(ctx, ev); err != nil { 121 t.Fatalf("processRepo: %v", err) 122 } 123 124 _, current, err := h.db.CurrentRkey("did:plc:repo1") 125 if err != nil { 126 t.Fatalf("CurrentRkey: %v", err) 127 } 128 if current != "bar" { 129 t.Errorf("current rkey = %q, want bar (highest rev alias)", current) 130 } 131 132 oldDid, err := h.db.GetRepoDid("did:plc:akshay", "foo") 133 if err != nil || oldDid != "did:plc:repo1" { 134 t.Errorf("old rkey foo should still resolve: got (%q, %v)", oldDid, err) 135 } 136} 137 138func TestProcessRepo_DeleteRemovesAlias(t *testing.T) { 139 h, ctx, _ := newProcessRepoFixture(t) 140 if err := h.db.StoreRepoKey("did:plc:repo1", []byte("k"), "did:plc:akshay", "foo"); err != nil { 141 t.Fatalf("StoreRepoKey: %v", err) 142 } 143 if err := h.db.UpsertRepoAlias(db.RepoAlias{ 144 OwnerDid: "did:plc:akshay", Rkey: "bar", RepoDid: "did:plc:repo1", Rev: "3laaaaaaaaaab", 145 }); err != nil { 146 t.Fatalf("UpsertRepoAlias: %v", err) 147 } 148 149 ev := repoEvent(t, "did:plc:akshay", "bar", "3laaaaaaaaaac", tangled.Repo{}, jsmodels.CommitOperationDelete) 150 if err := h.processRepo(ctx, ev); err != nil { 151 t.Fatalf("processRepo: %v", err) 152 } 153 154 if _, err := h.db.GetRepoDid("did:plc:akshay", "bar"); err == nil { 155 t.Errorf("bar alias should have been deleted") 156 } 157 158 _, current, _ := h.db.CurrentRkey("did:plc:repo1") 159 if current != "foo" { 160 t.Errorf("current rkey after delete = %q, want foo", current) 161 } 162} 163 164func TestProcessRepo_MalformedJSONReturnsError(t *testing.T) { 165 h, ctx, _ := newProcessRepoFixture(t) 166 167 ev := &jsmodels.Event{ 168 Did: "did:plc:akshay", 169 Kind: jsmodels.EventKindCommit, 170 Commit: &jsmodels.Commit{ 171 Operation: jsmodels.CommitOperationCreate, 172 Collection: tangled.RepoNSID, 173 RKey: "rkey1", 174 Record: []byte("{not valid json"), 175 }, 176 } 177 if err := h.processRepo(ctx, ev); err == nil { 178 t.Fatalf("processRepo returned nil, want unmarshal error") 179 } 180} 181 182func TestProcessRepo_NotOwnedRejected(t *testing.T) { 183 h, ctx, _ := newProcessRepoFixture(t) 184 if err := h.db.StoreRepoKey("did:plc:repo1", []byte("k"), "did:plc:akshay", "foo"); err != nil { 185 t.Fatalf("StoreRepoKey: %v", err) 186 } 187 188 ev := repoEvent(t, "did:plc:mallory", "pwned", "3laaaaaaaaaab", tangled.Repo{ 189 Knot: "knot.example", 190 RepoDid: ptr("did:plc:repo1"), 191 }, jsmodels.CommitOperationCreate) 192 if err := h.processRepo(ctx, ev); err != nil { 193 t.Fatalf("processRepo: %v", err) 194 } 195 196 _, current, _ := h.db.CurrentRkey("did:plc:repo1") 197 if current != "foo" { 198 t.Errorf("current rkey = %q, want foo (mallory's event must be rejected)", current) 199 } 200 if _, err := h.db.GetRepoDid("did:plc:mallory", "pwned"); err == nil { 201 t.Errorf("mallory should not be able to register an alias on alice's repo") 202 } 203} 204 205func TestProcessRepo_WrongKnotIgnored(t *testing.T) { 206 h, ctx, _ := newProcessRepoFixture(t) 207 if err := h.db.StoreRepoKey("did:plc:repo1", []byte("k"), "did:plc:akshay", "foo"); err != nil { 208 t.Fatalf("StoreRepoKey: %v", err) 209 } 210 211 ev := repoEvent(t, "did:plc:akshay", "bar", "3laaaaaaaaaab", tangled.Repo{ 212 Knot: "other.example", 213 RepoDid: ptr("did:plc:repo1"), 214 }, jsmodels.CommitOperationCreate) 215 if err := h.processRepo(ctx, ev); err != nil { 216 t.Fatalf("processRepo: %v", err) 217 } 218 219 _, current, _ := h.db.CurrentRkey("did:plc:repo1") 220 if current != "foo" { 221 t.Errorf("current rkey = %q, want foo (foreign-knot event must be ignored)", current) 222 } 223}