1package manifest
2
3import (
4 "encoding/json"
5 "testing"
6
7 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/require"
9)
10
11func TestOCIManifest_JSON(t *testing.T) {
12 tests := []struct {
13 name string
14 manifest OCIManifest
15 wantJSON string
16 }{
17 {
18 name: "basic OCI manifest",
19 manifest: OCIManifest{
20 SchemaVersion: 2,
21 MediaType: "application/vnd.oci.image.manifest.v1+json",
22 Config: OCIDescriptor{
23 MediaType: "application/vnd.oci.image.config.v1+json",
24 Digest: "sha256:abc123",
25 Size: 1234,
26 },
27 Layers: []OCIDescriptor{
28 {
29 MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
30 Digest: "sha256:layer1",
31 Size: 5678,
32 },
33 },
34 },
35 wantJSON: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:abc123","size":1234},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:layer1","size":5678}]}`,
36 },
37 {
38 name: "OCI manifest with annotations",
39 manifest: OCIManifest{
40 SchemaVersion: 2,
41 MediaType: "application/vnd.oci.image.manifest.v1+json",
42 Config: OCIDescriptor{
43 MediaType: "application/vnd.oci.image.config.v1+json",
44 Digest: "sha256:config123",
45 Size: 1024,
46 },
47 Layers: []OCIDescriptor{
48 {
49 MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
50 Digest: "sha256:layer1",
51 Size: 2048,
52 },
53 },
54 Annotations: map[string]string{
55 "version": "1.0.0",
56 "created-by": "gordon",
57 },
58 },
59 wantJSON: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:config123","size":1024},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:layer1","size":2048}],"annotations":{"created-by":"gordon","version":"1.0.0"}}`,
60 },
61 {
62 name: "OCI manifest with multiple layers",
63 manifest: OCIManifest{
64 SchemaVersion: 2,
65 MediaType: "application/vnd.oci.image.manifest.v1+json",
66 Config: OCIDescriptor{
67 MediaType: "application/vnd.oci.image.config.v1+json",
68 Digest: "sha256:config456",
69 Size: 512,
70 },
71 Layers: []OCIDescriptor{
72 {
73 MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
74 Digest: "sha256:layer1",
75 Size: 1024,
76 },
77 {
78 MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
79 Digest: "sha256:layer2",
80 Size: 2048,
81 },
82 {
83 MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
84 Digest: "sha256:layer3",
85 Size: 4096,
86 },
87 },
88 },
89 wantJSON: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:config456","size":512},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:layer1","size":1024},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:layer2","size":2048},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:layer3","size":4096}]}`,
90 },
91 }
92
93 for _, tt := range tests {
94 t.Run(tt.name, func(t *testing.T) {
95 // Test marshaling
96 gotJSON, err := json.Marshal(tt.manifest)
97 require.NoError(t, err)
98 assert.JSONEq(t, tt.wantJSON, string(gotJSON))
99
100 // Test unmarshaling
101 var gotManifest OCIManifest
102 err = json.Unmarshal([]byte(tt.wantJSON), &gotManifest)
103 require.NoError(t, err)
104 assert.Equal(t, tt.manifest, gotManifest)
105 })
106 }
107}
108
109func TestDockerManifest_JSON(t *testing.T) {
110 tests := []struct {
111 name string
112 manifest DockerManifest
113 wantJSON string
114 }{
115 {
116 name: "basic Docker manifest",
117 manifest: DockerManifest{
118 SchemaVersion: 2,
119 MediaType: "application/vnd.docker.distribution.manifest.v2+json",
120 Config: OCIDescriptor{
121 MediaType: "application/vnd.docker.container.image.v1+json",
122 Digest: "sha256:docker123",
123 Size: 1500,
124 },
125 Layers: []OCIDescriptor{
126 {
127 MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
128 Digest: "sha256:dockerlayer1",
129 Size: 7890,
130 },
131 },
132 },
133 wantJSON: `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:docker123","size":1500},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:dockerlayer1","size":7890}]}`,
134 },
135 {
136 name: "Docker manifest with empty layers",
137 manifest: DockerManifest{
138 SchemaVersion: 2,
139 MediaType: "application/vnd.docker.distribution.manifest.v2+json",
140 Config: OCIDescriptor{
141 MediaType: "application/vnd.docker.container.image.v1+json",
142 Digest: "sha256:empty123",
143 Size: 256,
144 },
145 Layers: []OCIDescriptor{},
146 },
147 wantJSON: `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:empty123","size":256},"layers":[]}`,
148 },
149 }
150
151 for _, tt := range tests {
152 t.Run(tt.name, func(t *testing.T) {
153 // Test marshaling
154 gotJSON, err := json.Marshal(tt.manifest)
155 require.NoError(t, err)
156 assert.JSONEq(t, tt.wantJSON, string(gotJSON))
157
158 // Test unmarshaling
159 var gotManifest DockerManifest
160 err = json.Unmarshal([]byte(tt.wantJSON), &gotManifest)
161 require.NoError(t, err)
162 assert.Equal(t, tt.manifest, gotManifest)
163 })
164 }
165}
166
167func TestOCIDescriptor_JSON(t *testing.T) {
168 tests := []struct {
169 name string
170 descriptor OCIDescriptor
171 wantJSON string
172 }{
173 {
174 name: "basic descriptor",
175 descriptor: OCIDescriptor{
176 MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
177 Digest: "sha256:descriptor123",
178 Size: 9999,
179 },
180 wantJSON: `{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:descriptor123","size":9999}`,
181 },
182 {
183 name: "descriptor with annotations",
184 descriptor: OCIDescriptor{
185 MediaType: "application/vnd.oci.image.config.v1+json",
186 Digest: "sha256:annotated456",
187 Size: 1111,
188 Annotations: map[string]string{
189 "title": "Test Layer",
190 "description": "A test layer for unit testing",
191 },
192 },
193 wantJSON: `{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:annotated456","size":1111,"annotations":{"description":"A test layer for unit testing","title":"Test Layer"}}`,
194 },
195 {
196 name: "descriptor with zero size",
197 descriptor: OCIDescriptor{
198 MediaType: "application/vnd.oci.empty.v1+json",
199 Digest: "sha256:empty",
200 Size: 0,
201 },
202 wantJSON: `{"mediaType":"application/vnd.oci.empty.v1+json","digest":"sha256:empty","size":0}`,
203 },
204 }
205
206 for _, tt := range tests {
207 t.Run(tt.name, func(t *testing.T) {
208 // Test marshaling
209 gotJSON, err := json.Marshal(tt.descriptor)
210 require.NoError(t, err)
211 assert.JSONEq(t, tt.wantJSON, string(gotJSON))
212
213 // Test unmarshaling
214 var gotDescriptor OCIDescriptor
215 err = json.Unmarshal([]byte(tt.wantJSON), &gotDescriptor)
216 require.NoError(t, err)
217 assert.Equal(t, tt.descriptor, gotDescriptor)
218 })
219 }
220}
221
222func TestManifestList_JSON(t *testing.T) {
223 tests := []struct {
224 name string
225 manifestList ManifestList
226 wantJSON string
227 }{
228 {
229 name: "basic manifest list",
230 manifestList: ManifestList{
231 SchemaVersion: 2,
232 MediaType: "application/vnd.oci.image.index.v1+json",
233 Manifests: []ManifestDescriptor{
234 {
235 MediaType: "application/vnd.oci.image.manifest.v1+json",
236 Digest: "sha256:manifest1",
237 Size: 2222,
238 Platform: &Platform{
239 Architecture: "amd64",
240 OS: "linux",
241 },
242 },
243 },
244 },
245 wantJSON: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:manifest1","size":2222,"platform":{"architecture":"amd64","os":"linux"}}]}`,
246 },
247 {
248 name: "manifest list with annotations",
249 manifestList: ManifestList{
250 SchemaVersion: 2,
251 MediaType: "application/vnd.oci.image.index.v1+json",
252 Manifests: []ManifestDescriptor{
253 {
254 MediaType: "application/vnd.oci.image.manifest.v1+json",
255 Digest: "sha256:manifest2",
256 Size: 3333,
257 Platform: &Platform{
258 Architecture: "arm64",
259 OS: "linux",
260 Variant: "v8",
261 },
262 Annotations: map[string]string{
263 "io.github.gordon.platform": "arm64-v8",
264 },
265 },
266 },
267 Annotations: map[string]string{
268 "version": "2.0.0",
269 "source": "github.com/user/repo",
270 },
271 },
272 wantJSON: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:manifest2","size":3333,"platform":{"architecture":"arm64","os":"linux","variant":"v8"},"annotations":{"io.github.gordon.platform":"arm64-v8"}}],"annotations":{"source":"github.com/user/repo","version":"2.0.0"}}`,
273 },
274 {
275 name: "multi-platform manifest list",
276 manifestList: ManifestList{
277 SchemaVersion: 2,
278 MediaType: "application/vnd.docker.distribution.manifest.list.v2+json",
279 Manifests: []ManifestDescriptor{
280 {
281 MediaType: "application/vnd.docker.distribution.manifest.v2+json",
282 Digest: "sha256:linux-amd64",
283 Size: 4444,
284 Platform: &Platform{
285 Architecture: "amd64",
286 OS: "linux",
287 },
288 },
289 {
290 MediaType: "application/vnd.docker.distribution.manifest.v2+json",
291 Digest: "sha256:linux-arm64",
292 Size: 5555,
293 Platform: &Platform{
294 Architecture: "arm64",
295 OS: "linux",
296 },
297 },
298 {
299 MediaType: "application/vnd.docker.distribution.manifest.v2+json",
300 Digest: "sha256:windows-amd64",
301 Size: 6666,
302 Platform: &Platform{
303 Architecture: "amd64",
304 OS: "windows",
305 OSVersion: "10.0.17763.1234",
306 },
307 },
308 },
309 },
310 wantJSON: `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.list.v2+json","manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:linux-amd64","size":4444,"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:linux-arm64","size":5555,"platform":{"architecture":"arm64","os":"linux"}},{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:windows-amd64","size":6666,"platform":{"architecture":"amd64","os":"windows","os.version":"10.0.17763.1234"}}]}`,
311 },
312 }
313
314 for _, tt := range tests {
315 t.Run(tt.name, func(t *testing.T) {
316 // Test marshaling
317 gotJSON, err := json.Marshal(tt.manifestList)
318 require.NoError(t, err)
319 assert.JSONEq(t, tt.wantJSON, string(gotJSON))
320
321 // Test unmarshaling
322 var gotManifestList ManifestList
323 err = json.Unmarshal([]byte(tt.wantJSON), &gotManifestList)
324 require.NoError(t, err)
325 assert.Equal(t, tt.manifestList, gotManifestList)
326 })
327 }
328}
329
330func TestPlatform_JSON(t *testing.T) {
331 tests := []struct {
332 name string
333 platform Platform
334 wantJSON string
335 }{
336 {
337 name: "basic platform",
338 platform: Platform{
339 Architecture: "amd64",
340 OS: "linux",
341 },
342 wantJSON: `{"architecture":"amd64","os":"linux"}`,
343 },
344 {
345 name: "platform with variant",
346 platform: Platform{
347 Architecture: "arm",
348 OS: "linux",
349 Variant: "v7",
350 },
351 wantJSON: `{"architecture":"arm","os":"linux","variant":"v7"}`,
352 },
353 {
354 name: "windows platform with version",
355 platform: Platform{
356 Architecture: "amd64",
357 OS: "windows",
358 OSVersion: "10.0.14393.1066",
359 },
360 wantJSON: `{"architecture":"amd64","os":"windows","os.version":"10.0.14393.1066"}`,
361 },
362 {
363 name: "platform with features",
364 platform: Platform{
365 Architecture: "amd64",
366 OS: "linux",
367 OSFeatures: []string{"feature1", "feature2"},
368 },
369 wantJSON: `{"architecture":"amd64","os":"linux","os.features":["feature1","feature2"]}`,
370 },
371 {
372 name: "complete platform",
373 platform: Platform{
374 Architecture: "s390x",
375 OS: "linux",
376 OSVersion: "4.15.0",
377 OSFeatures: []string{"capability1", "capability2", "capability3"},
378 Variant: "custom",
379 },
380 wantJSON: `{"architecture":"s390x","os":"linux","os.version":"4.15.0","os.features":["capability1","capability2","capability3"],"variant":"custom"}`,
381 },
382 {
383 name: "empty platform",
384 platform: Platform{
385 Architecture: "",
386 OS: "",
387 },
388 wantJSON: `{"architecture":"","os":""}`,
389 },
390 }
391
392 for _, tt := range tests {
393 t.Run(tt.name, func(t *testing.T) {
394 // Test marshaling
395 gotJSON, err := json.Marshal(tt.platform)
396 require.NoError(t, err)
397 assert.JSONEq(t, tt.wantJSON, string(gotJSON))
398
399 // Test unmarshaling
400 var gotPlatform Platform
401 err = json.Unmarshal([]byte(tt.wantJSON), &gotPlatform)
402 require.NoError(t, err)
403 assert.Equal(t, tt.platform, gotPlatform)
404 })
405 }
406}
407
408func TestManifestDescriptor_JSON(t *testing.T) {
409 tests := []struct {
410 name string
411 descriptor ManifestDescriptor
412 wantJSON string
413 }{
414 {
415 name: "basic manifest descriptor",
416 descriptor: ManifestDescriptor{
417 MediaType: "application/vnd.oci.image.manifest.v1+json",
418 Digest: "sha256:manifestdesc123",
419 Size: 7777,
420 },
421 wantJSON: `{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:manifestdesc123","size":7777}`,
422 },
423 {
424 name: "manifest descriptor with platform",
425 descriptor: ManifestDescriptor{
426 MediaType: "application/vnd.oci.image.manifest.v1+json",
427 Digest: "sha256:platform123",
428 Size: 8888,
429 Platform: &Platform{
430 Architecture: "riscv64",
431 OS: "linux",
432 },
433 },
434 wantJSON: `{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:platform123","size":8888,"platform":{"architecture":"riscv64","os":"linux"}}`,
435 },
436 {
437 name: "manifest descriptor with annotations",
438 descriptor: ManifestDescriptor{
439 MediaType: "application/vnd.oci.image.manifest.v1+json",
440 Digest: "sha256:annotations123",
441 Size: 9999,
442 Annotations: map[string]string{
443 "io.github.gordon.build": "automated",
444 "version": "3.1.4",
445 },
446 },
447 wantJSON: `{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:annotations123","size":9999,"annotations":{"io.github.gordon.build":"automated","version":"3.1.4"}}`,
448 },
449 {
450 name: "complete manifest descriptor",
451 descriptor: ManifestDescriptor{
452 MediaType: "application/vnd.oci.image.manifest.v1+json",
453 Digest: "sha256:complete123",
454 Size: 12345,
455 Platform: &Platform{
456 Architecture: "ppc64le",
457 OS: "linux",
458 Variant: "power9",
459 },
460 Annotations: map[string]string{
461 "build.date": "2023-01-01T00:00:00Z",
462 "maintainer": "gordon-team",
463 "io.buildah.version": "1.23.0",
464 },
465 },
466 wantJSON: `{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:complete123","size":12345,"platform":{"architecture":"ppc64le","os":"linux","variant":"power9"},"annotations":{"build.date":"2023-01-01T00:00:00Z","io.buildah.version":"1.23.0","maintainer":"gordon-team"}}`,
467 },
468 }
469
470 for _, tt := range tests {
471 t.Run(tt.name, func(t *testing.T) {
472 // Test marshaling
473 gotJSON, err := json.Marshal(tt.descriptor)
474 require.NoError(t, err)
475 assert.JSONEq(t, tt.wantJSON, string(gotJSON))
476
477 // Test unmarshaling
478 var gotDescriptor ManifestDescriptor
479 err = json.Unmarshal([]byte(tt.wantJSON), &gotDescriptor)
480 require.NoError(t, err)
481 assert.Equal(t, tt.descriptor, gotDescriptor)
482 })
483 }
484}
485
486func TestManifestTypes_EdgeCases(t *testing.T) {
487 t.Run("empty annotations should be omitted", func(t *testing.T) {
488 manifest := OCIManifest{
489 SchemaVersion: 2,
490 MediaType: "application/vnd.oci.image.manifest.v1+json",
491 Config: OCIDescriptor{
492 MediaType: "application/vnd.oci.image.config.v1+json",
493 Digest: "sha256:config",
494 Size: 100,
495 },
496 Layers: []OCIDescriptor{},
497 Annotations: map[string]string{}, // Empty map should be omitted
498 }
499
500 jsonData, err := json.Marshal(manifest)
501 require.NoError(t, err)
502
503 // Should not contain "annotations" field
504 assert.NotContains(t, string(jsonData), "annotations")
505 })
506
507 t.Run("nil annotations should be omitted", func(t *testing.T) {
508 manifest := OCIManifest{
509 SchemaVersion: 2,
510 MediaType: "application/vnd.oci.image.manifest.v1+json",
511 Config: OCIDescriptor{
512 MediaType: "application/vnd.oci.image.config.v1+json",
513 Digest: "sha256:config",
514 Size: 100,
515 },
516 Layers: []OCIDescriptor{},
517 // Annotations is nil by default
518 }
519
520 jsonData, err := json.Marshal(manifest)
521 require.NoError(t, err)
522
523 // Should not contain "annotations" field
524 assert.NotContains(t, string(jsonData), "annotations")
525 })
526
527 t.Run("nil platform should be omitted", func(t *testing.T) {
528 descriptor := ManifestDescriptor{
529 MediaType: "application/vnd.oci.image.manifest.v1+json",
530 Digest: "sha256:test",
531 Size: 123,
532 // Platform is nil by default
533 }
534
535 jsonData, err := json.Marshal(descriptor)
536 require.NoError(t, err)
537
538 // Should not contain "platform" field
539 assert.NotContains(t, string(jsonData), "platform")
540 })
541
542 t.Run("empty string fields are preserved", func(t *testing.T) {
543 platform := Platform{
544 Architecture: "",
545 OS: "",
546 OSVersion: "",
547 Variant: "",
548 }
549
550 jsonData, err := json.Marshal(platform)
551 require.NoError(t, err)
552
553 // Empty strings should be preserved
554 assert.Contains(t, string(jsonData), `"architecture":""`)
555 assert.Contains(t, string(jsonData), `"os":""`)
556 })
557}