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 size, err := storage.DeleteBlob(testDigest2)
179 require.NoError(t, err)
180 assert.Equal(t, int64(len(blobData)), size)
181
182 // Verify it's gone
183 assert.False(t, storage.BlobExists(testDigest2))
184}
185
186func TestBlobStorage_DeleteBlob_NotFound(t *testing.T) {
187 tmpDir := t.TempDir()
188 log := testLogger()
189
190 storage, err := NewBlobStorage(tmpDir, log)
191 require.NoError(t, err)
192
193 size, err := storage.DeleteBlob(testDigestMissing)
194
195 assert.Error(t, err)
196 assert.Equal(t, int64(0), size)
197 assert.Contains(t, err.Error(), "blob not found")
198}
199
200func TestBlobStorage_BlobExists(t *testing.T) {
201 tmpDir := t.TempDir()
202 log := testLogger()
203
204 storage, err := NewBlobStorage(tmpDir, log)
205 require.NoError(t, err)
206
207 // Should not exist initially
208 assert.False(t, storage.BlobExists(testDigest2))
209
210 // Put blob
211 blobData := []byte("test blob content")
212 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
213 require.NoError(t, err)
214
215 // Should exist now
216 assert.True(t, storage.BlobExists(testDigest2))
217}
218
219func TestBlobStorage_ListBlobs(t *testing.T) {
220 tmpDir := t.TempDir()
221 log := testLogger()
222
223 storage, err := NewBlobStorage(tmpDir, log)
224 require.NoError(t, err)
225
226 blobData := []byte("test blob content")
227
228 err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
229 require.NoError(t, err)
230
231 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
232 require.NoError(t, err)
233
234 digests, err := storage.ListBlobs()
235 require.NoError(t, err)
236
237 assert.ElementsMatch(t, []string{testDigest1, testDigest2}, digests)
238}
239
240func TestBlobStorage_ListBlobs_Empty(t *testing.T) {
241 tmpDir := t.TempDir()
242 log := testLogger()
243
244 storage, err := NewBlobStorage(tmpDir, log)
245 require.NoError(t, err)
246
247 digests, err := storage.ListBlobs()
248 require.NoError(t, err)
249 assert.Empty(t, digests)
250}
251
252func TestBlobStorage_StartBlobUpload(t *testing.T) {
253 tmpDir := t.TempDir()
254 log := testLogger()
255
256 storage, err := NewBlobStorage(tmpDir, log)
257 require.NoError(t, err)
258
259 uuid, err := storage.StartBlobUpload("myapp")
260
261 require.NoError(t, err)
262 assert.NotEmpty(t, uuid)
263 assert.Len(t, uuid, 36) // standard UUID format
264
265 // Verify upload file was created
266 uploadPath := filepath.Join(tmpDir, "uploads", uuid)
267 _, err = os.Stat(uploadPath)
268 assert.NoError(t, err)
269}
270
271func TestBlobStorage_AppendBlobChunk(t *testing.T) {
272 tmpDir := t.TempDir()
273 log := testLogger()
274
275 storage, err := NewBlobStorage(tmpDir, log)
276 require.NoError(t, err)
277
278 uuid, err := storage.StartBlobUpload("myapp")
279 require.NoError(t, err)
280
281 // Append first chunk
282 chunk1 := []byte("first chunk")
283 size1, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk1))
284 require.NoError(t, err)
285 assert.Equal(t, int64(len(chunk1)), size1)
286
287 // Append second chunk
288 chunk2 := []byte(" second chunk")
289 size2, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk2))
290 require.NoError(t, err)
291 assert.Equal(t, int64(len(chunk1)+len(chunk2)), size2)
292}
293
294func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) {
295 tmpDir := t.TempDir()
296 log := testLogger()
297
298 storage, err := NewBlobStorage(tmpDir, log)
299 require.NoError(t, err)
300
301 _, err = storage.AppendBlobChunk("myapp", "00000000-0000-4000-a000-000000000000", bytes.NewReader([]byte("data")))
302
303 assert.Error(t, err)
304 assert.Contains(t, err.Error(), "upload not found")
305}
306
307func TestBlobStorage_GetBlobUpload(t *testing.T) {
308 tmpDir := t.TempDir()
309 log := testLogger()
310
311 storage, err := NewBlobStorage(tmpDir, log)
312 require.NoError(t, err)
313
314 uuid, err := storage.StartBlobUpload("myapp")
315 require.NoError(t, err)
316
317 writer, err := storage.GetBlobUpload(uuid)
318 require.NoError(t, err)
319 defer writer.Close()
320
321 // Write some data
322 _, err = writer.Write([]byte("test data"))
323 assert.NoError(t, err)
324}
325
326func TestBlobStorage_GetBlobUpload_NotFound(t *testing.T) {
327 tmpDir := t.TempDir()
328 log := testLogger()
329
330 storage, err := NewBlobStorage(tmpDir, log)
331 require.NoError(t, err)
332
333 writer, err := storage.GetBlobUpload("00000000-0000-4000-a000-000000000000")
334
335 assert.Error(t, err)
336 assert.Nil(t, writer)
337 assert.Contains(t, err.Error(), "upload not found")
338}
339
340func TestBlobStorage_FinishBlobUpload(t *testing.T) {
341 tmpDir := t.TempDir()
342 log := testLogger()
343
344 storage, err := NewBlobStorage(tmpDir, log)
345 require.NoError(t, err)
346
347 // Start upload
348 uuid, err := storage.StartBlobUpload("myapp")
349 require.NoError(t, err)
350
351 // Append data - content that hashes to testDigest1
352 blobData := []byte("complete blob content")
353 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData))
354 require.NoError(t, err)
355
356 // Finish upload - use testDigest1 (validation will pass, but hash won't match)
357 // This tests the flow, actual digest verification happens in verifyUploadDigest
358 err = storage.FinishBlobUpload(uuid, testDigest1)
359 // This will fail because content doesn't match digest
360 assert.Error(t, err)
361 assert.Contains(t, err.Error(), "digest")
362}
363
364func TestBlobStorage_CancelBlobUpload(t *testing.T) {
365 tmpDir := t.TempDir()
366 log := testLogger()
367
368 storage, err := NewBlobStorage(tmpDir, log)
369 require.NoError(t, err)
370
371 // Start upload
372 uuid, err := storage.StartBlobUpload("myapp")
373 require.NoError(t, err)
374
375 // Verify upload file exists
376 uploadPath := filepath.Join(tmpDir, "uploads", uuid)
377 _, err = os.Stat(uploadPath)
378 require.NoError(t, err)
379
380 // Cancel upload
381 err = storage.CancelBlobUpload(uuid)
382 require.NoError(t, err)
383
384 // Verify upload file is gone
385 _, err = os.Stat(uploadPath)
386 assert.True(t, os.IsNotExist(err))
387}
388
389func TestBlobStorage_CancelBlobUpload_NotFound(t *testing.T) {
390 tmpDir := t.TempDir()
391 log := testLogger()
392
393 storage, err := NewBlobStorage(tmpDir, log)
394 require.NoError(t, err)
395
396 err = storage.CancelBlobUpload("00000000-0000-4000-a000-000000000000")
397
398 assert.Error(t, err)
399 assert.Contains(t, err.Error(), "upload not found")
400}
401
402func TestBlobStorage_BlobPathStructure(t *testing.T) {
403 tmpDir := t.TempDir()
404 log := testLogger()
405
406 storage, err := NewBlobStorage(tmpDir, log)
407 require.NoError(t, err)
408
409 // Test that blobs are stored with proper directory structure
410 blobData := []byte("test blob content")
411
412 err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
413 require.NoError(t, err)
414
415 // Check the path structure: blobs/sha256/a3/a3ed95caeb02...
416 // First 2 chars of hash after sha256: are "a3"
417 expectedPath := filepath.Join(tmpDir, "blobs", "sha256", "a3", "a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
418 _, err = os.Stat(expectedPath)
419 assert.NoError(t, err)
420}
421
422func TestBlobStorage_DigestFormats(t *testing.T) {
423 tmpDir := t.TempDir()
424 log := testLogger()
425
426 storage, err := NewBlobStorage(tmpDir, log)
427 require.NoError(t, err)
428
429 tests := []struct {
430 name string
431 digest string
432 }{
433 {"sha256 standard", testDigest1},
434 {"sha512", testDigestSHA512},
435 }
436
437 for _, tt := range tests {
438 t.Run(tt.name, func(t *testing.T) {
439 blobData := []byte("test content for " + tt.digest)
440
441 err := storage.PutBlob(tt.digest, bytes.NewReader(blobData), int64(len(blobData)))
442 require.NoError(t, err)
443
444 assert.True(t, storage.BlobExists(tt.digest))
445
446 reader, err := storage.GetBlob(tt.digest)
447 require.NoError(t, err)
448 defer reader.Close()
449
450 data, err := io.ReadAll(reader)
451 require.NoError(t, err)
452 assert.Equal(t, blobData, data)
453 })
454 }
455}
456
457func TestBlobStorage_CompleteUploadFlow(t *testing.T) {
458 tmpDir := t.TempDir()
459 log := testLogger()
460
461 storage, err := NewBlobStorage(tmpDir, log)
462 require.NoError(t, err)
463
464 // Simulate a complete upload flow
465 repoName := "myorg/myapp"
466
467 // 1. Start upload
468 uuid, err := storage.StartBlobUpload(repoName)
469 require.NoError(t, err)
470
471 // 2. Send multiple chunks
472 chunks := [][]byte{
473 []byte("first chunk of data"),
474 []byte(" - middle chunk"),
475 []byte(" - final chunk"),
476 }
477
478 var totalSize int64
479 for _, chunk := range chunks {
480 size, err := storage.AppendBlobChunk(repoName, uuid, bytes.NewReader(chunk))
481 require.NoError(t, err)
482 totalSize = size
483 }
484
485 expectedTotal := int64(len(chunks[0]) + len(chunks[1]) + len(chunks[2]))
486 assert.Equal(t, expectedTotal, totalSize)
487
488 // 3. Cancel upload (since we can't easily compute the real digest)
489 err = storage.CancelBlobUpload(uuid)
490 require.NoError(t, err)
491}
492
493func TestBlobStorage_ConcurrentAppendAndFinalize(t *testing.T) {
494 tmpDir := t.TempDir()
495 log := testLogger()
496
497 storage, err := NewBlobStorage(tmpDir, log)
498 require.NoError(t, err)
499
500 uuid, err := storage.StartBlobUpload("myapp")
501 require.NoError(t, err)
502
503 initialData := []byte("initial-")
504 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(initialData))
505 require.NoError(t, err)
506
507 appendChunk := []byte("chunk")
508 appenders := 8
509 var wg sync.WaitGroup
510 appendReady := make(chan struct{})
511 finalizeErrs := make(chan error, 1)
512 appendErrs := make(chan error, appenders)
513
514 for range appenders {
515 wg.Add(1)
516 go func() {
517 defer wg.Done()
518 <-appendReady
519 _, appendErr := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(appendChunk))
520 appendErrs <- appendErr
521 }()
522 }
523
524 close(appendReady)
525 fullyAppended := appendRepeated(initialData, appendChunk, appenders)
526
527 wg.Add(1)
528 go func() {
529 defer wg.Done()
530 finalizeErrs <- storage.FinishBlobUpload(uuid, digestForData(fullyAppended))
531 }()
532
533 wg.Wait()
534 close(appendErrs)
535
536 finalizeErr := <-finalizeErrs
537 if finalizeErr == nil {
538 for appendErr := range appendErrs {
539 require.NoError(t, appendErr)
540 }
541
542 reader, err := storage.GetBlob(digestForData(fullyAppended))
543 require.NoError(t, err)
544 defer reader.Close()
545
546 blobData, err := io.ReadAll(reader)
547 require.NoError(t, err)
548 assert.Equal(t, fullyAppended, blobData)
549 } else {
550 assert.Contains(t, finalizeErr.Error(), "digest verification failed")
551
552 appendCount := 0
553 for appendErr := range appendErrs {
554 require.NoError(t, appendErr)
555 appendCount++
556 }
557
558 require.Equal(t, appenders, appendCount)
559
560 retryDigest := digestForData(fullyAppended)
561 err = storage.FinishBlobUpload(uuid, retryDigest)
562 require.NoError(t, err)
563
564 reader, err := storage.GetBlob(retryDigest)
565 require.NoError(t, err)
566 defer reader.Close()
567
568 blobData, err := io.ReadAll(reader)
569 require.NoError(t, err)
570 assert.Equal(t, fullyAppended, blobData)
571 }
572
573 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("after")))
574 assert.Error(t, err)
575 assert.Contains(t, err.Error(), "upload not found")
576}
577
578func TestBlobStorage_AppendAfterFinalize(t *testing.T) {
579 tmpDir := t.TempDir()
580 log := testLogger()
581
582 storage, err := NewBlobStorage(tmpDir, log)
583 require.NoError(t, err)
584
585 uuid, err := storage.StartBlobUpload("myapp")
586 require.NoError(t, err)
587
588 blobData := []byte("finalized content")
589 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData))
590 require.NoError(t, err)
591
592 err = storage.FinishBlobUpload(uuid, digestForData(blobData))
593 require.NoError(t, err)
594
595 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("more")))
596 assert.Error(t, err)
597 assert.Contains(t, err.Error(), "upload not found")
598}
599
600func TestBlobStorage_FailedFinalizeRetryable(t *testing.T) {
601 tmpDir := t.TempDir()
602 log := testLogger()
603
604 storage, err := NewBlobStorage(tmpDir, log)
605 require.NoError(t, err)
606
607 uuid, err := storage.StartBlobUpload("myapp")
608 require.NoError(t, err)
609
610 blobData := []byte("retryable finalize")
611 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData))
612 require.NoError(t, err)
613
614 err = storage.FinishBlobUpload(uuid, testDigest1)
615 assert.Error(t, err)
616 assert.Contains(t, err.Error(), "digest verification failed")
617
618 err = storage.FinishBlobUpload(uuid, digestForData(blobData))
619 require.NoError(t, err)
620}
621
622func TestBlobStorage_SequentialFlow(t *testing.T) {
623 tmpDir := t.TempDir()
624 log := testLogger()
625
626 storage, err := NewBlobStorage(tmpDir, log)
627 require.NoError(t, err)
628
629 uuid, err := storage.StartBlobUpload("myapp")
630 require.NoError(t, err)
631
632 chunks := [][]byte{
633 []byte("one-"),
634 []byte("two-"),
635 []byte("three"),
636 }
637
638 var blobData []byte
639 for _, chunk := range chunks {
640 blobData = append(blobData, chunk...)
641 _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk))
642 require.NoError(t, err)
643 }
644
645 digest := digestForData(blobData)
646 err = storage.FinishBlobUpload(uuid, digest)
647 require.NoError(t, err)
648
649 reader, err := storage.GetBlob(digest)
650 require.NoError(t, err)
651 defer reader.Close()
652
653 storedData, err := io.ReadAll(reader)
654 require.NoError(t, err)
655 assert.Equal(t, blobData, storedData)
656}
657
658func digestForData(data []byte) string {
659 sum := sha256.Sum256(data)
660 return "sha256:" + hex.EncodeToString(sum[:])
661}
662
663func appendRepeated(initialData, chunk []byte, repetitions int) []byte {
664 data := append([]byte{}, initialData...)
665 for range repetitions {
666 data = append(data, chunk...)
667 }
668 return data
669}