v3.0.0-alpha.1 6fdddb8

blob_test.go RawHistoryFolder

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("myapp", 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("myapp", 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("myapp", "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(repoName, 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("myapp", 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("myapp", 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("myapp", 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("myapp", 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("myapp", uuid, testDigest1)
720	assert.Error(t, err)
721	assert.Contains(t, err.Error(), "digest verification failed")
722
723	err = storage.FinishBlobUpload("myapp", 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("myapp", 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 TestBlobStorage_UploadBoundToRepository(t *testing.T) {
764	storage, err := NewBlobStorage(t.TempDir(), testLogger())
765	require.NoError(t, err)
766
767	uuid, err := storage.StartBlobUpload("owner")
768	require.NoError(t, err)
769
770	// A known upload UUID is not a bearer token for another repository.
771	data := []byte("foreign attempt")
772	_, err = storage.AppendBlobChunk("intruder", uuid, bytes.NewReader(data), -1, 0)
773	assert.ErrorIs(t, err, domain.ErrUploadNotFound)
774
775	err = storage.FinishBlobUpload("intruder", uuid, digestForData(data))
776	assert.ErrorIs(t, err, domain.ErrUploadNotFound)
777
778	err = storage.CancelBlobUpload("intruder", uuid)
779	assert.ErrorIs(t, err, domain.ErrUploadNotFound)
780
781	// The owner can still complete the upload.
782	_, err = storage.AppendBlobChunk("owner", uuid, bytes.NewReader(data), -1, 0)
783	require.NoError(t, err)
784	require.NoError(t, storage.FinishBlobUpload("owner", uuid, digestForData(data)))
785}
786
787func TestBlobStorage_BlobOwnershipPersistsAcrossRestart(t *testing.T) {
788	dir := t.TempDir()
789	storage, err := NewBlobStorage(dir, testLogger())
790	require.NoError(t, err)
791
792	uuid, err := storage.StartBlobUpload("myapp")
793	require.NoError(t, err)
794	data := []byte("owned content")
795	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(data), -1, 0)
796	require.NoError(t, err)
797	digest := digestForData(data)
798	require.NoError(t, storage.FinishBlobUpload("myapp", uuid, digest))
799
800	owned, err := storage.BlobOwnedByRepository("myapp", digest)
801	require.NoError(t, err)
802	assert.True(t, owned)
803
804	foreign, err := storage.BlobOwnedByRepository("other", digest)
805	require.NoError(t, err)
806	assert.False(t, foreign)
807
808	// Ownership survives a restart of the storage adapter.
809	reopened, err := NewBlobStorage(dir, testLogger())
810	require.NoError(t, err)
811	owned, err = reopened.BlobOwnedByRepository("myapp", digest)
812	require.NoError(t, err)
813	assert.True(t, owned)
814}
815
816func TestBlobStorage_FailedFinishDoesNotRecordOwnership(t *testing.T) {
817	storage, err := NewBlobStorage(t.TempDir(), testLogger())
818	require.NoError(t, err)
819
820	uuid, err := storage.StartBlobUpload("myapp")
821	require.NoError(t, err)
822	data := []byte("mismatched content")
823	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(data), -1, 0)
824	require.NoError(t, err)
825
826	require.Error(t, storage.FinishBlobUpload("myapp", uuid, testDigest1))
827	owned, err := storage.BlobOwnedByRepository("myapp", testDigest1)
828	require.NoError(t, err)
829	assert.False(t, owned)
830}
831
832func digestForData(data []byte) string {
833	sum := sha256.Sum256(data)
834	return "sha256:" + hex.EncodeToString(sum[:])
835}
836
837func appendRepeated(initialData, chunk []byte, repetitions int) []byte {
838	data := append([]byte{}, initialData...)
839	for range repetitions {
840		data = append(data, chunk...)
841	}
842	return data
843}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close