like malachite (atproto-lastfm-importer) but in go and bluer
go
spotify
tealfm
lastfm
atproto
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}