1package filesystem
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10)
11
12func 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
25func 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
33func 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
55func 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
70func 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
94func 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
121func 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
134func 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
162func 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
176func TestManifestStorage_ListTags_SkipsDigestReference(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 err = storage.PutManifest("myapp", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", contentType, manifestData)
187 require.NoError(t, err)
188
189 tags, err := storage.ListTags("myapp")
190 require.NoError(t, err)
191
192 assert.Empty(t, tags)
193}
194
195func TestManifestStorage_ListTags_AfterDelete(t *testing.T) {
196 tmpDir := t.TempDir()
197 log := testLogger()
198
199 storage, err := NewManifestStorage(tmpDir, log)
200 require.NoError(t, err)
201
202 manifestData := []byte(`{"schemaVersion": 2}`)
203 contentType := "application/vnd.docker.distribution.manifest.v2+json"
204
205 // Put manifests
206 err = storage.PutManifest("myapp", "latest", contentType, manifestData)
207 require.NoError(t, err)
208 err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
209 require.NoError(t, err)
210
211 // Delete one
212 err = storage.DeleteManifest("myapp", "v1.0")
213 require.NoError(t, err)
214
215 // List tags
216 tags, err := storage.ListTags("myapp")
217 require.NoError(t, err)
218
219 assert.Len(t, tags, 1)
220 assert.Contains(t, tags, "latest")
221 assert.NotContains(t, tags, "v1.0")
222}
223
224func TestManifestStorage_ListRepositories(t *testing.T) {
225 tmpDir := t.TempDir()
226 log := testLogger()
227
228 storage, err := NewManifestStorage(tmpDir, log)
229 require.NoError(t, err)
230
231 manifestData := []byte(`{"schemaVersion": 2}`)
232 contentType := "application/vnd.docker.distribution.manifest.v2+json"
233
234 // Put manifests for different repos
235 err = storage.PutManifest("app1", "latest", contentType, manifestData)
236 require.NoError(t, err)
237 err = storage.PutManifest("app2", "latest", contentType, manifestData)
238 require.NoError(t, err)
239 err = storage.PutManifest("org/app3", "latest", contentType, manifestData)
240 require.NoError(t, err)
241
242 // List repositories
243 repos, err := storage.ListRepositories()
244 require.NoError(t, err)
245
246 assert.Len(t, repos, 3)
247 assert.Contains(t, repos, "app1")
248 assert.Contains(t, repos, "app2")
249 assert.Contains(t, repos, "org/app3")
250}
251
252func TestManifestStorage_ListRepositories_Empty(t *testing.T) {
253 tmpDir := t.TempDir()
254 log := testLogger()
255
256 storage, err := NewManifestStorage(tmpDir, log)
257 require.NoError(t, err)
258
259 repos, err := storage.ListRepositories()
260 require.NoError(t, err)
261
262 assert.Empty(t, repos)
263}
264
265func TestManifestStorage_NestedRepositoryNames(t *testing.T) {
266 tmpDir := t.TempDir()
267 log := testLogger()
268
269 storage, err := NewManifestStorage(tmpDir, log)
270 require.NoError(t, err)
271
272 manifestData := []byte(`{"schemaVersion": 2}`)
273 contentType := "application/vnd.docker.distribution.manifest.v2+json"
274
275 tests := []struct {
276 name string
277 reference string
278 }{
279 {"simple", "latest"},
280 {"org/project", "v1.0"},
281 {"org/team/project", "v2.0"},
282 {"deeply/nested/repo/name", "sha256:abc123"},
283 }
284
285 for _, tt := range tests {
286 t.Run(tt.name, func(t *testing.T) {
287 err := storage.PutManifest(tt.name, tt.reference, contentType, manifestData)
288 require.NoError(t, err)
289
290 data, ct, err := storage.GetManifest(tt.name, tt.reference)
291 require.NoError(t, err)
292 assert.Equal(t, manifestData, data)
293 assert.Equal(t, contentType, ct)
294 })
295 }
296}
297
298func TestManifestStorage_ContentTypes(t *testing.T) {
299 tmpDir := t.TempDir()
300 log := testLogger()
301
302 storage, err := NewManifestStorage(tmpDir, log)
303 require.NoError(t, err)
304
305 tests := []struct {
306 name string
307 contentType string
308 }{
309 {"v2 manifest", "application/vnd.docker.distribution.manifest.v2+json"},
310 {"v1 manifest", "application/vnd.docker.distribution.manifest.v1+json"},
311 {"OCI manifest", "application/vnd.oci.image.manifest.v1+json"},
312 {"OCI index", "application/vnd.oci.image.index.v1+json"},
313 {"manifest list", "application/vnd.docker.distribution.manifest.list.v2+json"},
314 }
315
316 for i, tt := range tests {
317 t.Run(tt.name, func(t *testing.T) {
318 repoName := "test-repo"
319 reference := "tag-" + string(rune('a'+i))
320 manifestData := []byte(`{"schemaVersion": 2}`)
321
322 err := storage.PutManifest(repoName, reference, tt.contentType, manifestData)
323 require.NoError(t, err)
324
325 _, ct, err := storage.GetManifest(repoName, reference)
326 require.NoError(t, err)
327 assert.Equal(t, tt.contentType, ct)
328 })
329 }
330}
331
332func TestManifestStorage_DuplicateTag(t *testing.T) {
333 tmpDir := t.TempDir()
334 log := testLogger()
335
336 storage, err := NewManifestStorage(tmpDir, log)
337 require.NoError(t, err)
338
339 contentType := "application/vnd.docker.distribution.manifest.v2+json"
340
341 // Put first manifest
342 manifestData1 := []byte(`{"schemaVersion": 2, "version": 1}`)
343 err = storage.PutManifest("myapp", "latest", contentType, manifestData1)
344 require.NoError(t, err)
345
346 // Put second manifest with same tag (overwrite)
347 manifestData2 := []byte(`{"schemaVersion": 2, "version": 2}`)
348 err = storage.PutManifest("myapp", "latest", contentType, manifestData2)
349 require.NoError(t, err)
350
351 // Get manifest should return the latest one
352 data, _, err := storage.GetManifest("myapp", "latest")
353 require.NoError(t, err)
354 assert.Equal(t, manifestData2, data)
355
356 // Tags list should only have one entry
357 tags, err := storage.ListTags("myapp")
358 require.NoError(t, err)
359 assert.Len(t, tags, 1)
360}
361
362func TestManifestStorage_LargeManifest(t *testing.T) {
363 tmpDir := t.TempDir()
364 log := testLogger()
365
366 storage, err := NewManifestStorage(tmpDir, log)
367 require.NoError(t, err)
368
369 // Create a large manifest (simulating one with many layers)
370 largeManifest := make([]byte, 1024*1024) // 1MB
371 for i := range largeManifest {
372 largeManifest[i] = byte('a' + (i % 26))
373 }
374
375 contentType := "application/vnd.docker.distribution.manifest.v2+json"
376
377 err = storage.PutManifest("large-app", "latest", contentType, largeManifest)
378 require.NoError(t, err)
379
380 data, _, err := storage.GetManifest("large-app", "latest")
381 require.NoError(t, err)
382 assert.Equal(t, largeManifest, data)
383}
384
385func TestManifestStorage_SpecialCharactersInReference(t *testing.T) {
386 tmpDir := t.TempDir()
387 log := testLogger()
388
389 storage, err := NewManifestStorage(tmpDir, log)
390 require.NoError(t, err)
391
392 manifestData := []byte(`{"schemaVersion": 2}`)
393 contentType := "application/vnd.docker.distribution.manifest.v2+json"
394
395 // SHA256 digests are valid references
396 reference := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
397
398 err = storage.PutManifest("myapp", reference, contentType, manifestData)
399 require.NoError(t, err)
400
401 data, _, err := storage.GetManifest("myapp", reference)
402 require.NoError(t, err)
403 assert.Equal(t, manifestData, data)
404}
405
406func TestManifestStorage_CompleteWorkflow(t *testing.T) {
407 tmpDir := t.TempDir()
408 log := testLogger()
409
410 storage, err := NewManifestStorage(tmpDir, log)
411 require.NoError(t, err)
412
413 contentType := "application/vnd.docker.distribution.manifest.v2+json"
414
415 // Simulate a complete registry workflow
416 // 1. Push first version
417 v1Data := []byte(`{"schemaVersion": 2, "version": "1.0"}`)
418 err = storage.PutManifest("myorg/myapp", "v1.0", contentType, v1Data)
419 require.NoError(t, err)
420 err = storage.PutManifest("myorg/myapp", "latest", contentType, v1Data)
421 require.NoError(t, err)
422
423 // 2. Push second version
424 v2Data := []byte(`{"schemaVersion": 2, "version": "2.0"}`)
425 err = storage.PutManifest("myorg/myapp", "v2.0", contentType, v2Data)
426 require.NoError(t, err)
427 err = storage.PutManifest("myorg/myapp", "latest", contentType, v2Data)
428 require.NoError(t, err)
429
430 // 3. Verify tags
431 tags, err := storage.ListTags("myorg/myapp")
432 require.NoError(t, err)
433 assert.Len(t, tags, 3)
434
435 // 4. Verify latest points to v2
436 data, _, err := storage.GetManifest("myorg/myapp", "latest")
437 require.NoError(t, err)
438 assert.Equal(t, v2Data, data)
439
440 // 5. Verify v1 is still accessible
441 data, _, err = storage.GetManifest("myorg/myapp", "v1.0")
442 require.NoError(t, err)
443 assert.Equal(t, v1Data, data)
444
445 // 6. Delete old version
446 err = storage.DeleteManifest("myorg/myapp", "v1.0")
447 require.NoError(t, err)
448
449 // 7. Verify tags updated
450 tags, err = storage.ListTags("myorg/myapp")
451 require.NoError(t, err)
452 assert.Len(t, tags, 2)
453 assert.NotContains(t, tags, "v1.0")
454
455 // 8. List repositories
456 repos, err := storage.ListRepositories()
457 require.NoError(t, err)
458 assert.Contains(t, repos, "myorg/myapp")
459}
460
461func TestManifestStorage_UpdateTagsList_CleansLegacyDigestEntries(t *testing.T) {
462 tmpDir := t.TempDir()
463 log := testLogger()
464
465 storage, err := NewManifestStorage(tmpDir, log)
466 require.NoError(t, err)
467
468 manifestData := []byte(`{"schemaVersion": 2}`)
469 contentType := "application/vnd.docker.distribution.manifest.v2+json"
470
471 // Create initial tags
472 err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
473 require.NoError(t, err)
474 err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
475 require.NoError(t, err)
476
477 // Manually inject digest entries into tags.json to simulate legacy data
478 tagsPath := filepath.Join(tmpDir, "repositories", "myapp", "tags.json")
479
480 // Write the contaminated tags list directly
481 err = os.WriteFile(tagsPath, []byte(`["v1.0","v2.0","sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]`), 0600)
482 require.NoError(t, err)
483
484 // Verify tags.json contains digest entries
485 tagsBefore, err := storage.ListTags("myapp")
486 require.NoError(t, err)
487 require.Len(t, tagsBefore, 4, "Should have 4 entries (2 tags + 2 digests)")
488
489 // Add a new tag - this should trigger cleanup of legacy digest entries
490 err = storage.PutManifest("myapp", "v3.0", contentType, manifestData)
491 require.NoError(t, err)
492
493 // Verify that digest entries were cleaned up
494 tagsAfter, err := storage.ListTags("myapp")
495 require.NoError(t, err)
496 assert.Len(t, tagsAfter, 3, "Should have 3 entries (only tags, digests removed)")
497 assert.Contains(t, tagsAfter, "v1.0")
498 assert.Contains(t, tagsAfter, "v2.0")
499 assert.Contains(t, tagsAfter, "v3.0")
500 assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
501 assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
502}
503
504func TestManifestStorage_RemoveFromTagsList_CleansLegacyDigestEntries(t *testing.T) {
505 tmpDir := t.TempDir()
506 log := testLogger()
507
508 storage, err := NewManifestStorage(tmpDir, log)
509 require.NoError(t, err)
510
511 manifestData := []byte(`{"schemaVersion": 2}`)
512 contentType := "application/vnd.docker.distribution.manifest.v2+json"
513
514 // Create initial tags
515 err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
516 require.NoError(t, err)
517 err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
518 require.NoError(t, err)
519 err = storage.PutManifest("myapp", "v3.0", contentType, manifestData)
520 require.NoError(t, err)
521
522 // Manually inject digest entries into tags.json to simulate legacy data
523 tagsPath := filepath.Join(tmpDir, "repositories", "myapp", "tags.json")
524 err = os.WriteFile(tagsPath, []byte(`["v1.0","v2.0","v3.0","sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]`), 0600)
525 require.NoError(t, err)
526
527 // Verify tags.json contains digest entries
528 tagsBefore, err := storage.ListTags("myapp")
529 require.NoError(t, err)
530 require.Len(t, tagsBefore, 5, "Should have 5 entries (3 tags + 2 digests)")
531
532 // Delete a tag - this should trigger cleanup of legacy digest entries
533 err = storage.DeleteManifest("myapp", "v2.0")
534 require.NoError(t, err)
535
536 // Verify that digest entries were cleaned up and v2.0 was removed
537 tagsAfter, err := storage.ListTags("myapp")
538 require.NoError(t, err)
539 assert.Len(t, tagsAfter, 2, "Should have 2 entries (only remaining tags, digests removed)")
540 assert.Contains(t, tagsAfter, "v1.0")
541 assert.Contains(t, tagsAfter, "v3.0")
542 assert.NotContains(t, tagsAfter, "v2.0")
543 assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
544 assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
545}