like malachite (atproto-lastfm-importer) but in go and bluer
go spotify tealfm lastfm atproto
at main 490 lines 12 kB view raw
1package cache 2 3import ( 4 "slices" 5 "testing" 6) 7 8func newTestStorage(t *testing.T) *BoltStorage { 9 t.Helper() 10 storage, err := NewBoltStorage(false) 11 if err != nil { 12 t.Fatalf("NewBoltStorage failed: %v", err) 13 } 14 t.Cleanup(func() { 15 storage.Close() 16 }) 17 return storage 18} 19 20// testDID generates a unique DID for each test to ensure test isolation. 21func testDID(t *testing.T, suffix string) string { 22 return "did:plc:test/" + t.Name() + "/" + suffix 23} 24 25func TestReadOnlyMode(t *testing.T) { 26 tests := []struct { 27 name string 28 readOnly bool 29 shouldWrite bool 30 expectError bool 31 }{ 32 { 33 name: "read-write mode allows writes", 34 readOnly: false, 35 shouldWrite: true, 36 expectError: false, 37 }, 38 { 39 name: "read-only mode prevents writes", 40 readOnly: true, 41 shouldWrite: true, 42 expectError: true, 43 }, 44 { 45 name: "read-only mode allows iteration", 46 readOnly: true, 47 shouldWrite: false, 48 expectError: false, 49 }, 50 } 51 52 for _, tt := range tests { 53 t.Run(tt.name, func(t *testing.T) { 54 storage, err := NewBoltStorage(tt.readOnly) 55 if err != nil { 56 t.Fatalf("NewBoltStorage failed: %v", err) 57 } 58 t.Cleanup(func() { 59 storage.Close() 60 }) 61 62 did := "did:plc:testro" + t.Name() 63 records := map[string][]byte{ 64 "key1": []byte(`{"trackName":"track1"}`), 65 } 66 67 if tt.shouldWrite { 68 err = storage.SaveRecords(did, records) 69 if tt.expectError && err == nil { 70 t.Error("expected error when writing to read-only storage") 71 } else if !tt.expectError && err != nil { 72 t.Errorf("unexpected error when writing: %v", err) 73 } 74 } else { 75 // Just test that iteration works on read-only storage 76 var count int 77 for range storage.IterateUnpublished(did, false) { 78 count++ 79 } 80 if count != 0 { 81 t.Errorf("expected 0 records, got %d", count) 82 } 83 } 84 }) 85 } 86} 87 88func TestSaveIterateRoundtrip(t *testing.T) { 89 storage := newTestStorage(t) 90 did := testDID(t, "roundtrip") 91 92 records := map[string][]byte{ 93 "key1": []byte(`{"trackName":"track1"}`), 94 "key2": []byte(`{"trackName":"track2"}`), 95 } 96 97 if err := storage.SaveRecords(did, records); err != nil { 98 t.Fatalf("SaveRecords failed: %v", err) 99 } 100 101 count := 0 102 for range storage.IterateUnpublished(did, false) { 103 count++ 104 } 105 if count != 2 { 106 t.Errorf("expected 2 records, got %d", count) 107 } 108} 109 110func TestMarkPublished(t *testing.T) { 111 storage := newTestStorage(t) 112 did := testDID(t, "mark") 113 114 records := map[string][]byte{ 115 "key1": []byte(`{"trackName":"track1"}`), 116 "key2": []byte(`{"trackName":"track2"}`), 117 } 118 storage.SaveRecords(did, records) 119 120 if err := storage.MarkPublished(did, "key1"); err != nil { 121 t.Fatalf("MarkPublished failed: %v", err) 122 } 123 124 count := 0 125 for range storage.IterateUnpublished(did, false) { 126 count++ 127 } 128 if count != 1 { 129 t.Errorf("expected 1 unpublished record, got %d", count) 130 } 131} 132 133func TestClear(t *testing.T) { 134 storage := newTestStorage(t) 135 did := testDID(t, "clear") 136 storage.SaveRecords(did, map[string][]byte{"key1": []byte(`{}`)}) 137 138 if !storage.IsValid(did) { 139 t.Error("cache should be valid") 140 } 141 142 storage.Clear(did) 143 144 if storage.IsValid(did) { 145 t.Error("cache should be invalid") 146 } 147} 148 149func TestIterateUnpublishedReverse(t *testing.T) { 150 tests := []struct { 151 name string 152 did string 153 records map[string][]byte 154 reverse bool 155 expectedKeys []string 156 }{ 157 { 158 name: "basic reverse iteration", 159 records: map[string][]byte{"aaa": []byte(`{"trackName":"a"}`), "bbb": []byte(`{"trackName":"b"}`), "ccc": []byte(`{"trackName":"c"}`)}, 160 reverse: true, 161 expectedKeys: []string{"ccc", "bbb", "aaa"}, 162 }, 163 { 164 name: "forward iteration", 165 records: map[string][]byte{"aaa": []byte(`{"trackName":"a"}`), "bbb": []byte(`{"trackName":"b"}`), "ccc": []byte(`{"trackName":"c"}`)}, 166 reverse: false, 167 expectedKeys: []string{"aaa", "bbb", "ccc"}, 168 }, 169 { 170 name: "single record reverse", 171 records: map[string][]byte{"only": []byte(`{"trackName":"one"}`)}, 172 reverse: true, 173 expectedKeys: []string{"only"}, 174 }, 175 } 176 177 for _, tt := range tests { 178 t.Run(tt.name, func(t *testing.T) { 179 storage := newTestStorage(t) 180 did := testDID(t, "unpublished") 181 182 if err := storage.SaveRecords(did, tt.records); err != nil { 183 t.Fatalf("SaveRecords failed: %v", err) 184 } 185 186 var keys []string 187 for key, rec := range storage.IterateUnpublished(did, tt.reverse) { 188 keys = append(keys, key) 189 _ = rec 190 } 191 192 if !slices.Equal(keys, tt.expectedKeys) { 193 t.Errorf("expected keys %v, got %v", tt.expectedKeys, keys) 194 } 195 }) 196 } 197} 198 199func TestIteratePublishedReverse(t *testing.T) { 200 tests := []struct { 201 name string 202 records map[string][]byte 203 published []string 204 reverse bool 205 expectedKeys []string 206 }{ 207 { 208 name: "basic published reverse", 209 records: map[string][]byte{"aaa": []byte(`{"trackName":"a"}`), "bbb": []byte(`{"trackName":"b"}`), "ccc": []byte(`{"trackName":"c"}`)}, 210 published: []string{"aaa", "ccc"}, reverse: true, expectedKeys: []string{"ccc", "aaa"}, 211 }, 212 { 213 name: "forward published", 214 records: map[string][]byte{"aaa": []byte(`{"trackName":"a"}`), "bbb": []byte(`{"trackName":"b"}`), "ccc": []byte(`{"trackName":"c"}`)}, 215 published: []string{"aaa", "ccc"}, reverse: false, expectedKeys: []string{"aaa", "ccc"}, 216 }, 217 } 218 219 for _, tt := range tests { 220 t.Run(tt.name, func(t *testing.T) { 221 storage := newTestStorage(t) 222 did := testDID(t, "published") 223 224 if err := storage.SaveRecords(did, tt.records); err != nil { 225 t.Fatalf("SaveRecords failed: %v", err) 226 } 227 228 if err := storage.MarkPublished(did, tt.published...); err != nil { 229 t.Fatalf("MarkPublished failed: %v", err) 230 } 231 232 var keys []string 233 for key, rec := range storage.IteratePublished(did, tt.reverse) { 234 keys = append(keys, key) 235 _ = rec 236 } 237 238 if !slices.Equal(keys, tt.expectedKeys) { 239 t.Errorf("expected keys %v, got %v", tt.expectedKeys, keys) 240 } 241 }) 242 } 243} 244 245func TestIterateReverseEmptyBucket(t *testing.T) { 246 tests := []struct { 247 name string 248 reverse bool 249 expectedLen int 250 }{ 251 { 252 name: "empty unpublished reverse", 253 reverse: true, 254 expectedLen: 0, 255 }, 256 { 257 name: "empty published reverse", 258 reverse: true, 259 expectedLen: 0, 260 }, 261 { 262 name: "empty unpublished forward", 263 reverse: false, 264 expectedLen: 0, 265 }, 266 } 267 268 for _, tt := range tests { 269 t.Run(tt.name, func(t *testing.T) { 270 storage := newTestStorage(t) 271 did := testDID(t, "empty") 272 273 count := 0 274 for range storage.IterateUnpublished(did, tt.reverse) { 275 count++ 276 } 277 278 if count != tt.expectedLen { 279 t.Errorf("expected %d records, got %d", tt.expectedLen, count) 280 } 281 }) 282 } 283} 284 285func TestIterateReverseWithEarlyExit(t *testing.T) { 286 tests := []struct { 287 name string 288 records map[string][]byte 289 reverse bool 290 breakAfter int 291 expectedKeys []string 292 }{ 293 { 294 name: "exit after first record reverse", 295 records: map[string][]byte{"aaa": []byte(`{"trackName":"a"}`), "bbb": []byte(`{"trackName":"b"}`), "ccc": []byte(`{"trackName":"c"}`)}, 296 reverse: true, 297 breakAfter: 1, 298 expectedKeys: []string{"ccc"}, 299 }, 300 { 301 name: "exit after two records reverse", 302 records: map[string][]byte{"aaa": []byte(`{"trackName":"a"}`), "bbb": []byte(`{"trackName":"b"}`), "ccc": []byte(`{"trackName":"c"}`)}, 303 reverse: true, 304 breakAfter: 2, 305 expectedKeys: []string{"ccc", "bbb"}, 306 }, 307 } 308 309 for _, tt := range tests { 310 t.Run(tt.name, func(t *testing.T) { 311 storage := newTestStorage(t) 312 did := testDID(t, "exit") 313 314 if err := storage.SaveRecords(did, tt.records); err != nil { 315 t.Fatalf("SaveRecords failed: %v", err) 316 } 317 318 var keys []string 319 for key, rec := range storage.IterateUnpublished(did, tt.reverse) { 320 keys = append(keys, key) 321 _ = rec 322 if len(keys) >= tt.breakAfter { 323 break 324 } 325 } 326 327 if !slices.Equal(keys, tt.expectedKeys) { 328 t.Errorf("expected keys %v, got %v", tt.expectedKeys, keys) 329 } 330 }) 331 } 332} 333 334func TestIterateFailed(t *testing.T) { 335 tests := []struct { 336 name string 337 setupStorage func(*BoltStorage, string) error 338 wantCount int 339 wantKeys []string 340 }{ 341 { 342 name: "returns failed records", 343 setupStorage: func(s *BoltStorage, did string) error { 344 records := map[string][]byte{ 345 "key1": []byte(`{"trackName":"a"}`), 346 "key2": []byte(`{"trackName":"b"}`), 347 "key3": []byte(`{"trackName":"c"}`), 348 } 349 if err := s.SaveRecords(did, records); err != nil { 350 return err 351 } 352 return s.MarkFailed(did, []string{"key1", "key3"}, "timeout error") 353 }, 354 wantCount: 2, 355 wantKeys: []string{"key1", "key3"}, 356 }, 357 { 358 name: "handles empty failed set", 359 setupStorage: func(s *BoltStorage, did string) error { 360 records := map[string][]byte{ 361 "key1": []byte(`{"trackName":"a"}`), 362 } 363 return s.SaveRecords(did, records) 364 }, 365 wantCount: 0, 366 wantKeys: nil, 367 }, 368 { 369 name: "handles non-existent DID", 370 setupStorage: func(s *BoltStorage, did string) error { 371 return nil 372 }, 373 wantCount: 0, 374 wantKeys: nil, 375 }, 376 { 377 name: "returns error messages", 378 setupStorage: func(s *BoltStorage, did string) error { 379 records := map[string][]byte{ 380 "key1": []byte(`{"trackName":"a"}`), 381 } 382 if err := s.SaveRecords(did, records); err != nil { 383 return err 384 } 385 return s.MarkFailed(did, []string{"key1"}, "custom error message") 386 }, 387 wantCount: 1, 388 wantKeys: []string{"key1"}, 389 }, 390 } 391 392 for _, tt := range tests { 393 t.Run(tt.name, func(t *testing.T) { 394 storage := newTestStorage(t) 395 did := testDID(t, "failed") 396 397 if err := tt.setupStorage(storage, did); err != nil { 398 t.Fatalf("setupStorage failed: %v", err) 399 } 400 401 var count int 402 var keys []string 403 iterateFailed := storage.IterateFailed(did) 404 iterateFailed(func(key string, rec []byte, errMsg string) bool { 405 count++ 406 keys = append(keys, key) 407 return true 408 }) 409 410 if count != tt.wantCount { 411 t.Errorf("IterateFailed() returned %d records, want %d", count, tt.wantCount) 412 } 413 414 if len(keys) != len(tt.wantKeys) { 415 t.Errorf("IterateFailed() returned %d keys, want %d", len(keys), len(tt.wantKeys)) 416 return 417 } 418 419 for i, key := range keys { 420 if key != tt.wantKeys[i] { 421 t.Errorf("IterateFailed() key[%d] = %s, want %s", i, key, tt.wantKeys[i]) 422 } 423 } 424 }) 425 } 426} 427 428func TestIterateFailedWithEarlyExit(t *testing.T) { 429 storage := newTestStorage(t) 430 did := testDID(t, "failed-exit") 431 432 records := map[string][]byte{ 433 "key1": []byte(`{"trackName":"a"}`), 434 "key2": []byte(`{"trackName":"b"}`), 435 "key3": []byte(`{"trackName":"c"}`), 436 } 437 if err := storage.SaveRecords(did, records); err != nil { 438 t.Fatalf("SaveRecords failed: %v", err) 439 } 440 if err := storage.MarkFailed(did, []string{"key1", "key2", "key3"}, "error"); err != nil { 441 t.Fatalf("MarkFailed failed: %v", err) 442 } 443 444 // Exit after 2 records 445 var count int 446 iterateFailed := storage.IterateFailed(did) 447 iterateFailed(func(key string, rec []byte, errMsg string) bool { 448 count++ 449 return count < 2 // Exit after 2 records 450 }) 451 452 if count != 2 { 453 t.Errorf("IterateFailed() returned %d records with early exit, want 2", count) 454 } 455} 456 457func TestIterateFailedPreservesRecordData(t *testing.T) { 458 storage := newTestStorage(t) 459 did := testDID(t, "failed-data") 460 461 records := map[string][]byte{ 462 "key1": []byte(`{"trackName":"Test Track","artist":"Test Artist"}`), 463 } 464 if err := storage.SaveRecords(did, records); err != nil { 465 t.Fatalf("SaveRecords failed: %v", err) 466 } 467 if err := storage.MarkFailed(did, []string{"key1"}, "network error"); err != nil { 468 t.Fatalf("MarkFailed failed: %v", err) 469 } 470 471 var gotRecord []byte 472 var gotErrMsg string 473 iterateFailed := storage.IterateFailed(did) 474 iterateFailed(func(key string, rec []byte, errMsg string) bool { 475 if key == "key1" { 476 gotRecord = rec 477 gotErrMsg = errMsg 478 } 479 return true 480 }) 481 482 expectedRecord := []byte(`{"trackName":"Test Track","artist":"Test Artist"}`) 483 if string(gotRecord) != string(expectedRecord) { 484 t.Errorf("IterateFailed() record = %s, want %s", string(gotRecord), string(expectedRecord)) 485 } 486 487 if gotErrMsg != "network error" { 488 t.Errorf("IterateFailed() errMsg = %s, want 'network error'", gotErrMsg) 489 } 490}