v2.31.0-dev8 ec8c5b9

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	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}

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