1package filesystem
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/hex"
7 "io"
8 "os"
9 "path/filepath"
10 "sync"
11 "testing"
12
13 "github.com/bnema/zerowrap"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16)
17
18const (
19 testDigest1 = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
20 testDigest2 = "sha256:b3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d5"
21 testDigestSHA512 = "sha512:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
22 testDigestMissing = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
23)
24
25func testLogger() zerowrap.Logger {
26 return zerowrap.Default()
27}
28
29func TestNewBlobStorage(t *testing.T) {
30 tmpDir := t.TempDir()
31 log := testLogger()
32
33 storage, err := NewBlobStorage(tmpDir, log)
34
35 require.NoError(t, err)
36 assert.NotNil(t, storage)
37
38 // Verify directories were created
39 assert.DirExists(t, filepath.Join(tmpDir, "blobs"))
40 assert.DirExists(t, filepath.Join(tmpDir, "uploads"))
41}
42
43func TestNewBlobStorage_InvalidPath(t *testing.T) {
44 log := testLogger()
45
46 // Try to create in a path that can't be created (no permissions)
47 _, err := NewBlobStorage("/nonexistent/deeply/nested/path/that/should/fail", log)
48
49 assert.Error(t, err)
50}
51
52func TestBlobStorage_PutAndGetBlob(t *testing.T) {
53 tmpDir := t.TempDir()
54 log := testLogger()
55
56 storage, err := NewBlobStorage(tmpDir, log)
57 require.NoError(t, err)
58
59 blobData := []byte("test blob content")
60
61 // Put blob
62 err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
63 require.NoError(t, err)
64
65 // Get blob
66 reader, err := storage.GetBlob(testDigest1)
67 require.NoError(t, err)
68 defer reader.Close()
69
70 data, err := io.ReadAll(reader)
71 require.NoError(t, err)
72
73 assert.Equal(t, blobData, data)
74}
75
76func TestBlobStorage_GetBlob_NotFound(t *testing.T) {
77 tmpDir := t.TempDir()
78 log := testLogger()
79
80 storage, err := NewBlobStorage(tmpDir, log)
81 require.NoError(t, err)
82
83 reader, err := storage.GetBlob(testDigestMissing)
84
85 assert.Error(t, err)
86 assert.Nil(t, reader)
87 assert.Contains(t, err.Error(), "blob not found")
88}
89
90func TestBlobStorage_GetBlobPath(t *testing.T) {
91 tmpDir := t.TempDir()
92 log := testLogger()
93
94 storage, err := NewBlobStorage(tmpDir, log)
95 require.NoError(t, err)
96
97 blobData := []byte("test blob content")
98
99 // Put blob first
100 err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
101 require.NoError(t, err)
102
103 // Get blob path
104 path, err := storage.GetBlobPath(testDigest1)
105 require.NoError(t, err)
106 assert.NotEmpty(t, path)
107
108 // Verify file exists at path
109 _, err = os.Stat(path)
110 assert.NoError(t, err)
111}
112
113func TestBlobStorage_GetBlobPath_NotFound(t *testing.T) {
114 tmpDir := t.TempDir()
115 log := testLogger()
116
117 storage, err := NewBlobStorage(tmpDir, log)
118 require.NoError(t, err)
119
120 path, err := storage.GetBlobPath(testDigestMissing)
121
122 assert.Error(t, err)
123 assert.Empty(t, path)
124 assert.Contains(t, err.Error(), "blob not found")
125}
126
127func TestBlobStorage_PutBlob_SizeMismatch(t *testing.T) {
128 tmpDir := t.TempDir()
129 log := testLogger()
130
131 storage, err := NewBlobStorage(tmpDir, log)
132 require.NoError(t, err)
133
134 blobData := []byte("test blob content")
135
136 // Put blob with wrong size
137 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), 999)
138
139 assert.Error(t, err)
140 assert.Contains(t, err.Error(), "size mismatch")
141}
142
143func TestBlobStorage_PutBlob_ZeroSize(t *testing.T) {
144 tmpDir := t.TempDir()
145 log := testLogger()
146
147 storage, err := NewBlobStorage(tmpDir, log)
148 require.NoError(t, err)
149
150 blobData := []byte("test blob content")
151
152 // Put blob with size=0 (should skip size check)
153 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), 0)
154
155 assert.NoError(t, err)
156
157 // Verify blob was stored
158 assert.True(t, storage.BlobExists(testDigest2))
159}
160
161func TestBlobStorage_DeleteBlob(t *testing.T) {
162 tmpDir := t.TempDir()
163 log := testLogger()
164
165 storage, err := NewBlobStorage(tmpDir, log)
166 require.NoError(t, err)
167
168 blobData := []byte("test blob content")
169
170 // Put blob
171 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
172 require.NoError(t, err)
173
174 // Verify it exists
175 assert.True(t, storage.BlobExists(testDigest2))
176
177 // Delete blob
178 err = storage.DeleteBlob(testDigest2)
179 require.NoError(t, err)
180
181 // Verify it's gone
182 assert.False(t, storage.BlobExists(testDigest2))
183}
184
185func TestBlobStorage_DeleteBlob_NotFound(t *testing.T) {
186 tmpDir := t.TempDir()
187 log := testLogger()
188
189 storage, err := NewBlobStorage(tmpDir, log)
190 require.NoError(t, err)
191
192 err = storage.DeleteBlob(testDigestMissing)
193
194 assert.Error(t, err)
195 assert.Contains(t, err.Error(), "blob not found")
196}
197
198func TestBlobStorage_BlobExists(t *testing.T) {
199 tmpDir := t.TempDir()
200 log := testLogger()
201
202 storage, err := NewBlobStorage(tmpDir, log)
203 require.NoError(t, err)
204
205 // Should not exist initially
206 assert.False(t, storage.BlobExists(testDigest2))
207
208 // Put blob
209 blobData := []byte("test blob content")
210 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
211 require.NoError(t, err)
212
213 // Should exist now
214 assert.True(t, storage.BlobExists(testDigest2))
215}
216
217func TestBlobStorage_ListBlobs(t *testing.T) {
218 tmpDir := t.TempDir()
219 log := testLogger()
220
221 storage, err := NewBlobStorage(tmpDir, log)
222 require.NoError(t, err)
223
224 blobData := []byte("test blob content")
225
226 err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
227 require.NoError(t, err)
228
229 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
230 require.NoError(t, err)
231
232 digests, err := storage.ListBlobs()
233 require.NoError(t, err)
234
235 assert.ElementsMatch(t, []string{testDigest1, testDigest2}, digests)
236}
237
238func TestBlobStorage_ListBlobs_Empty(t *testing.T) {
239 tmpDir := t.TempDir()
240 log := testLogger()
241
242 storage, err := NewBlobStorage(tmpDir, log)
243 require.NoError(t, err)
244
245 digests, err := storage.ListBlobs()
246 require.NoError(t, err)
247 assert.Empty(t, digests)
248}
249
250func TestBlobStorage_StartBlobUpload(t *testing.T) {
251 tmpDir := t.TempDir()
252 log := testLogger()
253
254 storage, err := NewBlobStorage(tmpDir, log)
255 require.NoError(t, err)
256
257 uuid, err := storage.StartBlobUpload("myapp")
258
259 require.NoError(t, err)
260 assert.NotEmpty(t, uuid)
261 assert.Len(t, uuid, 36) // standard UUID format
262
263 // Verify upload file was created
264 uploadPath := filepath.Join(tmpDir, "uploads", uuid)
265 _, err = os.Stat(uploadPath)
266 assert.NoError(t, err)
267}
268
269func TestBlobStorage_AppendBlobChunk(t *testing.T) {
270 tmpDir := t.TempDir()
271 log := testLogger()
272
273 storage, err := NewBlobStorage(tmpDir, log)
274 require.NoError(t, err)
275
276 uuid, err := storage.StartBlobUpload("myapp")
277 require.NoError(t, err)
278
279 // Append first chunk
280 chunk1 := []byte("first chunk")
281 size1, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk1))
282 require.NoError(t, err)
283 assert.Equal(t, int64(len(chunk1)), size1)
284
285 // Append second chunk
286 chunk2 := []byte(" second chunk")
287 size2, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk2))
288 require.NoError(t, err)
289 assert.Equal(t, int64(len(chunk1)+len(chunk2)), size2)
290}
291
292func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) {
293 tmpDir := t.TempDir()
294 log := testLogger()
295
296 storage, err := NewBlobStorage(tmpDir, log)
297 require.NoError(t, err)
298
299 _, err = storage.AppendBlobChunk("myapp", "00000000-0000-4000-a000-000000000000", bytes.NewReader([]byte("data")))
300
301 assert.Error(t, err)
302 assert.Contains(t, err.Error(), "upload not found")
303}
304
305func TestBlobStorage_GetBlobUpload(t *testing.T) {
306 tmpDir := t.TempDir()
307 log := testLogger()
308
309 storage, err := NewBlobStorage(tmpDir, log)
310 require.NoError(t, err)
311
312 uuid, err := storage.StartBlobUpload("myapp")
313 require.NoError(t, err)
314
315 writer, err := storage.GetBlobUpload(uuid)
316 require.NoError(t, err)
317 defer writer.Close()
318
319 // Write some data
320 _, err = writer.Write([]byte("test data"))
321 assert.NoError(t, err)
322}
323
324func TestBlobStorage_GetBlobUpload_NotFound(t *testing.T) {
325 tmpDir := t.TempDir()
326 log := testLogger()
327
328 storage, err := NewBlobStorage(tmpDir, log)
329 require.NoError(t, err)
330
331 writer, err := storage.GetBlobUpload("00000000-0000-4000-a000-000000000000")
332
333 assert.Error(t, err)
334 assert.Nil(t, writer)
335 assert.Contains(t, err.Error(), "upload not found")
336}
337
338func TestBlobStorage_FinishBlobUpload(t *testing.T) {
339 tmpDir := t.TempDir()
340 log := testLogger()
341
342 storage, err := NewBlobStorage(tmpDir, log)
343 require.NoError(t, err)
344
345 // Start upload
346 uuid, err := storage.StartBlobUpload("myapp")
347 require.NoError(t, err)
348
349 // Append data - content that hashes to testDigest1
350 blobData := []byte("complete blob content")
351 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData))
352 require.NoError(t, err)
353
354 // Finish upload - use testDigest1 (validation will pass, but hash won't match)
355 // This tests the flow, actual digest verification happens in verifyUploadDigest
356 err = storage.FinishBlobUpload(uuid, testDigest1)
357 // This will fail because content doesn't match digest
358 assert.Error(t, err)
359 assert.Contains(t, err.Error(), "digest")
360}
361
362func TestBlobStorage_CancelBlobUpload(t *testing.T) {
363 tmpDir := t.TempDir()
364 log := testLogger()
365
366 storage, err := NewBlobStorage(tmpDir, log)
367 require.NoError(t, err)
368
369 // Start upload
370 uuid, err := storage.StartBlobUpload("myapp")
371 require.NoError(t, err)
372
373 // Verify upload file exists
374 uploadPath := filepath.Join(tmpDir, "uploads", uuid)
375 _, err = os.Stat(uploadPath)
376 require.NoError(t, err)
377
378 // Cancel upload
379 err = storage.CancelBlobUpload(uuid)
380 require.NoError(t, err)
381
382 // Verify upload file is gone
383 _, err = os.Stat(uploadPath)
384 assert.True(t, os.IsNotExist(err))
385}
386
387func TestBlobStorage_CancelBlobUpload_NotFound(t *testing.T) {
388 tmpDir := t.TempDir()
389 log := testLogger()
390
391 storage, err := NewBlobStorage(tmpDir, log)
392 require.NoError(t, err)
393
394 err = storage.CancelBlobUpload("00000000-0000-4000-a000-000000000000")
395
396 assert.Error(t, err)
397 assert.Contains(t, err.Error(), "upload not found")
398}
399
400func TestBlobStorage_BlobPathStructure(t *testing.T) {
401 tmpDir := t.TempDir()
402 log := testLogger()
403
404 storage, err := NewBlobStorage(tmpDir, log)
405 require.NoError(t, err)
406
407 // Test that blobs are stored with proper directory structure
408 blobData := []byte("test blob content")
409
410 err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
411 require.NoError(t, err)
412
413 // Check the path structure: blobs/sha256/a3/a3ed95caeb02...
414 // First 2 chars of hash after sha256: are "a3"
415 expectedPath := filepath.Join(tmpDir, "blobs", "sha256", "a3", "a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
416 _, err = os.Stat(expectedPath)
417 assert.NoError(t, err)
418}
419
420func TestBlobStorage_DigestFormats(t *testing.T) {
421 tmpDir := t.TempDir()
422 log := testLogger()
423
424 storage, err := NewBlobStorage(tmpDir, log)
425 require.NoError(t, err)
426
427 tests := []struct {
428 name string
429 digest string
430 }{
431 {"sha256 standard", testDigest1},
432 {"sha512", testDigestSHA512},
433 }
434
435 for _, tt := range tests {
436 t.Run(tt.name, func(t *testing.T) {
437 blobData := []byte("test content for " + tt.digest)
438
439 err := storage.PutBlob(tt.digest, bytes.NewReader(blobData), int64(len(blobData)))
440 require.NoError(t, err)
441
442 assert.True(t, storage.BlobExists(tt.digest))
443
444 reader, err := storage.GetBlob(tt.digest)
445 require.NoError(t, err)
446 defer reader.Close()
447
448 data, err := io.ReadAll(reader)
449 require.NoError(t, err)
450 assert.Equal(t, blobData, data)
451 })
452 }
453}
454
455func TestBlobStorage_CompleteUploadFlow(t *testing.T) {
456 tmpDir := t.TempDir()
457 log := testLogger()
458
459 storage, err := NewBlobStorage(tmpDir, log)
460 require.NoError(t, err)
461
462 // Simulate a complete upload flow
463 repoName := "myorg/myapp"
464
465 // 1. Start upload
466 uuid, err := storage.StartBlobUpload(repoName)
467 require.NoError(t, err)
468
469 // 2. Send multiple chunks
470 chunks := [][]byte{
471 []byte("first chunk of data"),
472 []byte(" - middle chunk"),
473 []byte(" - final chunk"),
474 }
475
476 var totalSize int64
477 for _, chunk := range chunks {
478 size, err := storage.AppendBlobChunk(repoName, uuid, bytes.NewReader(chunk))
479 require.NoError(t, err)
480 totalSize = size
481 }
482
483 expectedTotal := int64(len(chunks[0]) + len(chunks[1]) + len(chunks[2]))
484 assert.Equal(t, expectedTotal, totalSize)
485
486 // 3. Cancel upload (since we can't easily compute the real digest)
487 err = storage.CancelBlobUpload(uuid)
488 require.NoError(t, err)
489}
490
491func TestBlobStorage_ConcurrentAppendAndFinalize(t *testing.T) {
492 tmpDir := t.TempDir()
493 log := testLogger()
494
495 storage, err := NewBlobStorage(tmpDir, log)
496 require.NoError(t, err)
497
498 uuid, err := storage.StartBlobUpload("myapp")
499 require.NoError(t, err)
500
501 initialData := []byte("initial-")
502 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(initialData))
503 require.NoError(t, err)
504
505 appendChunk := []byte("chunk")
506 appenders := 8
507 var wg sync.WaitGroup
508 appendReady := make(chan struct{})
509 finalizeErrs := make(chan error, 1)
510 appendErrs := make(chan error, appenders)
511
512 for range appenders {
513 wg.Add(1)
514 go func() {
515 defer wg.Done()
516 <-appendReady
517 _, appendErr := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(appendChunk))
518 appendErrs <- appendErr
519 }()
520 }
521
522 close(appendReady)
523 fullyAppended := appendRepeated(initialData, appendChunk, appenders)
524
525 wg.Add(1)
526 go func() {
527 defer wg.Done()
528 finalizeErrs <- storage.FinishBlobUpload(uuid, digestForData(fullyAppended))
529 }()
530
531 wg.Wait()
532 close(appendErrs)
533
534 finalizeErr := <-finalizeErrs
535 if finalizeErr == nil {
536 for appendErr := range appendErrs {
537 require.NoError(t, appendErr)
538 }
539
540 reader, err := storage.GetBlob(digestForData(fullyAppended))
541 require.NoError(t, err)
542 defer reader.Close()
543
544 blobData, err := io.ReadAll(reader)
545 require.NoError(t, err)
546 assert.Equal(t, fullyAppended, blobData)
547 } else {
548 assert.Contains(t, finalizeErr.Error(), "digest verification failed")
549
550 appendCount := 0
551 for appendErr := range appendErrs {
552 require.NoError(t, appendErr)
553 appendCount++
554 }
555
556 require.Equal(t, appenders, appendCount)
557
558 retryDigest := digestForData(fullyAppended)
559 err = storage.FinishBlobUpload(uuid, retryDigest)
560 require.NoError(t, err)
561
562 reader, err := storage.GetBlob(retryDigest)
563 require.NoError(t, err)
564 defer reader.Close()
565
566 blobData, err := io.ReadAll(reader)
567 require.NoError(t, err)
568 assert.Equal(t, fullyAppended, blobData)
569 }
570
571 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("after")))
572 assert.Error(t, err)
573 assert.Contains(t, err.Error(), "upload not found")
574}
575
576func TestBlobStorage_AppendAfterFinalize(t *testing.T) {
577 tmpDir := t.TempDir()
578 log := testLogger()
579
580 storage, err := NewBlobStorage(tmpDir, log)
581 require.NoError(t, err)
582
583 uuid, err := storage.StartBlobUpload("myapp")
584 require.NoError(t, err)
585
586 blobData := []byte("finalized content")
587 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData))
588 require.NoError(t, err)
589
590 err = storage.FinishBlobUpload(uuid, digestForData(blobData))
591 require.NoError(t, err)
592
593 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("more")))
594 assert.Error(t, err)
595 assert.Contains(t, err.Error(), "upload not found")
596}
597
598func TestBlobStorage_FailedFinalizeRetryable(t *testing.T) {
599 tmpDir := t.TempDir()
600 log := testLogger()
601
602 storage, err := NewBlobStorage(tmpDir, log)
603 require.NoError(t, err)
604
605 uuid, err := storage.StartBlobUpload("myapp")
606 require.NoError(t, err)
607
608 blobData := []byte("retryable finalize")
609 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData))
610 require.NoError(t, err)
611
612 err = storage.FinishBlobUpload(uuid, testDigest1)
613 assert.Error(t, err)
614 assert.Contains(t, err.Error(), "digest verification failed")
615
616 err = storage.FinishBlobUpload(uuid, digestForData(blobData))
617 require.NoError(t, err)
618}
619
620func TestBlobStorage_SequentialFlow(t *testing.T) {
621 tmpDir := t.TempDir()
622 log := testLogger()
623
624 storage, err := NewBlobStorage(tmpDir, log)
625 require.NoError(t, err)
626
627 uuid, err := storage.StartBlobUpload("myapp")
628 require.NoError(t, err)
629
630 chunks := [][]byte{
631 []byte("one-"),
632 []byte("two-"),
633 []byte("three"),
634 }
635
636 var blobData []byte
637 for _, chunk := range chunks {
638 blobData = append(blobData, chunk...)
639 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk))
640 require.NoError(t, err)
641 }
642
643 digest := digestForData(blobData)
644 err = storage.FinishBlobUpload(uuid, digest)
645 require.NoError(t, err)
646
647 reader, err := storage.GetBlob(digest)
648 require.NoError(t, err)
649 defer reader.Close()
650
651 storedData, err := io.ReadAll(reader)
652 require.NoError(t, err)
653 assert.Equal(t, blobData, storedData)
654}
655
656func digestForData(data []byte) string {
657 sum := sha256.Sum256(data)
658 return "sha256:" + hex.EncodeToString(sum[:])
659}
660
661func appendRepeated(initialData, chunk []byte, repetitions int) []byte {
662 data := append([]byte{}, initialData...)
663 for range repetitions {
664 data = append(data, chunk...)
665 }
666 return data
667}