Monorepo for Tangled tangled.org
6

Configure Feed

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

1package appview 2 3import ( 4 "bytes" 5 "encoding/json" 6 "log/slog" 7 "path/filepath" 8 "strings" 9 "testing" 10 "time" 11 12 jmodels "github.com/bluesky-social/jetstream/pkg/models" 13 "github.com/ipfs/go-cid" 14 "github.com/multiformats/go-multihash" 15 cbg "github.com/whyrusleeping/cbor-gen" 16 "tangled.org/core/api/tangled" 17 "tangled.org/core/appview/db" 18 "tangled.org/core/appview/models" 19 "tangled.org/core/appview/validator" 20 "tangled.org/core/orm" 21) 22 23func newStringIngester(t *testing.T) *Ingester { 24 t.Helper() 25 path := filepath.Join(t.TempDir(), "test.db") 26 d, err := db.Make(t.Context(), path) 27 if err != nil { 28 t.Fatalf("db.Make: %v", err) 29 } 30 t.Cleanup(func() { d.Close() }) 31 return &Ingester{ 32 Db: d, 33 Logger: slog.New(slog.DiscardHandler), 34 Validator: &validator.Validator{}, 35 } 36} 37 38func makeStringEvent(t *testing.T, op, did, rkey string, record tangled.String) *jmodels.Event { 39 t.Helper() 40 raw, err := json.Marshal(record) 41 if err != nil { 42 t.Fatalf("marshal record: %v", err) 43 } 44 return &jmodels.Event{ 45 Did: did, 46 Kind: jmodels.EventKindCommit, 47 Commit: &jmodels.Commit{ 48 Operation: op, 49 Collection: tangled.StringNSID, 50 RKey: rkey, 51 Record: raw, 52 CID: makeCID(t, &record), 53 }, 54 } 55} 56 57func makeCID(t *testing.T, record cbg.CBORMarshaler) string { 58 buf := new(bytes.Buffer) 59 if err := record.MarshalCBOR(buf); err != nil { 60 t.Fatalf("cbor marshal record: %v", err) 61 } 62 c, err := cid.NewPrefixV1(cid.DagCBOR, multihash.SHA2_256).Sum(buf.Bytes()) 63 if err != nil { 64 t.Fatalf("cid: sum: %v", err) 65 } 66 return c.String() 67} 68 69func loadString(t *testing.T, ing *Ingester, did, rkey string) (models.String, bool) { 70 t.Helper() 71 rows, err := db.GetStrings(ing.Db, 0, 72 orm.FilterEq("did", did), 73 orm.FilterEq("rkey", rkey), 74 ) 75 if err != nil { 76 t.Fatalf("GetStrings: %v", err) 77 } 78 if len(rows) == 0 { 79 return models.String{}, false 80 } 81 if len(rows) > 1 { 82 t.Fatalf("expected at most one row for (%s,%s), got %d", did, rkey, len(rows)) 83 } 84 return rows[0], true 85} 86 87func TestIngestString_CreateRoundTrip(t *testing.T) { 88 ing := newStringIngester(t) 89 created := time.Date(2025, 9, 14, 10, 30, 0, 0, time.UTC) 90 91 e := makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:boltless", "rk1", tangled.String{ 92 Filename: ptr("hello.txt"), 93 Description: ptr("a greeting"), 94 Contents: ptr("hello world\n"), 95 CreatedAt: created.Format(time.RFC3339), 96 }) 97 98 if err := ing.ingestString(t.Context(), e, ing.Logger); err != nil { 99 t.Fatalf("ingestString: %v", err) 100 } 101 102 s, ok := loadString(t, ing, "did:plc:boltless", "rk1") 103 if !ok { 104 t.Fatal("row not inserted") 105 } 106 if s.FileName != "hello.txt" { 107 t.Errorf("filename = %q, want hello.txt", s.FileName) 108 } 109 if s.Description == nil { 110 t.Errorf("description = nil") 111 } 112 if *s.Description != "a greeting" { 113 t.Errorf("description = %q", *s.Description) 114 } 115 if s.FileContent != "hello world\n" { 116 t.Errorf("contents = %q", s.FileContent) 117 } 118 if !s.Created.Equal(created) { 119 t.Errorf("created = %v, want %v (record CreatedAt must round-trip)", s.Created, created) 120 } 121 if s.Edited != nil { 122 t.Errorf("edited = %v, want nil on create", s.Edited) 123 } 124} 125 126func TestIngestString_UpdateBumpsEditedOnContentChange(t *testing.T) { 127 ing := newStringIngester(t) 128 created := time.Date(2025, 9, 14, 10, 30, 0, 0, time.UTC) 129 base := tangled.String{ 130 Filename: ptr("hello.txt"), 131 Description: ptr("a greeting"), 132 Contents: ptr("hello world\n"), 133 CreatedAt: created.Format(time.RFC3339), 134 } 135 136 if err := ing.ingestString(t.Context(), makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:boltless", "rk1", base), ing.Logger); err != nil { 137 t.Fatalf("ingestString create: %v", err) 138 } 139 140 updated := base 141 updated.Contents = ptr("hello, world!\n") 142 if err := ing.ingestString(t.Context(), makeStringEvent(t, jmodels.CommitOperationUpdate, "did:plc:boltless", "rk1", updated), ing.Logger); err != nil { 143 t.Fatalf("ingestString update: %v", err) 144 } 145 146 s, ok := loadString(t, ing, "did:plc:boltless", "rk1") 147 if !ok { 148 t.Fatal("row missing after update") 149 } 150 if s.FileContent != "hello, world!\n" { 151 t.Errorf("contents = %q, want updated value", s.FileContent) 152 } 153 if !s.Created.Equal(created) { 154 t.Errorf("update overwrote created: got %v, want %v", s.Created, created) 155 } 156 if s.Edited == nil { 157 t.Fatal("edited not set after content change") 158 } 159} 160 161func TestIngestString_UpdateNoChangeKeepsEditedNil(t *testing.T) { 162 ing := newStringIngester(t) 163 rec := tangled.String{ 164 Filename: ptr("hello.txt"), 165 Description: ptr("a greeting"), 166 Contents: ptr("hello world\n"), 167 CreatedAt: time.Date(2025, 9, 14, 10, 30, 0, 0, time.UTC).Format(time.RFC3339), 168 } 169 170 if err := ing.ingestString(t.Context(), makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "rk2", rec), ing.Logger); err != nil { 171 t.Fatalf("create: %v", err) 172 } 173 if err := ing.ingestString(t.Context(), makeStringEvent(t, jmodels.CommitOperationUpdate, "did:plc:akshay", "rk2", rec), ing.Logger); err != nil { 174 t.Fatalf("update: %v", err) 175 } 176 177 s, _ := loadString(t, ing, "did:plc:akshay", "rk2") 178 if s.Edited != nil { 179 t.Errorf("edited = %v, want nil when no field changed", s.Edited) 180 } 181} 182 183func TestIngestString_DeleteRemovesRow(t *testing.T) { 184 ing := newStringIngester(t) 185 rec := tangled.String{ 186 Filename: ptr("hello.txt"), 187 Contents: ptr("x"), 188 CreatedAt: time.Now().UTC().Format(time.RFC3339), 189 } 190 if err := ing.ingestString(t.Context(), makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:boltless", "rk1", rec), ing.Logger); err != nil { 191 t.Fatalf("create: %v", err) 192 } 193 194 del := &jmodels.Event{ 195 Did: "did:plc:boltless", 196 Kind: jmodels.EventKindCommit, 197 Commit: &jmodels.Commit{ 198 Operation: jmodels.CommitOperationDelete, 199 Collection: tangled.StringNSID, 200 RKey: "rk1", 201 CID: makeCID(t, &rec), 202 }, 203 } 204 if err := ing.ingestString(t.Context(), del, ing.Logger); err != nil { 205 t.Fatalf("delete: %v", err) 206 } 207 208 if _, ok := loadString(t, ing, "did:plc:boltless", "rk1"); ok { 209 t.Fatal("row still present after delete") 210 } 211} 212 213func TestIngestString_ValidatorRejects(t *testing.T) { 214 ing := newStringIngester(t) 215 now := time.Now().UTC().Format(time.RFC3339) 216 217 cases := []struct { 218 name string 219 rec tangled.String 220 }{ 221 {"long filename", tangled.String{Filename: ptr(strings.Repeat("a", 141)), Contents: ptr("x"), CreatedAt: now}}, 222 {"long description", tangled.String{Filename: ptr("x"), Description: ptr(strings.Repeat("d", 281)), Contents: ptr("x"), CreatedAt: now}}, 223 } 224 225 for _, tc := range cases { 226 t.Run(tc.name, func(t *testing.T) { 227 e := makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:akshay", "bad", tc.rec) 228 if err := ing.ingestString(t.Context(), e, ing.Logger); err == nil { 229 t.Fatal("expected validator error, got nil") 230 } 231 if _, ok := loadString(t, ing, "did:plc:akshay", "bad"); ok { 232 t.Fatal("row inserted despite validator failure") 233 } 234 }) 235 } 236} 237 238func TestIngestString_ColdReplayPreservesCreated(t *testing.T) { 239 created := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC) 240 rec := tangled.String{ 241 Filename: ptr("snippet.go"), 242 Description: ptr("first cut"), 243 Contents: ptr("package main\n"), 244 CreatedAt: created.Format(time.RFC3339), 245 } 246 event := makeStringEvent(t, jmodels.CommitOperationCreate, "did:plc:boltless", "rkcold", rec) 247 248 first := newStringIngester(t) 249 if err := first.ingestString(t.Context(), event, first.Logger); err != nil { 250 t.Fatalf("first ingest: %v", err) 251 } 252 live, _ := loadString(t, first, "did:plc:boltless", "rkcold") 253 254 second := newStringIngester(t) 255 if err := second.ingestString(t.Context(), event, second.Logger); err != nil { 256 t.Fatalf("replay ingest: %v", err) 257 } 258 replayed, _ := loadString(t, second, "did:plc:boltless", "rkcold") 259 260 if !live.Created.Equal(replayed.Created) { 261 t.Fatalf("cold replay drifted: live=%v replayed=%v", live.Created, replayed.Created) 262 } 263 if !replayed.Created.Equal(created) { 264 t.Fatalf("replay lost record CreatedAt: got %v, want %v", replayed.Created, created) 265 } 266}