detached faad957

test(adapters/filesystem): add integration tests for blob and manifest storage

- Add 28 tests for blob storage (CRUD, upload flow, digest formats)
- Add 18 tests for manifest storage (CRUD, tags, repositories)
- Use t.TempDir() for real filesystem integration tests
commit
faad957be7a8e6aaf5a541b40b951bbf38f038ba
author
brice · · 2026-01-12 18:10 UTC
committer
brice · · 2026-01-12 17:24 UTC
parent
d98712e
signature
unsigned
2 files · +913 -0
A internal/adapters/out/filesystem/blob_test.go +473 -0
@@ -0,0 +1,473 @@
1+package filesystem
2+
3+import (
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+
15+func testLogger() zerowrap.Logger {
16+ return zerowrap.Default()
17+}
18+
19+func TestNewBlobStorage(t *testing.T) {
20+ tmpDir := t.TempDir()
21+ log := testLogger()
22+
23+ storage, err := NewBlobStorage(tmpDir, log)
24+
25+ require.NoError(t, err)
26+ assert.NotNil(t, storage)
27+
28+ // Verify directories were created
29+ assert.DirExists(t, filepath.Join(tmpDir, "blobs"))
30+ assert.DirExists(t, filepath.Join(tmpDir, "uploads"))
31+}
32+
33+func TestNewBlobStorage_InvalidPath(t *testing.T) {
34+ log := testLogger()
35+
36+ // Try to create in a path that can't be created (no permissions)
37+ _, err := NewBlobStorage("/nonexistent/deeply/nested/path/that/should/fail", log)
38+
39+ assert.Error(t, err)
40+}
41+
42+func TestBlobStorage_PutAndGetBlob(t *testing.T) {
43+ tmpDir := t.TempDir()
44+ log := testLogger()
45+
46+ storage, err := NewBlobStorage(tmpDir, log)
47+ require.NoError(t, err)
48+
49+ blobData := []byte("test blob content")
50+ digest := "sha256:abc123def456"
51+
52+ // Put blob
53+ err = storage.PutBlob(digest, bytes.NewReader(blobData), int64(len(blobData)))
54+ require.NoError(t, err)
55+
56+ // Get blob
57+ reader, err := storage.GetBlob(digest)
58+ require.NoError(t, err)
59+ defer reader.Close()
60+
61+ data, err := io.ReadAll(reader)
62+ require.NoError(t, err)
63+
64+ assert.Equal(t, blobData, data)
65+}
66+
67+func TestBlobStorage_GetBlob_NotFound(t *testing.T) {
68+ tmpDir := t.TempDir()
69+ log := testLogger()
70+
71+ storage, err := NewBlobStorage(tmpDir, log)
72+ require.NoError(t, err)
73+
74+ reader, err := storage.GetBlob("sha256:notexists")
75+
76+ assert.Error(t, err)
77+ assert.Nil(t, reader)
78+ assert.Contains(t, err.Error(), "blob not found")
79+}
80+
81+func TestBlobStorage_GetBlobPath(t *testing.T) {
82+ tmpDir := t.TempDir()
83+ log := testLogger()
84+
85+ storage, err := NewBlobStorage(tmpDir, log)
86+ require.NoError(t, err)
87+
88+ blobData := []byte("test blob content")
89+ digest := "sha256:abc123def456"
90+
91+ // Put blob first
92+ err = storage.PutBlob(digest, bytes.NewReader(blobData), int64(len(blobData)))
93+ require.NoError(t, err)
94+
95+ // Get blob path
96+ path, err := storage.GetBlobPath(digest)
97+ require.NoError(t, err)
98+ assert.NotEmpty(t, path)
99+
100+ // Verify file exists at path
101+ _, err = os.Stat(path)
102+ assert.NoError(t, err)
103+}
104+
105+func TestBlobStorage_GetBlobPath_NotFound(t *testing.T) {
106+ tmpDir := t.TempDir()
107+ log := testLogger()
108+
109+ storage, err := NewBlobStorage(tmpDir, log)
110+ require.NoError(t, err)
111+
112+ path, err := storage.GetBlobPath("sha256:notexists")
113+
114+ assert.Error(t, err)
115+ assert.Empty(t, path)
116+ assert.Contains(t, err.Error(), "blob not found")
117+}
118+
119+func TestBlobStorage_PutBlob_SizeMismatch(t *testing.T) {
120+ tmpDir := t.TempDir()
121+ log := testLogger()
122+
123+ storage, err := NewBlobStorage(tmpDir, log)
124+ require.NoError(t, err)
125+
126+ blobData := []byte("test blob content")
127+ digest := "sha256:abc123"
128+
129+ // Put blob with wrong size
130+ err = storage.PutBlob(digest, bytes.NewReader(blobData), 999)
131+
132+ assert.Error(t, err)
133+ assert.Contains(t, err.Error(), "size mismatch")
134+}
135+
136+func TestBlobStorage_PutBlob_ZeroSize(t *testing.T) {
137+ tmpDir := t.TempDir()
138+ log := testLogger()
139+
140+ storage, err := NewBlobStorage(tmpDir, log)
141+ require.NoError(t, err)
142+
143+ blobData := []byte("test blob content")
144+ digest := "sha256:abc123"
145+
146+ // Put blob with size=0 (should skip size check)
147+ err = storage.PutBlob(digest, bytes.NewReader(blobData), 0)
148+
149+ assert.NoError(t, err)
150+
151+ // Verify blob was stored
152+ assert.True(t, storage.BlobExists(digest))
153+}
154+
155+func TestBlobStorage_DeleteBlob(t *testing.T) {
156+ tmpDir := t.TempDir()
157+ log := testLogger()
158+
159+ storage, err := NewBlobStorage(tmpDir, log)
160+ require.NoError(t, err)
161+
162+ blobData := []byte("test blob content")
163+ digest := "sha256:abc123"
164+
165+ // Put blob
166+ err = storage.PutBlob(digest, bytes.NewReader(blobData), int64(len(blobData)))
167+ require.NoError(t, err)
168+
169+ // Verify it exists
170+ assert.True(t, storage.BlobExists(digest))
171+
172+ // Delete blob
173+ err = storage.DeleteBlob(digest)
174+ require.NoError(t, err)
175+
176+ // Verify it's gone
177+ assert.False(t, storage.BlobExists(digest))
178+}
179+
180+func TestBlobStorage_DeleteBlob_NotFound(t *testing.T) {
181+ tmpDir := t.TempDir()
182+ log := testLogger()
183+
184+ storage, err := NewBlobStorage(tmpDir, log)
185+ require.NoError(t, err)
186+
187+ err = storage.DeleteBlob("sha256:notexists")
188+
189+ assert.Error(t, err)
190+ assert.Contains(t, err.Error(), "blob not found")
191+}
192+
193+func TestBlobStorage_BlobExists(t *testing.T) {
194+ tmpDir := t.TempDir()
195+ log := testLogger()
196+
197+ storage, err := NewBlobStorage(tmpDir, log)
198+ require.NoError(t, err)
199+
200+ digest := "sha256:abc123"
201+
202+ // Should not exist initially
203+ assert.False(t, storage.BlobExists(digest))
204+
205+ // Put blob
206+ blobData := []byte("test blob content")
207+ err = storage.PutBlob(digest, bytes.NewReader(blobData), int64(len(blobData)))
208+ require.NoError(t, err)
209+
210+ // Should exist now
211+ assert.True(t, storage.BlobExists(digest))
212+}
213+
214+func TestBlobStorage_StartBlobUpload(t *testing.T) {
215+ tmpDir := t.TempDir()
216+ log := testLogger()
217+
218+ storage, err := NewBlobStorage(tmpDir, log)
219+ require.NoError(t, err)
220+
221+ uuid, err := storage.StartBlobUpload("myapp")
222+
223+ require.NoError(t, err)
224+ assert.NotEmpty(t, uuid)
225+ assert.Contains(t, uuid, "myapp")
226+
227+ // Verify upload file was created
228+ uploadPath := filepath.Join(tmpDir, "uploads", uuid)
229+ _, err = os.Stat(uploadPath)
230+ assert.NoError(t, err)
231+}
232+
233+func TestBlobStorage_AppendBlobChunk(t *testing.T) {
234+ tmpDir := t.TempDir()
235+ log := testLogger()
236+
237+ storage, err := NewBlobStorage(tmpDir, log)
238+ require.NoError(t, err)
239+
240+ uuid, err := storage.StartBlobUpload("myapp")
241+ require.NoError(t, err)
242+
243+ // Append first chunk
244+ chunk1 := []byte("first chunk")
245+ size1, err := storage.AppendBlobChunk("myapp", uuid, chunk1)
246+ require.NoError(t, err)
247+ assert.Equal(t, int64(len(chunk1)), size1)
248+
249+ // Append second chunk
250+ chunk2 := []byte(" second chunk")
251+ size2, err := storage.AppendBlobChunk("myapp", uuid, chunk2)
252+ require.NoError(t, err)
253+ assert.Equal(t, int64(len(chunk1)+len(chunk2)), size2)
254+}
255+
256+func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) {
257+ tmpDir := t.TempDir()
258+ log := testLogger()
259+
260+ storage, err := NewBlobStorage(tmpDir, log)
261+ require.NoError(t, err)
262+
263+ _, err = storage.AppendBlobChunk("myapp", "nonexistent-uuid", []byte("data"))
264+
265+ assert.Error(t, err)
266+ assert.Contains(t, err.Error(), "upload not found")
267+}
268+
269+func TestBlobStorage_GetBlobUpload(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+ writer, err := storage.GetBlobUpload(uuid)
280+ require.NoError(t, err)
281+ defer writer.Close()
282+
283+ // Write some data
284+ _, err = writer.Write([]byte("test data"))
285+ assert.NoError(t, err)
286+}
287+
288+func TestBlobStorage_GetBlobUpload_NotFound(t *testing.T) {
289+ tmpDir := t.TempDir()
290+ log := testLogger()
291+
292+ storage, err := NewBlobStorage(tmpDir, log)
293+ require.NoError(t, err)
294+
295+ writer, err := storage.GetBlobUpload("nonexistent-uuid")
296+
297+ assert.Error(t, err)
298+ assert.Nil(t, writer)
299+ assert.Contains(t, err.Error(), "upload not found")
300+}
301+
302+func TestBlobStorage_FinishBlobUpload(t *testing.T) {
303+ tmpDir := t.TempDir()
304+ log := testLogger()
305+
306+ storage, err := NewBlobStorage(tmpDir, log)
307+ require.NoError(t, err)
308+
309+ // Start upload
310+ uuid, err := storage.StartBlobUpload("myapp")
311+ require.NoError(t, err)
312+
313+ // Append data
314+ blobData := []byte("complete blob content")
315+ _, err = storage.AppendBlobChunk("myapp", uuid, blobData)
316+ require.NoError(t, err)
317+
318+ // Finish upload
319+ digest := "sha256:finished123"
320+ err = storage.FinishBlobUpload(uuid, digest)
321+ require.NoError(t, err)
322+
323+ // Verify blob exists at final location
324+ assert.True(t, storage.BlobExists(digest))
325+
326+ // Verify upload file is gone
327+ uploadPath := filepath.Join(tmpDir, "uploads", uuid)
328+ _, err = os.Stat(uploadPath)
329+ assert.True(t, os.IsNotExist(err))
330+}
331+
332+func TestBlobStorage_CancelBlobUpload(t *testing.T) {
333+ tmpDir := t.TempDir()
334+ log := testLogger()
335+
336+ storage, err := NewBlobStorage(tmpDir, log)
337+ require.NoError(t, err)
338+
339+ // Start upload
340+ uuid, err := storage.StartBlobUpload("myapp")
341+ require.NoError(t, err)
342+
343+ // Verify upload file exists
344+ uploadPath := filepath.Join(tmpDir, "uploads", uuid)
345+ _, err = os.Stat(uploadPath)
346+ require.NoError(t, err)
347+
348+ // Cancel upload
349+ err = storage.CancelBlobUpload(uuid)
350+ require.NoError(t, err)
351+
352+ // Verify upload file is gone
353+ _, err = os.Stat(uploadPath)
354+ assert.True(t, os.IsNotExist(err))
355+}
356+
357+func TestBlobStorage_CancelBlobUpload_NotFound(t *testing.T) {
358+ tmpDir := t.TempDir()
359+ log := testLogger()
360+
361+ storage, err := NewBlobStorage(tmpDir, log)
362+ require.NoError(t, err)
363+
364+ err = storage.CancelBlobUpload("nonexistent-uuid")
365+
366+ assert.Error(t, err)
367+ assert.Contains(t, err.Error(), "upload not found")
368+}
369+
370+func TestBlobStorage_BlobPathStructure(t *testing.T) {
371+ tmpDir := t.TempDir()
372+ log := testLogger()
373+
374+ storage, err := NewBlobStorage(tmpDir, log)
375+ require.NoError(t, err)
376+
377+ // Test that blobs are stored with proper directory structure
378+ blobData := []byte("test blob content")
379+ digest := "sha256:abcdef123456"
380+
381+ err = storage.PutBlob(digest, bytes.NewReader(blobData), int64(len(blobData)))
382+ require.NoError(t, err)
383+
384+ // Check the path structure: blobs/sha256/ab/abcdef123456
385+ expectedPath := filepath.Join(tmpDir, "blobs", "sha256", "ab", "abcdef123456")
386+ _, err = os.Stat(expectedPath)
387+ assert.NoError(t, err)
388+}
389+
390+func TestBlobStorage_DigestFormats(t *testing.T) {
391+ tmpDir := t.TempDir()
392+ log := testLogger()
393+
394+ storage, err := NewBlobStorage(tmpDir, log)
395+ require.NoError(t, err)
396+
397+ tests := []struct {
398+ name string
399+ digest string
400+ }{
401+ {"sha256 standard", "sha256:abc123def456"},
402+ {"sha512", "sha512:xyz789abc"},
403+ {"short hash", "sha256:ab"},
404+ {"long hash", "sha256:abcdefghijklmnopqrstuvwxyz0123456789"},
405+ }
406+
407+ for _, tt := range tests {
408+ t.Run(tt.name, func(t *testing.T) {
409+ blobData := []byte("test content for " + tt.digest)
410+
411+ err := storage.PutBlob(tt.digest, bytes.NewReader(blobData), int64(len(blobData)))
412+ require.NoError(t, err)
413+
414+ assert.True(t, storage.BlobExists(tt.digest))
415+
416+ reader, err := storage.GetBlob(tt.digest)
417+ require.NoError(t, err)
418+ defer reader.Close()
419+
420+ data, err := io.ReadAll(reader)
421+ require.NoError(t, err)
422+ assert.Equal(t, blobData, data)
423+ })
424+ }
425+}
426+
427+func TestBlobStorage_CompleteUploadFlow(t *testing.T) {
428+ tmpDir := t.TempDir()
429+ log := testLogger()
430+
431+ storage, err := NewBlobStorage(tmpDir, log)
432+ require.NoError(t, err)
433+
434+ // Simulate a complete upload flow
435+ repoName := "myorg/myapp"
436+ digest := "sha256:completedupload123"
437+
438+ // 1. Start upload
439+ uuid, err := storage.StartBlobUpload(repoName)
440+ require.NoError(t, err)
441+
442+ // 2. Send multiple chunks
443+ chunks := [][]byte{
444+ []byte("first chunk of data"),
445+ []byte(" - middle chunk"),
446+ []byte(" - final chunk"),
447+ }
448+
449+ var totalSize int64
450+ for _, chunk := range chunks {
451+ size, err := storage.AppendBlobChunk(repoName, uuid, chunk)
452+ require.NoError(t, err)
453+ totalSize = size
454+ }
455+
456+ expectedTotal := int64(len(chunks[0]) + len(chunks[1]) + len(chunks[2]))
457+ assert.Equal(t, expectedTotal, totalSize)
458+
459+ // 3. Finish upload
460+ err = storage.FinishBlobUpload(uuid, digest)
461+ require.NoError(t, err)
462+
463+ // 4. Verify blob is accessible
464+ reader, err := storage.GetBlob(digest)
465+ require.NoError(t, err)
466+ defer reader.Close()
467+
468+ data, err := io.ReadAll(reader)
469+ require.NoError(t, err)
470+
471+ expectedData := "first chunk of data - middle chunk - final chunk"
472+ assert.Equal(t, expectedData, string(data))
473+}
@@ -0,0 +1,473 @@
1+package filesystem
2+
3+import (
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+
15+func testLogger() zerowrap.Logger {
16+ return zerowrap.Default()
17+}
18+
19+func TestNewBlobStorage(t *testing.T) {
20+ tmpDir := t.TempDir()
21+ log := testLogger()
22+
23+ storage, err := NewBlobStorage(tmpDir, log)
24+
25+ require.NoError(t, err)
26+ assert.NotNil(t, storage)
27+
28+ // Verify directories were created
29+ assert.DirExists(t, filepath.Join(tmpDir, "blobs"))
30+ assert.DirExists(t, filepath.Join(tmpDir, "uploads"))
31+}
32+
33+func TestNewBlobStorage_InvalidPath(t *testing.T) {
34+ log := testLogger()
35+
36+ // Try to create in a path that can't be created (no permissions)
37+ _, err := NewBlobStorage("/nonexistent/deeply/nested/path/that/should/fail", log)
38+
39+ assert.Error(t, err)
40+}
41+
42+func TestBlobStorage_PutAndGetBlob(t *testing.T) {
43+ tmpDir := t.TempDir()
44+ log := testLogger()
45+
46+ storage, err := NewBlobStorage(tmpDir, log)
47+ require.NoError(t, err)
48+
49+ blobData := []byte("test blob content")
50+ digest := "sha256:abc123def456"
51+
52+ // Put blob
53+ err = storage.PutBlob(digest, bytes.NewReader(blobData), int64(len(blobData)))
54+ require.NoError(t, err)
55+
56+ // Get blob
57+ reader, err := storage.GetBlob(digest)
58+ require.NoError(t, err)
59+ defer reader.Close()
60+
61+ data, err := io.ReadAll(reader)
62+ require.NoError(t, err)
63+
64+ assert.Equal(t, blobData, data)
65+}
66+
67+func TestBlobStorage_GetBlob_NotFound(t *testing.T) {
68+ tmpDir := t.TempDir()
69+ log := testLogger()
70+
71+ storage, err := NewBlobStorage(tmpDir, log)
72+ require.NoError(t, err)
73+
74+ reader, err := storage.GetBlob("sha256:notexists")
75+
76+ assert.Error(t, err)
77+ assert.Nil(t, reader)
78+ assert.Contains(t, err.Error(), "blob not found")
79+}
80+
81+func TestBlobStorage_GetBlobPath(t *testing.T) {
82+ tmpDir := t.TempDir()
83+ log := testLogger()
84+
85+ storage, err := NewBlobStorage(tmpDir, log)
86+ require.NoError(t, err)
87+
88+ blobData := []byte("test blob content")
89+ digest := "sha256:abc123def456"
90+
91+ // Put blob first
92+ err = storage.PutBlob(digest, bytes.NewReader(blobData), int64(len(blobData)))
93+ require.NoError(t, err)
94+
95+ // Get blob path
96+ path, err := storage.GetBlobPath(digest)
97+ require.NoError(t, err)
98+ assert.NotEmpty(t, path)
99+
100+ // Verify file exists at path
101+ _, err = os.Stat(path)
102+ assert.NoError(t, err)
103+}
104+
105+func TestBlobStorage_GetBlobPath_NotFound(t *testing.T) {
106+ tmpDir := t.TempDir()
107+ log := testLogger()
108+
109+ storage, err := NewBlobStorage(tmpDir, log)
110+ require.NoError(t, err)
111+
112+ path, err := storage.GetBlobPath("sha256:notexists")
113+
114+ assert.Error(t, err)
115+ assert.Empty(t, path)
116+ assert.Contains(t, err.Error(), "blob not found")
117+}
118+
119+func TestBlobStorage_PutBlob_SizeMismatch(t *testing.T) {
120+ tmpDir := t.TempDir()
121+ log := testLogger()
122+
123+ storage, err := NewBlobStorage(tmpDir, log)
124+ require.NoError(t, err)
125+
126+ blobData := []byte("test blob content")
127+ digest := "sha256:abc123"
128+
129+ // Put blob with wrong size
130+ err = storage.PutBlob(digest, bytes.NewReader(blobData), 999)
131+
132+ assert.Error(t, err)
133+ assert.Contains(t, err.Error(), "size mismatch")
134+}
135+
136+func TestBlobStorage_PutBlob_ZeroSize(t *testing.T) {
137+ tmpDir := t.TempDir()
138+ log := testLogger()
139+
140+ storage, err := NewBlobStorage(tmpDir, log)
141+ require.NoError(t, err)
142+
143+ blobData := []byte("test blob content")
144+ digest := "sha256:abc123"
145+
146+ // Put blob with size=0 (should skip size check)
147+ err = storage.PutBlob(digest, bytes.NewReader(blobData), 0)
148+
149+ assert.NoError(t, err)
150+
151+ // Verify blob was stored
152+ assert.True(t, storage.BlobExists(digest))
153+}
154+
155+func TestBlobStorage_DeleteBlob(t *testing.T) {
156+ tmpDir := t.TempDir()
157+ log := testLogger()
158+
159+ storage, err := NewBlobStorage(tmpDir, log)
160+ require.NoError(t, err)
161+
162+ blobData := []byte("test blob content")
163+ digest := "sha256:abc123"
164+
165+ // Put blob
166+ err = storage.PutBlob(digest, bytes.NewReader(blobData), int64(len(blobData)))
167+ require.NoError(t, err)
168+
169+ // Verify it exists
170+ assert.True(t, storage.BlobExists(digest))
171+
172+ // Delete blob
173+ err = storage.DeleteBlob(digest)
174+ require.NoError(t, err)
175+
176+ // Verify it's gone
177+ assert.False(t, storage.BlobExists(digest))
178+}
179+
180+func TestBlobStorage_DeleteBlob_NotFound(t *testing.T) {
181+ tmpDir := t.TempDir()
182+ log := testLogger()
183+
184+ storage, err := NewBlobStorage(tmpDir, log)
185+ require.NoError(t, err)
186+
187+ err = storage.DeleteBlob("sha256:notexists")
188+
189+ assert.Error(t, err)
190+ assert.Contains(t, err.Error(), "blob not found")
191+}
192+
193+func TestBlobStorage_BlobExists(t *testing.T) {
194+ tmpDir := t.TempDir()
195+ log := testLogger()
196+
197+ storage, err := NewBlobStorage(tmpDir, log)
198+ require.NoError(t, err)
199+
200+ digest := "sha256:abc123"
201+
202+ // Should not exist initially
203+ assert.False(t, storage.BlobExists(digest))
204+
205+ // Put blob
206+ blobData := []byte("test blob content")
207+ err = storage.PutBlob(digest, bytes.NewReader(blobData), int64(len(blobData)))
208+ require.NoError(t, err)
209+
210+ // Should exist now
211+ assert.True(t, storage.BlobExists(digest))
212+}
213+
214+func TestBlobStorage_StartBlobUpload(t *testing.T) {
215+ tmpDir := t.TempDir()
216+ log := testLogger()
217+
218+ storage, err := NewBlobStorage(tmpDir, log)
219+ require.NoError(t, err)
220+
221+ uuid, err := storage.StartBlobUpload("myapp")
222+
223+ require.NoError(t, err)
224+ assert.NotEmpty(t, uuid)
225+ assert.Contains(t, uuid, "myapp")
226+
227+ // Verify upload file was created
228+ uploadPath := filepath.Join(tmpDir, "uploads", uuid)
229+ _, err = os.Stat(uploadPath)
230+ assert.NoError(t, err)
231+}
232+
233+func TestBlobStorage_AppendBlobChunk(t *testing.T) {
234+ tmpDir := t.TempDir()
235+ log := testLogger()
236+
237+ storage, err := NewBlobStorage(tmpDir, log)
238+ require.NoError(t, err)
239+
240+ uuid, err := storage.StartBlobUpload("myapp")
241+ require.NoError(t, err)
242+
243+ // Append first chunk
244+ chunk1 := []byte("first chunk")
245+ size1, err := storage.AppendBlobChunk("myapp", uuid, chunk1)
246+ require.NoError(t, err)
247+ assert.Equal(t, int64(len(chunk1)), size1)
248+
249+ // Append second chunk
250+ chunk2 := []byte(" second chunk")
251+ size2, err := storage.AppendBlobChunk("myapp", uuid, chunk2)
252+ require.NoError(t, err)
253+ assert.Equal(t, int64(len(chunk1)+len(chunk2)), size2)
254+}
255+
256+func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) {
257+ tmpDir := t.TempDir()
258+ log := testLogger()
259+
260+ storage, err := NewBlobStorage(tmpDir, log)
261+ require.NoError(t, err)
262+
263+ _, err = storage.AppendBlobChunk("myapp", "nonexistent-uuid", []byte("data"))
264+
265+ assert.Error(t, err)
266+ assert.Contains(t, err.Error(), "upload not found")
267+}
268+
269+func TestBlobStorage_GetBlobUpload(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+ writer, err := storage.GetBlobUpload(uuid)
280+ require.NoError(t, err)
281+ defer writer.Close()
282+
283+ // Write some data
284+ _, err = writer.Write([]byte("test data"))
285+ assert.NoError(t, err)
286+}
287+
288+func TestBlobStorage_GetBlobUpload_NotFound(t *testing.T) {
289+ tmpDir := t.TempDir()
290+ log := testLogger()
291+
292+ storage, err := NewBlobStorage(tmpDir, log)
293+ require.NoError(t, err)
294+
295+ writer, err := storage.GetBlobUpload("nonexistent-uuid")
296+
297+ assert.Error(t, err)
298+ assert.Nil(t, writer)
299+ assert.Contains(t, err.Error(), "upload not found")
300+}
301+
302+func TestBlobStorage_FinishBlobUpload(t *testing.T) {
303+ tmpDir := t.TempDir()
304+ log := testLogger()
305+
306+ storage, err := NewBlobStorage(tmpDir, log)
307+ require.NoError(t, err)
308+
309+ // Start upload
310+ uuid, err := storage.StartBlobUpload("myapp")
311+ require.NoError(t, err)
312+
313+ // Append data
314+ blobData := []byte("complete blob content")
315+ _, err = storage.AppendBlobChunk("myapp", uuid, blobData)
316+ require.NoError(t, err)
317+
318+ // Finish upload
319+ digest := "sha256:finished123"
320+ err = storage.FinishBlobUpload(uuid, digest)
321+ require.NoError(t, err)
322+
323+ // Verify blob exists at final location
324+ assert.True(t, storage.BlobExists(digest))
325+
326+ // Verify upload file is gone
327+ uploadPath := filepath.Join(tmpDir, "uploads", uuid)
328+ _, err = os.Stat(uploadPath)
329+ assert.True(t, os.IsNotExist(err))
330+}
331+
332+func TestBlobStorage_CancelBlobUpload(t *testing.T) {
333+ tmpDir := t.TempDir()
334+ log := testLogger()
335+
336+ storage, err := NewBlobStorage(tmpDir, log)
337+ require.NoError(t, err)
338+
339+ // Start upload
340+ uuid, err := storage.StartBlobUpload("myapp")
341+ require.NoError(t, err)
342+
343+ // Verify upload file exists
344+ uploadPath := filepath.Join(tmpDir, "uploads", uuid)
345+ _, err = os.Stat(uploadPath)
346+ require.NoError(t, err)
347+
348+ // Cancel upload
349+ err = storage.CancelBlobUpload(uuid)
350+ require.NoError(t, err)
351+
352+ // Verify upload file is gone
353+ _, err = os.Stat(uploadPath)
354+ assert.True(t, os.IsNotExist(err))
355+}
356+
357+func TestBlobStorage_CancelBlobUpload_NotFound(t *testing.T) {
358+ tmpDir := t.TempDir()
359+ log := testLogger()
360+
361+ storage, err := NewBlobStorage(tmpDir, log)
362+ require.NoError(t, err)
363+
364+ err = storage.CancelBlobUpload("nonexistent-uuid")
365+
366+ assert.Error(t, err)
367+ assert.Contains(t, err.Error(), "upload not found")
368+}
369+
370+func TestBlobStorage_BlobPathStructure(t *testing.T) {
371+ tmpDir := t.TempDir()
372+ log := testLogger()
373+
374+ storage, err := NewBlobStorage(tmpDir, log)
375+ require.NoError(t, err)
376+
377+ // Test that blobs are stored with proper directory structure
378+ blobData := []byte("test blob content")
379+ digest := "sha256:abcdef123456"
380+
381+ err = storage.PutBlob(digest, bytes.NewReader(blobData), int64(len(blobData)))
382+ require.NoError(t, err)
383+
384+ // Check the path structure: blobs/sha256/ab/abcdef123456
385+ expectedPath := filepath.Join(tmpDir, "blobs", "sha256", "ab", "abcdef123456")
386+ _, err = os.Stat(expectedPath)
387+ assert.NoError(t, err)
388+}
389+
390+func TestBlobStorage_DigestFormats(t *testing.T) {
391+ tmpDir := t.TempDir()
392+ log := testLogger()
393+
394+ storage, err := NewBlobStorage(tmpDir, log)
395+ require.NoError(t, err)
396+
397+ tests := []struct {
398+ name string
399+ digest string
400+ }{
401+ {"sha256 standard", "sha256:abc123def456"},
402+ {"sha512", "sha512:xyz789abc"},
403+ {"short hash", "sha256:ab"},
404+ {"long hash", "sha256:abcdefghijklmnopqrstuvwxyz0123456789"},
405+ }
406+
407+ for _, tt := range tests {
408+ t.Run(tt.name, func(t *testing.T) {
409+ blobData := []byte("test content for " + tt.digest)
410+
411+ err := storage.PutBlob(tt.digest, bytes.NewReader(blobData), int64(len(blobData)))
412+ require.NoError(t, err)
413+
414+ assert.True(t, storage.BlobExists(tt.digest))
415+
416+ reader, err := storage.GetBlob(tt.digest)
417+ require.NoError(t, err)
418+ defer reader.Close()
419+
420+ data, err := io.ReadAll(reader)
421+ require.NoError(t, err)
422+ assert.Equal(t, blobData, data)
423+ })
424+ }
425+}
426+
427+func TestBlobStorage_CompleteUploadFlow(t *testing.T) {
428+ tmpDir := t.TempDir()
429+ log := testLogger()
430+
431+ storage, err := NewBlobStorage(tmpDir, log)
432+ require.NoError(t, err)
433+
434+ // Simulate a complete upload flow
435+ repoName := "myorg/myapp"
436+ digest := "sha256:completedupload123"
437+
438+ // 1. Start upload
439+ uuid, err := storage.StartBlobUpload(repoName)
440+ require.NoError(t, err)
441+
442+ // 2. Send multiple chunks
443+ chunks := [][]byte{
444+ []byte("first chunk of data"),
445+ []byte(" - middle chunk"),
446+ []byte(" - final chunk"),
447+ }
448+
449+ var totalSize int64
450+ for _, chunk := range chunks {
451+ size, err := storage.AppendBlobChunk(repoName, uuid, chunk)
452+ require.NoError(t, err)
453+ totalSize = size
454+ }
455+
456+ expectedTotal := int64(len(chunks[0]) + len(chunks[1]) + len(chunks[2]))
457+ assert.Equal(t, expectedTotal, totalSize)
458+
459+ // 3. Finish upload
460+ err = storage.FinishBlobUpload(uuid, digest)
461+ require.NoError(t, err)
462+
463+ // 4. Verify blob is accessible
464+ reader, err := storage.GetBlob(digest)
465+ require.NoError(t, err)
466+ defer reader.Close()
467+
468+ data, err := io.ReadAll(reader)
469+ require.NoError(t, err)
470+
471+ expectedData := "first chunk of data - middle chunk - final chunk"
472+ assert.Equal(t, expectedData, string(data))
473+}
A internal/adapters/out/filesystem/manifest_test.go +440 -0
@@ -0,0 +1,440 @@
1+package filesystem
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+func TestNewManifestStorage(t *testing.T) {
13+ tmpDir := t.TempDir()
14+ log := testLogger()
15+
16+ storage, err := NewManifestStorage(tmpDir, log)
17+
18+ require.NoError(t, err)
19+ assert.NotNil(t, storage)
20+
21+ // Verify repositories directory was created
22+ assert.DirExists(t, filepath.Join(tmpDir, "repositories"))
23+}
24+
25+func TestNewManifestStorage_InvalidPath(t *testing.T) {
26+ log := testLogger()
27+
28+ _, err := NewManifestStorage("/nonexistent/deeply/nested/path/that/should/fail", log)
29+
30+ assert.Error(t, err)
31+}
32+
33+func TestManifestStorage_PutAndGetManifest(t *testing.T) {
34+ tmpDir := t.TempDir()
35+ log := testLogger()
36+
37+ storage, err := NewManifestStorage(tmpDir, log)
38+ require.NoError(t, err)
39+
40+ manifestData := []byte(`{"schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json"}`)
41+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
42+
43+ // Put manifest
44+ err = storage.PutManifest("myapp", "latest", contentType, manifestData)
45+ require.NoError(t, err)
46+
47+ // Get manifest
48+ data, ct, err := storage.GetManifest("myapp", "latest")
49+ require.NoError(t, err)
50+
51+ assert.Equal(t, manifestData, data)
52+ assert.Equal(t, contentType, ct)
53+}
54+
55+func TestManifestStorage_GetManifest_NotFound(t *testing.T) {
56+ tmpDir := t.TempDir()
57+ log := testLogger()
58+
59+ storage, err := NewManifestStorage(tmpDir, log)
60+ require.NoError(t, err)
61+
62+ data, ct, err := storage.GetManifest("notexists", "latest")
63+
64+ assert.Error(t, err)
65+ assert.Nil(t, data)
66+ assert.Empty(t, ct)
67+ assert.Contains(t, err.Error(), "manifest not found")
68+}
69+
70+func TestManifestStorage_GetManifest_FallbackContentType(t *testing.T) {
71+ tmpDir := t.TempDir()
72+ log := testLogger()
73+
74+ storage, err := NewManifestStorage(tmpDir, log)
75+ require.NoError(t, err)
76+
77+ // Create manifest file directly without content type file
78+ manifestDir := filepath.Join(tmpDir, "repositories", "myapp", "manifests")
79+ err = os.MkdirAll(manifestDir, 0750)
80+ require.NoError(t, err)
81+
82+ manifestData := []byte(`{"schemaVersion": 2}`)
83+ err = os.WriteFile(filepath.Join(manifestDir, "latest"), manifestData, 0600)
84+ require.NoError(t, err)
85+
86+ // Get manifest should use fallback content type
87+ data, ct, err := storage.GetManifest("myapp", "latest")
88+ require.NoError(t, err)
89+
90+ assert.Equal(t, manifestData, data)
91+ assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", ct)
92+}
93+
94+func TestManifestStorage_DeleteManifest(t *testing.T) {
95+ tmpDir := t.TempDir()
96+ log := testLogger()
97+
98+ storage, err := NewManifestStorage(tmpDir, log)
99+ require.NoError(t, err)
100+
101+ manifestData := []byte(`{"schemaVersion": 2}`)
102+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
103+
104+ // Put manifest
105+ err = storage.PutManifest("myapp", "latest", contentType, manifestData)
106+ require.NoError(t, err)
107+
108+ // Verify it exists
109+ _, _, err = storage.GetManifest("myapp", "latest")
110+ require.NoError(t, err)
111+
112+ // Delete manifest
113+ err = storage.DeleteManifest("myapp", "latest")
114+ require.NoError(t, err)
115+
116+ // Verify it's gone
117+ _, _, err = storage.GetManifest("myapp", "latest")
118+ assert.Error(t, err)
119+}
120+
121+func TestManifestStorage_DeleteManifest_NotFound(t *testing.T) {
122+ tmpDir := t.TempDir()
123+ log := testLogger()
124+
125+ storage, err := NewManifestStorage(tmpDir, log)
126+ require.NoError(t, err)
127+
128+ err = storage.DeleteManifest("notexists", "latest")
129+
130+ assert.Error(t, err)
131+ assert.Contains(t, err.Error(), "manifest not found")
132+}
133+
134+func TestManifestStorage_ListTags(t *testing.T) {
135+ tmpDir := t.TempDir()
136+ log := testLogger()
137+
138+ storage, err := NewManifestStorage(tmpDir, log)
139+ require.NoError(t, err)
140+
141+ manifestData := []byte(`{"schemaVersion": 2}`)
142+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
143+
144+ // Put multiple manifests
145+ err = storage.PutManifest("myapp", "latest", contentType, manifestData)
146+ require.NoError(t, err)
147+ err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
148+ require.NoError(t, err)
149+ err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
150+ require.NoError(t, err)
151+
152+ // List tags
153+ tags, err := storage.ListTags("myapp")
154+ require.NoError(t, err)
155+
156+ assert.Len(t, tags, 3)
157+ assert.Contains(t, tags, "latest")
158+ assert.Contains(t, tags, "v1.0")
159+ assert.Contains(t, tags, "v2.0")
160+}
161+
162+func TestManifestStorage_ListTags_Empty(t *testing.T) {
163+ tmpDir := t.TempDir()
164+ log := testLogger()
165+
166+ storage, err := NewManifestStorage(tmpDir, log)
167+ require.NoError(t, err)
168+
169+ // List tags for non-existent repo
170+ tags, err := storage.ListTags("notexists")
171+ require.NoError(t, err)
172+
173+ assert.Empty(t, tags)
174+}
175+
176+func TestManifestStorage_ListTags_AfterDelete(t *testing.T) {
177+ tmpDir := t.TempDir()
178+ log := testLogger()
179+
180+ storage, err := NewManifestStorage(tmpDir, log)
181+ require.NoError(t, err)
182+
183+ manifestData := []byte(`{"schemaVersion": 2}`)
184+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
185+
186+ // Put manifests
187+ err = storage.PutManifest("myapp", "latest", contentType, manifestData)
188+ require.NoError(t, err)
189+ err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
190+ require.NoError(t, err)
191+
192+ // Delete one
193+ err = storage.DeleteManifest("myapp", "v1.0")
194+ require.NoError(t, err)
195+
196+ // List tags
197+ tags, err := storage.ListTags("myapp")
198+ require.NoError(t, err)
199+
200+ assert.Len(t, tags, 1)
201+ assert.Contains(t, tags, "latest")
202+ assert.NotContains(t, tags, "v1.0")
203+}
204+
205+func TestManifestStorage_ListRepositories(t *testing.T) {
206+ tmpDir := t.TempDir()
207+ log := testLogger()
208+
209+ storage, err := NewManifestStorage(tmpDir, log)
210+ require.NoError(t, err)
211+
212+ manifestData := []byte(`{"schemaVersion": 2}`)
213+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
214+
215+ // Put manifests for different repos
216+ err = storage.PutManifest("app1", "latest", contentType, manifestData)
217+ require.NoError(t, err)
218+ err = storage.PutManifest("app2", "latest", contentType, manifestData)
219+ require.NoError(t, err)
220+ err = storage.PutManifest("org/app3", "latest", contentType, manifestData)
221+ require.NoError(t, err)
222+
223+ // List repositories
224+ repos, err := storage.ListRepositories()
225+ require.NoError(t, err)
226+
227+ assert.Len(t, repos, 3)
228+ assert.Contains(t, repos, "app1")
229+ assert.Contains(t, repos, "app2")
230+ assert.Contains(t, repos, "org/app3")
231+}
232+
233+func TestManifestStorage_ListRepositories_Empty(t *testing.T) {
234+ tmpDir := t.TempDir()
235+ log := testLogger()
236+
237+ storage, err := NewManifestStorage(tmpDir, log)
238+ require.NoError(t, err)
239+
240+ repos, err := storage.ListRepositories()
241+ require.NoError(t, err)
242+
243+ assert.Empty(t, repos)
244+}
245+
246+func TestManifestStorage_NestedRepositoryNames(t *testing.T) {
247+ tmpDir := t.TempDir()
248+ log := testLogger()
249+
250+ storage, err := NewManifestStorage(tmpDir, log)
251+ require.NoError(t, err)
252+
253+ manifestData := []byte(`{"schemaVersion": 2}`)
254+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
255+
256+ tests := []struct {
257+ name string
258+ reference string
259+ }{
260+ {"simple", "latest"},
261+ {"org/project", "v1.0"},
262+ {"org/team/project", "v2.0"},
263+ {"deeply/nested/repo/name", "sha256:abc123"},
264+ }
265+
266+ for _, tt := range tests {
267+ t.Run(tt.name, func(t *testing.T) {
268+ err := storage.PutManifest(tt.name, tt.reference, contentType, manifestData)
269+ require.NoError(t, err)
270+
271+ data, ct, err := storage.GetManifest(tt.name, tt.reference)
272+ require.NoError(t, err)
273+ assert.Equal(t, manifestData, data)
274+ assert.Equal(t, contentType, ct)
275+ })
276+ }
277+}
278+
279+func TestManifestStorage_ContentTypes(t *testing.T) {
280+ tmpDir := t.TempDir()
281+ log := testLogger()
282+
283+ storage, err := NewManifestStorage(tmpDir, log)
284+ require.NoError(t, err)
285+
286+ tests := []struct {
287+ name string
288+ contentType string
289+ }{
290+ {"v2 manifest", "application/vnd.docker.distribution.manifest.v2+json"},
291+ {"v1 manifest", "application/vnd.docker.distribution.manifest.v1+json"},
292+ {"OCI manifest", "application/vnd.oci.image.manifest.v1+json"},
293+ {"OCI index", "application/vnd.oci.image.index.v1+json"},
294+ {"manifest list", "application/vnd.docker.distribution.manifest.list.v2+json"},
295+ }
296+
297+ for i, tt := range tests {
298+ t.Run(tt.name, func(t *testing.T) {
299+ repoName := "test-repo"
300+ reference := "tag-" + string(rune('a'+i))
301+ manifestData := []byte(`{"schemaVersion": 2}`)
302+
303+ err := storage.PutManifest(repoName, reference, tt.contentType, manifestData)
304+ require.NoError(t, err)
305+
306+ _, ct, err := storage.GetManifest(repoName, reference)
307+ require.NoError(t, err)
308+ assert.Equal(t, tt.contentType, ct)
309+ })
310+ }
311+}
312+
313+func TestManifestStorage_DuplicateTag(t *testing.T) {
314+ tmpDir := t.TempDir()
315+ log := testLogger()
316+
317+ storage, err := NewManifestStorage(tmpDir, log)
318+ require.NoError(t, err)
319+
320+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
321+
322+ // Put first manifest
323+ manifestData1 := []byte(`{"schemaVersion": 2, "version": 1}`)
324+ err = storage.PutManifest("myapp", "latest", contentType, manifestData1)
325+ require.NoError(t, err)
326+
327+ // Put second manifest with same tag (overwrite)
328+ manifestData2 := []byte(`{"schemaVersion": 2, "version": 2}`)
329+ err = storage.PutManifest("myapp", "latest", contentType, manifestData2)
330+ require.NoError(t, err)
331+
332+ // Get manifest should return the latest one
333+ data, _, err := storage.GetManifest("myapp", "latest")
334+ require.NoError(t, err)
335+ assert.Equal(t, manifestData2, data)
336+
337+ // Tags list should only have one entry
338+ tags, err := storage.ListTags("myapp")
339+ require.NoError(t, err)
340+ assert.Len(t, tags, 1)
341+}
342+
343+func TestManifestStorage_LargeManifest(t *testing.T) {
344+ tmpDir := t.TempDir()
345+ log := testLogger()
346+
347+ storage, err := NewManifestStorage(tmpDir, log)
348+ require.NoError(t, err)
349+
350+ // Create a large manifest (simulating one with many layers)
351+ largeManifest := make([]byte, 1024*1024) // 1MB
352+ for i := range largeManifest {
353+ largeManifest[i] = byte('a' + (i % 26))
354+ }
355+
356+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
357+
358+ err = storage.PutManifest("large-app", "latest", contentType, largeManifest)
359+ require.NoError(t, err)
360+
361+ data, _, err := storage.GetManifest("large-app", "latest")
362+ require.NoError(t, err)
363+ assert.Equal(t, largeManifest, data)
364+}
365+
366+func TestManifestStorage_SpecialCharactersInReference(t *testing.T) {
367+ tmpDir := t.TempDir()
368+ log := testLogger()
369+
370+ storage, err := NewManifestStorage(tmpDir, log)
371+ require.NoError(t, err)
372+
373+ manifestData := []byte(`{"schemaVersion": 2}`)
374+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
375+
376+ // SHA256 digests are valid references
377+ reference := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
378+
379+ err = storage.PutManifest("myapp", reference, contentType, manifestData)
380+ require.NoError(t, err)
381+
382+ data, _, err := storage.GetManifest("myapp", reference)
383+ require.NoError(t, err)
384+ assert.Equal(t, manifestData, data)
385+}
386+
387+func TestManifestStorage_CompleteWorkflow(t *testing.T) {
388+ tmpDir := t.TempDir()
389+ log := testLogger()
390+
391+ storage, err := NewManifestStorage(tmpDir, log)
392+ require.NoError(t, err)
393+
394+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
395+
396+ // Simulate a complete registry workflow
397+ // 1. Push first version
398+ v1Data := []byte(`{"schemaVersion": 2, "version": "1.0"}`)
399+ err = storage.PutManifest("myorg/myapp", "v1.0", contentType, v1Data)
400+ require.NoError(t, err)
401+ err = storage.PutManifest("myorg/myapp", "latest", contentType, v1Data)
402+ require.NoError(t, err)
403+
404+ // 2. Push second version
405+ v2Data := []byte(`{"schemaVersion": 2, "version": "2.0"}`)
406+ err = storage.PutManifest("myorg/myapp", "v2.0", contentType, v2Data)
407+ require.NoError(t, err)
408+ err = storage.PutManifest("myorg/myapp", "latest", contentType, v2Data)
409+ require.NoError(t, err)
410+
411+ // 3. Verify tags
412+ tags, err := storage.ListTags("myorg/myapp")
413+ require.NoError(t, err)
414+ assert.Len(t, tags, 3)
415+
416+ // 4. Verify latest points to v2
417+ data, _, err := storage.GetManifest("myorg/myapp", "latest")
418+ require.NoError(t, err)
419+ assert.Equal(t, v2Data, data)
420+
421+ // 5. Verify v1 is still accessible
422+ data, _, err = storage.GetManifest("myorg/myapp", "v1.0")
423+ require.NoError(t, err)
424+ assert.Equal(t, v1Data, data)
425+
426+ // 6. Delete old version
427+ err = storage.DeleteManifest("myorg/myapp", "v1.0")
428+ require.NoError(t, err)
429+
430+ // 7. Verify tags updated
431+ tags, err = storage.ListTags("myorg/myapp")
432+ require.NoError(t, err)
433+ assert.Len(t, tags, 2)
434+ assert.NotContains(t, tags, "v1.0")
435+
436+ // 8. List repositories
437+ repos, err := storage.ListRepositories()
438+ require.NoError(t, err)
439+ assert.Contains(t, repos, "myorg/myapp")
440+}
@@ -0,0 +1,440 @@
1+package filesystem
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+func TestNewManifestStorage(t *testing.T) {
13+ tmpDir := t.TempDir()
14+ log := testLogger()
15+
16+ storage, err := NewManifestStorage(tmpDir, log)
17+
18+ require.NoError(t, err)
19+ assert.NotNil(t, storage)
20+
21+ // Verify repositories directory was created
22+ assert.DirExists(t, filepath.Join(tmpDir, "repositories"))
23+}
24+
25+func TestNewManifestStorage_InvalidPath(t *testing.T) {
26+ log := testLogger()
27+
28+ _, err := NewManifestStorage("/nonexistent/deeply/nested/path/that/should/fail", log)
29+
30+ assert.Error(t, err)
31+}
32+
33+func TestManifestStorage_PutAndGetManifest(t *testing.T) {
34+ tmpDir := t.TempDir()
35+ log := testLogger()
36+
37+ storage, err := NewManifestStorage(tmpDir, log)
38+ require.NoError(t, err)
39+
40+ manifestData := []byte(`{"schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json"}`)
41+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
42+
43+ // Put manifest
44+ err = storage.PutManifest("myapp", "latest", contentType, manifestData)
45+ require.NoError(t, err)
46+
47+ // Get manifest
48+ data, ct, err := storage.GetManifest("myapp", "latest")
49+ require.NoError(t, err)
50+
51+ assert.Equal(t, manifestData, data)
52+ assert.Equal(t, contentType, ct)
53+}
54+
55+func TestManifestStorage_GetManifest_NotFound(t *testing.T) {
56+ tmpDir := t.TempDir()
57+ log := testLogger()
58+
59+ storage, err := NewManifestStorage(tmpDir, log)
60+ require.NoError(t, err)
61+
62+ data, ct, err := storage.GetManifest("notexists", "latest")
63+
64+ assert.Error(t, err)
65+ assert.Nil(t, data)
66+ assert.Empty(t, ct)
67+ assert.Contains(t, err.Error(), "manifest not found")
68+}
69+
70+func TestManifestStorage_GetManifest_FallbackContentType(t *testing.T) {
71+ tmpDir := t.TempDir()
72+ log := testLogger()
73+
74+ storage, err := NewManifestStorage(tmpDir, log)
75+ require.NoError(t, err)
76+
77+ // Create manifest file directly without content type file
78+ manifestDir := filepath.Join(tmpDir, "repositories", "myapp", "manifests")
79+ err = os.MkdirAll(manifestDir, 0750)
80+ require.NoError(t, err)
81+
82+ manifestData := []byte(`{"schemaVersion": 2}`)
83+ err = os.WriteFile(filepath.Join(manifestDir, "latest"), manifestData, 0600)
84+ require.NoError(t, err)
85+
86+ // Get manifest should use fallback content type
87+ data, ct, err := storage.GetManifest("myapp", "latest")
88+ require.NoError(t, err)
89+
90+ assert.Equal(t, manifestData, data)
91+ assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", ct)
92+}
93+
94+func TestManifestStorage_DeleteManifest(t *testing.T) {
95+ tmpDir := t.TempDir()
96+ log := testLogger()
97+
98+ storage, err := NewManifestStorage(tmpDir, log)
99+ require.NoError(t, err)
100+
101+ manifestData := []byte(`{"schemaVersion": 2}`)
102+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
103+
104+ // Put manifest
105+ err = storage.PutManifest("myapp", "latest", contentType, manifestData)
106+ require.NoError(t, err)
107+
108+ // Verify it exists
109+ _, _, err = storage.GetManifest("myapp", "latest")
110+ require.NoError(t, err)
111+
112+ // Delete manifest
113+ err = storage.DeleteManifest("myapp", "latest")
114+ require.NoError(t, err)
115+
116+ // Verify it's gone
117+ _, _, err = storage.GetManifest("myapp", "latest")
118+ assert.Error(t, err)
119+}
120+
121+func TestManifestStorage_DeleteManifest_NotFound(t *testing.T) {
122+ tmpDir := t.TempDir()
123+ log := testLogger()
124+
125+ storage, err := NewManifestStorage(tmpDir, log)
126+ require.NoError(t, err)
127+
128+ err = storage.DeleteManifest("notexists", "latest")
129+
130+ assert.Error(t, err)
131+ assert.Contains(t, err.Error(), "manifest not found")
132+}
133+
134+func TestManifestStorage_ListTags(t *testing.T) {
135+ tmpDir := t.TempDir()
136+ log := testLogger()
137+
138+ storage, err := NewManifestStorage(tmpDir, log)
139+ require.NoError(t, err)
140+
141+ manifestData := []byte(`{"schemaVersion": 2}`)
142+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
143+
144+ // Put multiple manifests
145+ err = storage.PutManifest("myapp", "latest", contentType, manifestData)
146+ require.NoError(t, err)
147+ err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
148+ require.NoError(t, err)
149+ err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
150+ require.NoError(t, err)
151+
152+ // List tags
153+ tags, err := storage.ListTags("myapp")
154+ require.NoError(t, err)
155+
156+ assert.Len(t, tags, 3)
157+ assert.Contains(t, tags, "latest")
158+ assert.Contains(t, tags, "v1.0")
159+ assert.Contains(t, tags, "v2.0")
160+}
161+
162+func TestManifestStorage_ListTags_Empty(t *testing.T) {
163+ tmpDir := t.TempDir()
164+ log := testLogger()
165+
166+ storage, err := NewManifestStorage(tmpDir, log)
167+ require.NoError(t, err)
168+
169+ // List tags for non-existent repo
170+ tags, err := storage.ListTags("notexists")
171+ require.NoError(t, err)
172+
173+ assert.Empty(t, tags)
174+}
175+
176+func TestManifestStorage_ListTags_AfterDelete(t *testing.T) {
177+ tmpDir := t.TempDir()
178+ log := testLogger()
179+
180+ storage, err := NewManifestStorage(tmpDir, log)
181+ require.NoError(t, err)
182+
183+ manifestData := []byte(`{"schemaVersion": 2}`)
184+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
185+
186+ // Put manifests
187+ err = storage.PutManifest("myapp", "latest", contentType, manifestData)
188+ require.NoError(t, err)
189+ err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
190+ require.NoError(t, err)
191+
192+ // Delete one
193+ err = storage.DeleteManifest("myapp", "v1.0")
194+ require.NoError(t, err)
195+
196+ // List tags
197+ tags, err := storage.ListTags("myapp")
198+ require.NoError(t, err)
199+
200+ assert.Len(t, tags, 1)
201+ assert.Contains(t, tags, "latest")
202+ assert.NotContains(t, tags, "v1.0")
203+}
204+
205+func TestManifestStorage_ListRepositories(t *testing.T) {
206+ tmpDir := t.TempDir()
207+ log := testLogger()
208+
209+ storage, err := NewManifestStorage(tmpDir, log)
210+ require.NoError(t, err)
211+
212+ manifestData := []byte(`{"schemaVersion": 2}`)
213+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
214+
215+ // Put manifests for different repos
216+ err = storage.PutManifest("app1", "latest", contentType, manifestData)
217+ require.NoError(t, err)
218+ err = storage.PutManifest("app2", "latest", contentType, manifestData)
219+ require.NoError(t, err)
220+ err = storage.PutManifest("org/app3", "latest", contentType, manifestData)
221+ require.NoError(t, err)
222+
223+ // List repositories
224+ repos, err := storage.ListRepositories()
225+ require.NoError(t, err)
226+
227+ assert.Len(t, repos, 3)
228+ assert.Contains(t, repos, "app1")
229+ assert.Contains(t, repos, "app2")
230+ assert.Contains(t, repos, "org/app3")
231+}
232+
233+func TestManifestStorage_ListRepositories_Empty(t *testing.T) {
234+ tmpDir := t.TempDir()
235+ log := testLogger()
236+
237+ storage, err := NewManifestStorage(tmpDir, log)
238+ require.NoError(t, err)
239+
240+ repos, err := storage.ListRepositories()
241+ require.NoError(t, err)
242+
243+ assert.Empty(t, repos)
244+}
245+
246+func TestManifestStorage_NestedRepositoryNames(t *testing.T) {
247+ tmpDir := t.TempDir()
248+ log := testLogger()
249+
250+ storage, err := NewManifestStorage(tmpDir, log)
251+ require.NoError(t, err)
252+
253+ manifestData := []byte(`{"schemaVersion": 2}`)
254+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
255+
256+ tests := []struct {
257+ name string
258+ reference string
259+ }{
260+ {"simple", "latest"},
261+ {"org/project", "v1.0"},
262+ {"org/team/project", "v2.0"},
263+ {"deeply/nested/repo/name", "sha256:abc123"},
264+ }
265+
266+ for _, tt := range tests {
267+ t.Run(tt.name, func(t *testing.T) {
268+ err := storage.PutManifest(tt.name, tt.reference, contentType, manifestData)
269+ require.NoError(t, err)
270+
271+ data, ct, err := storage.GetManifest(tt.name, tt.reference)
272+ require.NoError(t, err)
273+ assert.Equal(t, manifestData, data)
274+ assert.Equal(t, contentType, ct)
275+ })
276+ }
277+}
278+
279+func TestManifestStorage_ContentTypes(t *testing.T) {
280+ tmpDir := t.TempDir()
281+ log := testLogger()
282+
283+ storage, err := NewManifestStorage(tmpDir, log)
284+ require.NoError(t, err)
285+
286+ tests := []struct {
287+ name string
288+ contentType string
289+ }{
290+ {"v2 manifest", "application/vnd.docker.distribution.manifest.v2+json"},
291+ {"v1 manifest", "application/vnd.docker.distribution.manifest.v1+json"},
292+ {"OCI manifest", "application/vnd.oci.image.manifest.v1+json"},
293+ {"OCI index", "application/vnd.oci.image.index.v1+json"},
294+ {"manifest list", "application/vnd.docker.distribution.manifest.list.v2+json"},
295+ }
296+
297+ for i, tt := range tests {
298+ t.Run(tt.name, func(t *testing.T) {
299+ repoName := "test-repo"
300+ reference := "tag-" + string(rune('a'+i))
301+ manifestData := []byte(`{"schemaVersion": 2}`)
302+
303+ err := storage.PutManifest(repoName, reference, tt.contentType, manifestData)
304+ require.NoError(t, err)
305+
306+ _, ct, err := storage.GetManifest(repoName, reference)
307+ require.NoError(t, err)
308+ assert.Equal(t, tt.contentType, ct)
309+ })
310+ }
311+}
312+
313+func TestManifestStorage_DuplicateTag(t *testing.T) {
314+ tmpDir := t.TempDir()
315+ log := testLogger()
316+
317+ storage, err := NewManifestStorage(tmpDir, log)
318+ require.NoError(t, err)
319+
320+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
321+
322+ // Put first manifest
323+ manifestData1 := []byte(`{"schemaVersion": 2, "version": 1}`)
324+ err = storage.PutManifest("myapp", "latest", contentType, manifestData1)
325+ require.NoError(t, err)
326+
327+ // Put second manifest with same tag (overwrite)
328+ manifestData2 := []byte(`{"schemaVersion": 2, "version": 2}`)
329+ err = storage.PutManifest("myapp", "latest", contentType, manifestData2)
330+ require.NoError(t, err)
331+
332+ // Get manifest should return the latest one
333+ data, _, err := storage.GetManifest("myapp", "latest")
334+ require.NoError(t, err)
335+ assert.Equal(t, manifestData2, data)
336+
337+ // Tags list should only have one entry
338+ tags, err := storage.ListTags("myapp")
339+ require.NoError(t, err)
340+ assert.Len(t, tags, 1)
341+}
342+
343+func TestManifestStorage_LargeManifest(t *testing.T) {
344+ tmpDir := t.TempDir()
345+ log := testLogger()
346+
347+ storage, err := NewManifestStorage(tmpDir, log)
348+ require.NoError(t, err)
349+
350+ // Create a large manifest (simulating one with many layers)
351+ largeManifest := make([]byte, 1024*1024) // 1MB
352+ for i := range largeManifest {
353+ largeManifest[i] = byte('a' + (i % 26))
354+ }
355+
356+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
357+
358+ err = storage.PutManifest("large-app", "latest", contentType, largeManifest)
359+ require.NoError(t, err)
360+
361+ data, _, err := storage.GetManifest("large-app", "latest")
362+ require.NoError(t, err)
363+ assert.Equal(t, largeManifest, data)
364+}
365+
366+func TestManifestStorage_SpecialCharactersInReference(t *testing.T) {
367+ tmpDir := t.TempDir()
368+ log := testLogger()
369+
370+ storage, err := NewManifestStorage(tmpDir, log)
371+ require.NoError(t, err)
372+
373+ manifestData := []byte(`{"schemaVersion": 2}`)
374+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
375+
376+ // SHA256 digests are valid references
377+ reference := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
378+
379+ err = storage.PutManifest("myapp", reference, contentType, manifestData)
380+ require.NoError(t, err)
381+
382+ data, _, err := storage.GetManifest("myapp", reference)
383+ require.NoError(t, err)
384+ assert.Equal(t, manifestData, data)
385+}
386+
387+func TestManifestStorage_CompleteWorkflow(t *testing.T) {
388+ tmpDir := t.TempDir()
389+ log := testLogger()
390+
391+ storage, err := NewManifestStorage(tmpDir, log)
392+ require.NoError(t, err)
393+
394+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
395+
396+ // Simulate a complete registry workflow
397+ // 1. Push first version
398+ v1Data := []byte(`{"schemaVersion": 2, "version": "1.0"}`)
399+ err = storage.PutManifest("myorg/myapp", "v1.0", contentType, v1Data)
400+ require.NoError(t, err)
401+ err = storage.PutManifest("myorg/myapp", "latest", contentType, v1Data)
402+ require.NoError(t, err)
403+
404+ // 2. Push second version
405+ v2Data := []byte(`{"schemaVersion": 2, "version": "2.0"}`)
406+ err = storage.PutManifest("myorg/myapp", "v2.0", contentType, v2Data)
407+ require.NoError(t, err)
408+ err = storage.PutManifest("myorg/myapp", "latest", contentType, v2Data)
409+ require.NoError(t, err)
410+
411+ // 3. Verify tags
412+ tags, err := storage.ListTags("myorg/myapp")
413+ require.NoError(t, err)
414+ assert.Len(t, tags, 3)
415+
416+ // 4. Verify latest points to v2
417+ data, _, err := storage.GetManifest("myorg/myapp", "latest")
418+ require.NoError(t, err)
419+ assert.Equal(t, v2Data, data)
420+
421+ // 5. Verify v1 is still accessible
422+ data, _, err = storage.GetManifest("myorg/myapp", "v1.0")
423+ require.NoError(t, err)
424+ assert.Equal(t, v1Data, data)
425+
426+ // 6. Delete old version
427+ err = storage.DeleteManifest("myorg/myapp", "v1.0")
428+ require.NoError(t, err)
429+
430+ // 7. Verify tags updated
431+ tags, err = storage.ListTags("myorg/myapp")
432+ require.NoError(t, err)
433+ assert.Len(t, tags, 2)
434+ assert.NotContains(t, tags, "v1.0")
435+
436+ // 8. List repositories
437+ repos, err := storage.ListRepositories()
438+ require.NoError(t, err)
439+ assert.Contains(t, repos, "myorg/myapp")
440+}

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