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