v2.30.0-pre2 f03ab25

blob_test.go RawHistoryFolder

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}

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