1package filesystem
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7 "time"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11)
12
13func TestNewManifestStorage(t *testing.T) {
14 tmpDir := t.TempDir()
15 log := testLogger()
16
17 storage, err := NewManifestStorage(tmpDir, log)
18
19 require.NoError(t, err)
20 assert.NotNil(t, storage)
21
22 // Verify repositories directory was created
23 assert.DirExists(t, filepath.Join(tmpDir, "repositories"))
24}
25
26func TestNewManifestStorage_InvalidPath(t *testing.T) {
27 log := testLogger()
28
29 _, err := NewManifestStorage("/nonexistent/deeply/nested/path/that/should/fail", log)
30
31 assert.Error(t, err)
32}
33
34func TestManifestStorage_PutAndGetManifest(t *testing.T) {
35 tmpDir := t.TempDir()
36 log := testLogger()
37
38 storage, err := NewManifestStorage(tmpDir, log)
39 require.NoError(t, err)
40
41 manifestData := []byte(`{"schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json"}`)
42 contentType := "application/vnd.docker.distribution.manifest.v2+json"
43
44 // Put manifest
45 err = storage.PutManifest("myapp", "latest", contentType, manifestData)
46 require.NoError(t, err)
47
48 // Get manifest
49 data, ct, err := storage.GetManifest("myapp", "latest")
50 require.NoError(t, err)
51
52 assert.Equal(t, manifestData, data)
53 assert.Equal(t, contentType, ct)
54}
55
56func TestManifestStorage_GetManifest_NotFound(t *testing.T) {
57 tmpDir := t.TempDir()
58 log := testLogger()
59
60 storage, err := NewManifestStorage(tmpDir, log)
61 require.NoError(t, err)
62
63 data, ct, err := storage.GetManifest("notexists", "latest")
64
65 assert.Error(t, err)
66 assert.Nil(t, data)
67 assert.Empty(t, ct)
68 assert.Contains(t, err.Error(), "manifest not found")
69}
70
71func TestManifestStorage_GetManifestModTime(t *testing.T) {
72 tmpDir := t.TempDir()
73 log := testLogger()
74
75 storage, err := NewManifestStorage(tmpDir, log)
76 require.NoError(t, err)
77
78 manifestData := []byte(`{"schemaVersion": 2}`)
79 contentType := "application/vnd.docker.distribution.manifest.v2+json"
80
81 err = storage.PutManifest("myapp", "latest", contentType, manifestData)
82 require.NoError(t, err)
83
84 modTime, err := storage.GetManifestModTime("myapp", "latest")
85 require.NoError(t, err)
86
87 manifestPath := filepath.Join(tmpDir, "repositories", "myapp", "manifests", "latest")
88 info, err := os.Stat(manifestPath)
89 require.NoError(t, err)
90
91 assert.WithinDuration(t, info.ModTime(), modTime, time.Second)
92}
93
94func TestManifestStorage_GetManifestModTime_NotFound(t *testing.T) {
95 tmpDir := t.TempDir()
96 log := testLogger()
97
98 storage, err := NewManifestStorage(tmpDir, log)
99 require.NoError(t, err)
100
101 _, err = storage.GetManifestModTime("notexists", "latest")
102
103 assert.Error(t, err)
104 assert.Contains(t, err.Error(), "manifest not found")
105}
106
107func TestManifestStorage_GetManifest_FallbackContentType(t *testing.T) {
108 tmpDir := t.TempDir()
109 log := testLogger()
110
111 storage, err := NewManifestStorage(tmpDir, log)
112 require.NoError(t, err)
113
114 // Create manifest file directly without content type file
115 manifestDir := filepath.Join(tmpDir, "repositories", "myapp", "manifests")
116 err = os.MkdirAll(manifestDir, 0750)
117 require.NoError(t, err)
118
119 manifestData := []byte(`{"schemaVersion": 2}`)
120 err = os.WriteFile(filepath.Join(manifestDir, "latest"), manifestData, 0600)
121 require.NoError(t, err)
122
123 // Get manifest should use fallback content type
124 data, ct, err := storage.GetManifest("myapp", "latest")
125 require.NoError(t, err)
126
127 assert.Equal(t, manifestData, data)
128 assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", ct)
129}
130
131func TestManifestStorage_DeleteManifest(t *testing.T) {
132 tmpDir := t.TempDir()
133 log := testLogger()
134
135 storage, err := NewManifestStorage(tmpDir, log)
136 require.NoError(t, err)
137
138 manifestData := []byte(`{"schemaVersion": 2}`)
139 contentType := "application/vnd.docker.distribution.manifest.v2+json"
140
141 // Put manifest
142 err = storage.PutManifest("myapp", "latest", contentType, manifestData)
143 require.NoError(t, err)
144
145 // Verify it exists
146 _, _, err = storage.GetManifest("myapp", "latest")
147 require.NoError(t, err)
148
149 // Delete manifest
150 err = storage.DeleteManifest("myapp", "latest")
151 require.NoError(t, err)
152
153 // Verify it's gone
154 _, _, err = storage.GetManifest("myapp", "latest")
155 assert.Error(t, err)
156}
157
158func TestManifestStorage_DeleteManifest_NotFound(t *testing.T) {
159 tmpDir := t.TempDir()
160 log := testLogger()
161
162 storage, err := NewManifestStorage(tmpDir, log)
163 require.NoError(t, err)
164
165 err = storage.DeleteManifest("notexists", "latest")
166
167 assert.Error(t, err)
168 assert.Contains(t, err.Error(), "manifest not found")
169}
170
171func TestManifestStorage_ListTags(t *testing.T) {
172 tmpDir := t.TempDir()
173 log := testLogger()
174
175 storage, err := NewManifestStorage(tmpDir, log)
176 require.NoError(t, err)
177
178 manifestData := []byte(`{"schemaVersion": 2}`)
179 contentType := "application/vnd.docker.distribution.manifest.v2+json"
180
181 // Put multiple manifests
182 err = storage.PutManifest("myapp", "latest", contentType, manifestData)
183 require.NoError(t, err)
184 err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
185 require.NoError(t, err)
186 err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
187 require.NoError(t, err)
188
189 // List tags
190 tags, err := storage.ListTags("myapp")
191 require.NoError(t, err)
192
193 assert.Len(t, tags, 3)
194 assert.Contains(t, tags, "latest")
195 assert.Contains(t, tags, "v1.0")
196 assert.Contains(t, tags, "v2.0")
197}
198
199func TestManifestStorage_ListTags_Empty(t *testing.T) {
200 tmpDir := t.TempDir()
201 log := testLogger()
202
203 storage, err := NewManifestStorage(tmpDir, log)
204 require.NoError(t, err)
205
206 // List tags for non-existent repo
207 tags, err := storage.ListTags("notexists")
208 require.NoError(t, err)
209
210 assert.Empty(t, tags)
211}
212
213func TestManifestStorage_ListTags_SkipsDigestReference(t *testing.T) {
214 tmpDir := t.TempDir()
215 log := testLogger()
216
217 storage, err := NewManifestStorage(tmpDir, log)
218 require.NoError(t, err)
219
220 manifestData := []byte(`{"schemaVersion": 2}`)
221 contentType := "application/vnd.docker.distribution.manifest.v2+json"
222
223 err = storage.PutManifest("myapp", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", contentType, manifestData)
224 require.NoError(t, err)
225
226 tags, err := storage.ListTags("myapp")
227 require.NoError(t, err)
228
229 assert.Empty(t, tags)
230}
231
232func TestManifestStorage_ListTags_AfterDelete(t *testing.T) {
233 tmpDir := t.TempDir()
234 log := testLogger()
235
236 storage, err := NewManifestStorage(tmpDir, log)
237 require.NoError(t, err)
238
239 manifestData := []byte(`{"schemaVersion": 2}`)
240 contentType := "application/vnd.docker.distribution.manifest.v2+json"
241
242 // Put manifests
243 err = storage.PutManifest("myapp", "latest", contentType, manifestData)
244 require.NoError(t, err)
245 err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
246 require.NoError(t, err)
247
248 // Delete one
249 err = storage.DeleteManifest("myapp", "v1.0")
250 require.NoError(t, err)
251
252 // List tags
253 tags, err := storage.ListTags("myapp")
254 require.NoError(t, err)
255
256 assert.Len(t, tags, 1)
257 assert.Contains(t, tags, "latest")
258 assert.NotContains(t, tags, "v1.0")
259}
260
261func TestManifestStorage_ListRepositories(t *testing.T) {
262 tmpDir := t.TempDir()
263 log := testLogger()
264
265 storage, err := NewManifestStorage(tmpDir, log)
266 require.NoError(t, err)
267
268 manifestData := []byte(`{"schemaVersion": 2}`)
269 contentType := "application/vnd.docker.distribution.manifest.v2+json"
270
271 // Put manifests for different repos
272 err = storage.PutManifest("app1", "latest", contentType, manifestData)
273 require.NoError(t, err)
274 err = storage.PutManifest("app2", "latest", contentType, manifestData)
275 require.NoError(t, err)
276 err = storage.PutManifest("org/app3", "latest", contentType, manifestData)
277 require.NoError(t, err)
278
279 // List repositories
280 repos, err := storage.ListRepositories()
281 require.NoError(t, err)
282
283 assert.Len(t, repos, 3)
284 assert.Contains(t, repos, "app1")
285 assert.Contains(t, repos, "app2")
286 assert.Contains(t, repos, "org/app3")
287}
288
289func TestManifestStorage_ListRepositories_Empty(t *testing.T) {
290 tmpDir := t.TempDir()
291 log := testLogger()
292
293 storage, err := NewManifestStorage(tmpDir, log)
294 require.NoError(t, err)
295
296 repos, err := storage.ListRepositories()
297 require.NoError(t, err)
298
299 assert.Empty(t, repos)
300}
301
302func TestManifestStorage_NestedRepositoryNames(t *testing.T) {
303 tmpDir := t.TempDir()
304 log := testLogger()
305
306 storage, err := NewManifestStorage(tmpDir, log)
307 require.NoError(t, err)
308
309 manifestData := []byte(`{"schemaVersion": 2}`)
310 contentType := "application/vnd.docker.distribution.manifest.v2+json"
311
312 tests := []struct {
313 name string
314 reference string
315 }{
316 {"simple", "latest"},
317 {"org/project", "v1.0"},
318 {"org/team/project", "v2.0"},
319 {"deeply/nested/repo/name", "sha256:abc123"},
320 }
321
322 for _, tt := range tests {
323 t.Run(tt.name, func(t *testing.T) {
324 err := storage.PutManifest(tt.name, tt.reference, contentType, manifestData)
325 require.NoError(t, err)
326
327 data, ct, err := storage.GetManifest(tt.name, tt.reference)
328 require.NoError(t, err)
329 assert.Equal(t, manifestData, data)
330 assert.Equal(t, contentType, ct)
331 })
332 }
333}
334
335func TestManifestStorage_ContentTypes(t *testing.T) {
336 tmpDir := t.TempDir()
337 log := testLogger()
338
339 storage, err := NewManifestStorage(tmpDir, log)
340 require.NoError(t, err)
341
342 tests := []struct {
343 name string
344 contentType string
345 }{
346 {"v2 manifest", "application/vnd.docker.distribution.manifest.v2+json"},
347 {"v1 manifest", "application/vnd.docker.distribution.manifest.v1+json"},
348 {"OCI manifest", "application/vnd.oci.image.manifest.v1+json"},
349 {"OCI index", "application/vnd.oci.image.index.v1+json"},
350 {"manifest list", "application/vnd.docker.distribution.manifest.list.v2+json"},
351 }
352
353 for i, tt := range tests {
354 t.Run(tt.name, func(t *testing.T) {
355 repoName := "test-repo"
356 reference := "tag-" + string(rune('a'+i))
357 manifestData := []byte(`{"schemaVersion": 2}`)
358
359 err := storage.PutManifest(repoName, reference, tt.contentType, manifestData)
360 require.NoError(t, err)
361
362 _, ct, err := storage.GetManifest(repoName, reference)
363 require.NoError(t, err)
364 assert.Equal(t, tt.contentType, ct)
365 })
366 }
367}
368
369func TestManifestStorage_DuplicateTag(t *testing.T) {
370 tmpDir := t.TempDir()
371 log := testLogger()
372
373 storage, err := NewManifestStorage(tmpDir, log)
374 require.NoError(t, err)
375
376 contentType := "application/vnd.docker.distribution.manifest.v2+json"
377
378 // Put first manifest
379 manifestData1 := []byte(`{"schemaVersion": 2, "version": 1}`)
380 err = storage.PutManifest("myapp", "latest", contentType, manifestData1)
381 require.NoError(t, err)
382
383 // Put second manifest with same tag (overwrite)
384 manifestData2 := []byte(`{"schemaVersion": 2, "version": 2}`)
385 err = storage.PutManifest("myapp", "latest", contentType, manifestData2)
386 require.NoError(t, err)
387
388 // Get manifest should return the latest one
389 data, _, err := storage.GetManifest("myapp", "latest")
390 require.NoError(t, err)
391 assert.Equal(t, manifestData2, data)
392
393 // Tags list should only have one entry
394 tags, err := storage.ListTags("myapp")
395 require.NoError(t, err)
396 assert.Len(t, tags, 1)
397}
398
399func TestManifestStorage_LargeManifest(t *testing.T) {
400 tmpDir := t.TempDir()
401 log := testLogger()
402
403 storage, err := NewManifestStorage(tmpDir, log)
404 require.NoError(t, err)
405
406 // Create a large manifest (simulating one with many layers)
407 largeManifest := make([]byte, 1024*1024) // 1MB
408 for i := range largeManifest {
409 largeManifest[i] = byte('a' + (i % 26))
410 }
411
412 contentType := "application/vnd.docker.distribution.manifest.v2+json"
413
414 err = storage.PutManifest("large-app", "latest", contentType, largeManifest)
415 require.NoError(t, err)
416
417 data, _, err := storage.GetManifest("large-app", "latest")
418 require.NoError(t, err)
419 assert.Equal(t, largeManifest, data)
420}
421
422func TestManifestStorage_SpecialCharactersInReference(t *testing.T) {
423 tmpDir := t.TempDir()
424 log := testLogger()
425
426 storage, err := NewManifestStorage(tmpDir, log)
427 require.NoError(t, err)
428
429 manifestData := []byte(`{"schemaVersion": 2}`)
430 contentType := "application/vnd.docker.distribution.manifest.v2+json"
431
432 // SHA256 digests are valid references
433 reference := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
434
435 err = storage.PutManifest("myapp", reference, contentType, manifestData)
436 require.NoError(t, err)
437
438 data, _, err := storage.GetManifest("myapp", reference)
439 require.NoError(t, err)
440 assert.Equal(t, manifestData, data)
441}
442
443func TestManifestStorage_CompleteWorkflow(t *testing.T) {
444 tmpDir := t.TempDir()
445 log := testLogger()
446
447 storage, err := NewManifestStorage(tmpDir, log)
448 require.NoError(t, err)
449
450 contentType := "application/vnd.docker.distribution.manifest.v2+json"
451
452 // Simulate a complete registry workflow
453 // 1. Push first version
454 v1Data := []byte(`{"schemaVersion": 2, "version": "1.0"}`)
455 err = storage.PutManifest("myorg/myapp", "v1.0", contentType, v1Data)
456 require.NoError(t, err)
457 err = storage.PutManifest("myorg/myapp", "latest", contentType, v1Data)
458 require.NoError(t, err)
459
460 // 2. Push second version
461 v2Data := []byte(`{"schemaVersion": 2, "version": "2.0"}`)
462 err = storage.PutManifest("myorg/myapp", "v2.0", contentType, v2Data)
463 require.NoError(t, err)
464 err = storage.PutManifest("myorg/myapp", "latest", contentType, v2Data)
465 require.NoError(t, err)
466
467 // 3. Verify tags
468 tags, err := storage.ListTags("myorg/myapp")
469 require.NoError(t, err)
470 assert.Len(t, tags, 3)
471
472 // 4. Verify latest points to v2
473 data, _, err := storage.GetManifest("myorg/myapp", "latest")
474 require.NoError(t, err)
475 assert.Equal(t, v2Data, data)
476
477 // 5. Verify v1 is still accessible
478 data, _, err = storage.GetManifest("myorg/myapp", "v1.0")
479 require.NoError(t, err)
480 assert.Equal(t, v1Data, data)
481
482 // 6. Delete old version
483 err = storage.DeleteManifest("myorg/myapp", "v1.0")
484 require.NoError(t, err)
485
486 // 7. Verify tags updated
487 tags, err = storage.ListTags("myorg/myapp")
488 require.NoError(t, err)
489 assert.Len(t, tags, 2)
490 assert.NotContains(t, tags, "v1.0")
491
492 // 8. List repositories
493 repos, err := storage.ListRepositories()
494 require.NoError(t, err)
495 assert.Contains(t, repos, "myorg/myapp")
496}
497
498func TestManifestStorage_UpdateTagsList_CleansLegacyDigestEntries(t *testing.T) {
499 tmpDir := t.TempDir()
500 log := testLogger()
501
502 storage, err := NewManifestStorage(tmpDir, log)
503 require.NoError(t, err)
504
505 manifestData := []byte(`{"schemaVersion": 2}`)
506 contentType := "application/vnd.docker.distribution.manifest.v2+json"
507
508 // Create initial tags
509 err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
510 require.NoError(t, err)
511 err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
512 require.NoError(t, err)
513
514 // Manually inject digest entries into tags.json to simulate legacy data
515 tagsPath := filepath.Join(tmpDir, "repositories", "myapp", "tags.json")
516
517 // Write the contaminated tags list directly
518 err = os.WriteFile(tagsPath, []byte(`["v1.0","v2.0","sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]`), 0600)
519 require.NoError(t, err)
520
521 // Verify tags.json contains digest entries
522 tagsBefore, err := storage.ListTags("myapp")
523 require.NoError(t, err)
524 require.Len(t, tagsBefore, 4, "Should have 4 entries (2 tags + 2 digests)")
525
526 // Add a new tag - this should trigger cleanup of legacy digest entries
527 err = storage.PutManifest("myapp", "v3.0", contentType, manifestData)
528 require.NoError(t, err)
529
530 // Verify that digest entries were cleaned up
531 tagsAfter, err := storage.ListTags("myapp")
532 require.NoError(t, err)
533 assert.Len(t, tagsAfter, 3, "Should have 3 entries (only tags, digests removed)")
534 assert.Contains(t, tagsAfter, "v1.0")
535 assert.Contains(t, tagsAfter, "v2.0")
536 assert.Contains(t, tagsAfter, "v3.0")
537 assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
538 assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
539}
540
541func TestManifestStorage_RemoveFromTagsList_CleansLegacyDigestEntries(t *testing.T) {
542 tmpDir := t.TempDir()
543 log := testLogger()
544
545 storage, err := NewManifestStorage(tmpDir, log)
546 require.NoError(t, err)
547
548 manifestData := []byte(`{"schemaVersion": 2}`)
549 contentType := "application/vnd.docker.distribution.manifest.v2+json"
550
551 // Create initial tags
552 err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
553 require.NoError(t, err)
554 err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
555 require.NoError(t, err)
556 err = storage.PutManifest("myapp", "v3.0", contentType, manifestData)
557 require.NoError(t, err)
558
559 // Manually inject digest entries into tags.json to simulate legacy data
560 tagsPath := filepath.Join(tmpDir, "repositories", "myapp", "tags.json")
561 err = os.WriteFile(tagsPath, []byte(`["v1.0","v2.0","v3.0","sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]`), 0600)
562 require.NoError(t, err)
563
564 // Verify tags.json contains digest entries
565 tagsBefore, err := storage.ListTags("myapp")
566 require.NoError(t, err)
567 require.Len(t, tagsBefore, 5, "Should have 5 entries (3 tags + 2 digests)")
568
569 // Delete a tag - this should trigger cleanup of legacy digest entries
570 err = storage.DeleteManifest("myapp", "v2.0")
571 require.NoError(t, err)
572
573 // Verify that digest entries were cleaned up and v2.0 was removed
574 tagsAfter, err := storage.ListTags("myapp")
575 require.NoError(t, err)
576 assert.Len(t, tagsAfter, 2, "Should have 2 entries (only remaining tags, digests removed)")
577 assert.Contains(t, tagsAfter, "v1.0")
578 assert.Contains(t, tagsAfter, "v3.0")
579 assert.NotContains(t, tagsAfter, "v2.0")
580 assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
581 assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
582}