v2.16.0 ebd2ab3

blob_test.go RawHistoryFolder

1package filesystem
2
3import (
4	"bytes"
5	"io"
6	"os"
7	"path/filepath"
8	"testing"
9
10	"github.com/bnema/zerowrap"
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/require"
13)
14
15const (
16	testDigest1       = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
17	testDigest2       = "sha256:b3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d5"
18	testDigestSHA512  = "sha512:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
19	testDigestMissing = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
20)
21
22func testLogger() zerowrap.Logger {
23	return zerowrap.Default()
24}
25
26func TestNewBlobStorage(t *testing.T) {
27	tmpDir := t.TempDir()
28	log := testLogger()
29
30	storage, err := NewBlobStorage(tmpDir, log)
31
32	require.NoError(t, err)
33	assert.NotNil(t, storage)
34
35	// Verify directories were created
36	assert.DirExists(t, filepath.Join(tmpDir, "blobs"))
37	assert.DirExists(t, filepath.Join(tmpDir, "uploads"))
38}
39
40func TestNewBlobStorage_InvalidPath(t *testing.T) {
41	log := testLogger()
42
43	// Try to create in a path that can't be created (no permissions)
44	_, err := NewBlobStorage("/nonexistent/deeply/nested/path/that/should/fail", log)
45
46	assert.Error(t, err)
47}
48
49func TestBlobStorage_PutAndGetBlob(t *testing.T) {
50	tmpDir := t.TempDir()
51	log := testLogger()
52
53	storage, err := NewBlobStorage(tmpDir, log)
54	require.NoError(t, err)
55
56	blobData := []byte("test blob content")
57
58	// Put blob
59	err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
60	require.NoError(t, err)
61
62	// Get blob
63	reader, err := storage.GetBlob(testDigest1)
64	require.NoError(t, err)
65	defer reader.Close()
66
67	data, err := io.ReadAll(reader)
68	require.NoError(t, err)
69
70	assert.Equal(t, blobData, data)
71}
72
73func TestBlobStorage_GetBlob_NotFound(t *testing.T) {
74	tmpDir := t.TempDir()
75	log := testLogger()
76
77	storage, err := NewBlobStorage(tmpDir, log)
78	require.NoError(t, err)
79
80	reader, err := storage.GetBlob(testDigestMissing)
81
82	assert.Error(t, err)
83	assert.Nil(t, reader)
84	assert.Contains(t, err.Error(), "blob not found")
85}
86
87func TestBlobStorage_GetBlobPath(t *testing.T) {
88	tmpDir := t.TempDir()
89	log := testLogger()
90
91	storage, err := NewBlobStorage(tmpDir, log)
92	require.NoError(t, err)
93
94	blobData := []byte("test blob content")
95
96	// Put blob first
97	err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
98	require.NoError(t, err)
99
100	// Get blob path
101	path, err := storage.GetBlobPath(testDigest1)
102	require.NoError(t, err)
103	assert.NotEmpty(t, path)
104
105	// Verify file exists at path
106	_, err = os.Stat(path)
107	assert.NoError(t, err)
108}
109
110func TestBlobStorage_GetBlobPath_NotFound(t *testing.T) {
111	tmpDir := t.TempDir()
112	log := testLogger()
113
114	storage, err := NewBlobStorage(tmpDir, log)
115	require.NoError(t, err)
116
117	path, err := storage.GetBlobPath(testDigestMissing)
118
119	assert.Error(t, err)
120	assert.Empty(t, path)
121	assert.Contains(t, err.Error(), "blob not found")
122}
123
124func TestBlobStorage_PutBlob_SizeMismatch(t *testing.T) {
125	tmpDir := t.TempDir()
126	log := testLogger()
127
128	storage, err := NewBlobStorage(tmpDir, log)
129	require.NoError(t, err)
130
131	blobData := []byte("test blob content")
132
133	// Put blob with wrong size
134	err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), 999)
135
136	assert.Error(t, err)
137	assert.Contains(t, err.Error(), "size mismatch")
138}
139
140func TestBlobStorage_PutBlob_ZeroSize(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 size=0 (should skip size check)
150	err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), 0)
151
152	assert.NoError(t, err)
153
154	// Verify blob was stored
155	assert.True(t, storage.BlobExists(testDigest2))
156}
157
158func TestBlobStorage_DeleteBlob(t *testing.T) {
159	tmpDir := t.TempDir()
160	log := testLogger()
161
162	storage, err := NewBlobStorage(tmpDir, log)
163	require.NoError(t, err)
164
165	blobData := []byte("test blob content")
166
167	// Put blob
168	err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
169	require.NoError(t, err)
170
171	// Verify it exists
172	assert.True(t, storage.BlobExists(testDigest2))
173
174	// Delete blob
175	err = storage.DeleteBlob(testDigest2)
176	require.NoError(t, err)
177
178	// Verify it's gone
179	assert.False(t, storage.BlobExists(testDigest2))
180}
181
182func TestBlobStorage_DeleteBlob_NotFound(t *testing.T) {
183	tmpDir := t.TempDir()
184	log := testLogger()
185
186	storage, err := NewBlobStorage(tmpDir, log)
187	require.NoError(t, err)
188
189	err = storage.DeleteBlob(testDigestMissing)
190
191	assert.Error(t, err)
192	assert.Contains(t, err.Error(), "blob not found")
193}
194
195func TestBlobStorage_BlobExists(t *testing.T) {
196	tmpDir := t.TempDir()
197	log := testLogger()
198
199	storage, err := NewBlobStorage(tmpDir, log)
200	require.NoError(t, err)
201
202	// Should not exist initially
203	assert.False(t, storage.BlobExists(testDigest2))
204
205	// Put blob
206	blobData := []byte("test blob content")
207	err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
208	require.NoError(t, err)
209
210	// Should exist now
211	assert.True(t, storage.BlobExists(testDigest2))
212}
213
214func TestBlobStorage_ListBlobs(t *testing.T) {
215	tmpDir := t.TempDir()
216	log := testLogger()
217
218	storage, err := NewBlobStorage(tmpDir, log)
219	require.NoError(t, err)
220
221	blobData := []byte("test blob content")
222
223	err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
224	require.NoError(t, err)
225
226	err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
227	require.NoError(t, err)
228
229	digests, err := storage.ListBlobs()
230	require.NoError(t, err)
231
232	assert.ElementsMatch(t, []string{testDigest1, testDigest2}, digests)
233}
234
235func TestBlobStorage_ListBlobs_Empty(t *testing.T) {
236	tmpDir := t.TempDir()
237	log := testLogger()
238
239	storage, err := NewBlobStorage(tmpDir, log)
240	require.NoError(t, err)
241
242	digests, err := storage.ListBlobs()
243	require.NoError(t, err)
244	assert.Empty(t, digests)
245}
246
247func TestBlobStorage_StartBlobUpload(t *testing.T) {
248	tmpDir := t.TempDir()
249	log := testLogger()
250
251	storage, err := NewBlobStorage(tmpDir, log)
252	require.NoError(t, err)
253
254	uuid, err := storage.StartBlobUpload("myapp")
255
256	require.NoError(t, err)
257	assert.NotEmpty(t, uuid)
258	assert.Len(t, uuid, 36) // standard UUID format
259
260	// Verify upload file was created
261	uploadPath := filepath.Join(tmpDir, "uploads", uuid)
262	_, err = os.Stat(uploadPath)
263	assert.NoError(t, err)
264}
265
266func TestBlobStorage_AppendBlobChunk(t *testing.T) {
267	tmpDir := t.TempDir()
268	log := testLogger()
269
270	storage, err := NewBlobStorage(tmpDir, log)
271	require.NoError(t, err)
272
273	uuid, err := storage.StartBlobUpload("myapp")
274	require.NoError(t, err)
275
276	// Append first chunk
277	chunk1 := []byte("first chunk")
278	size1, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk1))
279	require.NoError(t, err)
280	assert.Equal(t, int64(len(chunk1)), size1)
281
282	// Append second chunk
283	chunk2 := []byte(" second chunk")
284	size2, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk2))
285	require.NoError(t, err)
286	assert.Equal(t, int64(len(chunk1)+len(chunk2)), size2)
287}
288
289func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) {
290	tmpDir := t.TempDir()
291	log := testLogger()
292
293	storage, err := NewBlobStorage(tmpDir, log)
294	require.NoError(t, err)
295
296	_, err = storage.AppendBlobChunk("myapp", "00000000-0000-4000-a000-000000000000", bytes.NewReader([]byte("data")))
297
298	assert.Error(t, err)
299	assert.Contains(t, err.Error(), "upload not found")
300}
301
302func TestBlobStorage_GetBlobUpload(t *testing.T) {
303	tmpDir := t.TempDir()
304	log := testLogger()
305
306	storage, err := NewBlobStorage(tmpDir, log)
307	require.NoError(t, err)
308
309	uuid, err := storage.StartBlobUpload("myapp")
310	require.NoError(t, err)
311
312	writer, err := storage.GetBlobUpload(uuid)
313	require.NoError(t, err)
314	defer writer.Close()
315
316	// Write some data
317	_, err = writer.Write([]byte("test data"))
318	assert.NoError(t, err)
319}
320
321func TestBlobStorage_GetBlobUpload_NotFound(t *testing.T) {
322	tmpDir := t.TempDir()
323	log := testLogger()
324
325	storage, err := NewBlobStorage(tmpDir, log)
326	require.NoError(t, err)
327
328	writer, err := storage.GetBlobUpload("00000000-0000-4000-a000-000000000000")
329
330	assert.Error(t, err)
331	assert.Nil(t, writer)
332	assert.Contains(t, err.Error(), "upload not found")
333}
334
335func TestBlobStorage_FinishBlobUpload(t *testing.T) {
336	tmpDir := t.TempDir()
337	log := testLogger()
338
339	storage, err := NewBlobStorage(tmpDir, log)
340	require.NoError(t, err)
341
342	// Start upload
343	uuid, err := storage.StartBlobUpload("myapp")
344	require.NoError(t, err)
345
346	// Append data - content that hashes to testDigest1
347	blobData := []byte("complete blob content")
348	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData))
349	require.NoError(t, err)
350
351	// Finish upload - use testDigest1 (validation will pass, but hash won't match)
352	// This tests the flow, actual digest verification happens in verifyUploadDigest
353	err = storage.FinishBlobUpload(uuid, testDigest1)
354	// This will fail because content doesn't match digest
355	assert.Error(t, err)
356	assert.Contains(t, err.Error(), "digest")
357}
358
359func TestBlobStorage_CancelBlobUpload(t *testing.T) {
360	tmpDir := t.TempDir()
361	log := testLogger()
362
363	storage, err := NewBlobStorage(tmpDir, log)
364	require.NoError(t, err)
365
366	// Start upload
367	uuid, err := storage.StartBlobUpload("myapp")
368	require.NoError(t, err)
369
370	// Verify upload file exists
371	uploadPath := filepath.Join(tmpDir, "uploads", uuid)
372	_, err = os.Stat(uploadPath)
373	require.NoError(t, err)
374
375	// Cancel upload
376	err = storage.CancelBlobUpload(uuid)
377	require.NoError(t, err)
378
379	// Verify upload file is gone
380	_, err = os.Stat(uploadPath)
381	assert.True(t, os.IsNotExist(err))
382}
383
384func TestBlobStorage_CancelBlobUpload_NotFound(t *testing.T) {
385	tmpDir := t.TempDir()
386	log := testLogger()
387
388	storage, err := NewBlobStorage(tmpDir, log)
389	require.NoError(t, err)
390
391	err = storage.CancelBlobUpload("00000000-0000-4000-a000-000000000000")
392
393	assert.Error(t, err)
394	assert.Contains(t, err.Error(), "upload not found")
395}
396
397func TestBlobStorage_BlobPathStructure(t *testing.T) {
398	tmpDir := t.TempDir()
399	log := testLogger()
400
401	storage, err := NewBlobStorage(tmpDir, log)
402	require.NoError(t, err)
403
404	// Test that blobs are stored with proper directory structure
405	blobData := []byte("test blob content")
406
407	err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
408	require.NoError(t, err)
409
410	// Check the path structure: blobs/sha256/a3/a3ed95caeb02...
411	// First 2 chars of hash after sha256: are "a3"
412	expectedPath := filepath.Join(tmpDir, "blobs", "sha256", "a3", "a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
413	_, err = os.Stat(expectedPath)
414	assert.NoError(t, err)
415}
416
417func TestBlobStorage_DigestFormats(t *testing.T) {
418	tmpDir := t.TempDir()
419	log := testLogger()
420
421	storage, err := NewBlobStorage(tmpDir, log)
422	require.NoError(t, err)
423
424	tests := []struct {
425		name   string
426		digest string
427	}{
428		{"sha256 standard", testDigest1},
429		{"sha512", testDigestSHA512},
430	}
431
432	for _, tt := range tests {
433		t.Run(tt.name, func(t *testing.T) {
434			blobData := []byte("test content for " + tt.digest)
435
436			err := storage.PutBlob(tt.digest, bytes.NewReader(blobData), int64(len(blobData)))
437			require.NoError(t, err)
438
439			assert.True(t, storage.BlobExists(tt.digest))
440
441			reader, err := storage.GetBlob(tt.digest)
442			require.NoError(t, err)
443			defer reader.Close()
444
445			data, err := io.ReadAll(reader)
446			require.NoError(t, err)
447			assert.Equal(t, blobData, data)
448		})
449	}
450}
451
452func TestBlobStorage_CompleteUploadFlow(t *testing.T) {
453	tmpDir := t.TempDir()
454	log := testLogger()
455
456	storage, err := NewBlobStorage(tmpDir, log)
457	require.NoError(t, err)
458
459	// Simulate a complete upload flow
460	repoName := "myorg/myapp"
461
462	// 1. Start upload
463	uuid, err := storage.StartBlobUpload(repoName)
464	require.NoError(t, err)
465
466	// 2. Send multiple chunks
467	chunks := [][]byte{
468		[]byte("first chunk of data"),
469		[]byte(" - middle chunk"),
470		[]byte(" - final chunk"),
471	}
472
473	var totalSize int64
474	for _, chunk := range chunks {
475		size, err := storage.AppendBlobChunk(repoName, uuid, bytes.NewReader(chunk))
476		require.NoError(t, err)
477		totalSize = size
478	}
479
480	expectedTotal := int64(len(chunks[0]) + len(chunks[1]) + len(chunks[2]))
481	assert.Equal(t, expectedTotal, totalSize)
482
483	// 3. Cancel upload (since we can't easily compute the real digest)
484	err = storage.CancelBlobUpload(uuid)
485	require.NoError(t, err)
486}

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