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