1package reference
2
3import (
4	_ "crypto/sha256"
5	_ "crypto/sha512"
6	"encoding/json"
7	"strconv"
8	"strings"
9	"testing"
10
11	"github.com/opencontainers/go-digest"
12)
13
14func TestReferenceParse(t *testing.T) {
15	// referenceTestcases is a unified set of testcases for
16	// testing the parsing of references
17	referenceTestcases := []struct {
18		// input is the repository name or name component testcase
19		input string
20		// err is the error expected from Parse, or nil
21		err error
22		// repository is the string representation for the reference
23		repository string
24		// domain is the domain expected in the reference
25		domain string
26		// tag is the tag for the reference
27		tag string
28		// digest is the digest for the reference (enforces digest reference)
29		digest string
30	}{
31		{
32			input:      "test_com",
33			repository: "test_com",
34		},
35		{
36			input:      "test.com:tag",
37			repository: "test.com",
38			tag:        "tag",
39		},
40		{
41			input:      "test.com:5000",
42			repository: "test.com",
43			tag:        "5000",
44		},
45		{
46			input:      "test.com/repo:tag",
47			domain:     "test.com",
48			repository: "test.com/repo",
49			tag:        "tag",
50		},
51		{
52			input:      "test:5000/repo",
53			domain:     "test:5000",
54			repository: "test:5000/repo",
55		},
56		{
57			input:      "test:5000/repo:tag",
58			domain:     "test:5000",
59			repository: "test:5000/repo",
60			tag:        "tag",
61		},
62		{
63			input:      "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
64			domain:     "test:5000",
65			repository: "test:5000/repo",
66			digest:     "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
67		},
68		{
69			input:      "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
70			domain:     "test:5000",
71			repository: "test:5000/repo",
72			tag:        "tag",
73			digest:     "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
74		},
75		{
76			input:      "test:5000/repo",
77			domain:     "test:5000",
78			repository: "test:5000/repo",
79		},
80		{
81			input: "",
82			err:   ErrNameEmpty,
83		},
84		{
85			input: ":justtag",
86			err:   ErrReferenceInvalidFormat,
87		},
88		{
89			input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
90			err:   ErrReferenceInvalidFormat,
91		},
92		{
93			input: "repo@sha256:ffffffffffffffffffffffffffffffffff",
94			err:   digest.ErrDigestInvalidLength,
95		},
96		{
97			input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
98			err:   digest.ErrDigestUnsupported,
99		},
100		{
101			input: "Uppercase:tag",
102			err:   ErrNameContainsUppercase,
103		},
104		// FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes.
105		// See https://github.com/docker/distribution/pull/1778, and https://github.com/docker/docker/pull/20175
106		//{
107		//	input: "Uppercase/lowercase:tag",
108		//	err:   ErrNameContainsUppercase,
109		//},
110		{
111			input: "test:5000/Uppercase/lowercase:tag",
112			err:   ErrNameContainsUppercase,
113		},
114		{
115			input:      "lowercase:Uppercase",
116			repository: "lowercase",
117			tag:        "Uppercase",
118		},
119		{
120			input: strings.Repeat("a/", 128) + "a:tag",
121			err:   ErrNameTooLong,
122		},
123		{
124			input:      strings.Repeat("a/", 127) + "a:tag-puts-this-over-max",
125			domain:     "a",
126			repository: strings.Repeat("a/", 127) + "a",
127			tag:        "tag-puts-this-over-max",
128		},
129		{
130			input: "aa/asdf$$^/aa",
131			err:   ErrReferenceInvalidFormat,
132		},
133		{
134			input:      "sub-dom1.foo.com/bar/baz/quux",
135			domain:     "sub-dom1.foo.com",
136			repository: "sub-dom1.foo.com/bar/baz/quux",
137		},
138		{
139			input:      "sub-dom1.foo.com/bar/baz/quux:some-long-tag",
140			domain:     "sub-dom1.foo.com",
141			repository: "sub-dom1.foo.com/bar/baz/quux",
142			tag:        "some-long-tag",
143		},
144		{
145			input:      "b.gcr.io/test.example.com/my-app:test.example.com",
146			domain:     "b.gcr.io",
147			repository: "b.gcr.io/test.example.com/my-app",
148			tag:        "test.example.com",
149		},
150		{
151			input:      "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode
152			domain:     "xn--n3h.com",
153			repository: "xn--n3h.com/myimage",
154			tag:        "xn--n3h.com",
155		},
156		{
157			input:      "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // ��.com in punycode
158			domain:     "xn--7o8h.com",
159			repository: "xn--7o8h.com/myimage",
160			tag:        "xn--7o8h.com",
161			digest:     "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
162		},
163		{
164			input:      "foo_bar.com:8080",
165			repository: "foo_bar.com",
166			tag:        "8080",
167		},
168		{
169			input:      "foo/foo_bar.com:8080",
170			domain:     "foo",
171			repository: "foo/foo_bar.com",
172			tag:        "8080",
173		},
174	}
175	for _, testcase := range referenceTestcases {
176		failf := func(format string, v ...interface{}) {
177			t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
178			t.Fail()
179		}
180
181		repo, err := Parse(testcase.input)
182		if testcase.err != nil {
183			if err == nil {
184				failf("missing expected error: %v", testcase.err)
185			} else if testcase.err != err {
186				failf("mismatched error: got %v, expected %v", err, testcase.err)
187			}
188			continue
189		} else if err != nil {
190			failf("unexpected parse error: %v", err)
191			continue
192		}
193		if repo.String() != testcase.input {
194			failf("mismatched repo: got %q, expected %q", repo.String(), testcase.input)
195		}
196
197		if named, ok := repo.(Named); ok {
198			if named.Name() != testcase.repository {
199				failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository)
200			}
201			domain, _ := SplitHostname(named)
202			if domain != testcase.domain {
203				failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
204			}
205		} else if testcase.repository != "" || testcase.domain != "" {
206			failf("expected named type, got %T", repo)
207		}
208
209		tagged, ok := repo.(Tagged)
210		if testcase.tag != "" {
211			if ok {
212				if tagged.Tag() != testcase.tag {
213					failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
214				}
215			} else {
216				failf("expected tagged type, got %T", repo)
217			}
218		} else if ok {
219			failf("unexpected tagged type")
220		}
221
222		digested, ok := repo.(Digested)
223		if testcase.digest != "" {
224			if ok {
225				if digested.Digest().String() != testcase.digest {
226					failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
227				}
228			} else {
229				failf("expected digested type, got %T", repo)
230			}
231		} else if ok {
232			failf("unexpected digested type")
233		}
234
235	}
236}
237
238// TestWithNameFailure tests cases where WithName should fail. Cases where it
239// should succeed are covered by TestSplitHostname, below.
240func TestWithNameFailure(t *testing.T) {
241	testcases := []struct {
242		input string
243		err   error
244	}{
245		{
246			input: "",
247			err:   ErrNameEmpty,
248		},
249		{
250			input: ":justtag",
251			err:   ErrReferenceInvalidFormat,
252		},
253		{
254			input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
255			err:   ErrReferenceInvalidFormat,
256		},
257		{
258			input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
259			err:   ErrReferenceInvalidFormat,
260		},
261		{
262			input: strings.Repeat("a/", 128) + "a:tag",
263			err:   ErrNameTooLong,
264		},
265		{
266			input: "aa/asdf$$^/aa",
267			err:   ErrReferenceInvalidFormat,
268		},
269	}
270	for _, testcase := range testcases {
271		failf := func(format string, v ...interface{}) {
272			t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
273			t.Fail()
274		}
275
276		_, err := WithName(testcase.input)
277		if err == nil {
278			failf("no error parsing name. expected: %s", testcase.err)
279		}
280	}
281}
282
283func TestSplitHostname(t *testing.T) {
284	testcases := []struct {
285		input  string
286		domain string
287		name   string
288	}{
289		{
290			input:  "test.com/foo",
291			domain: "test.com",
292			name:   "foo",
293		},
294		{
295			input:  "test_com/foo",
296			domain: "",
297			name:   "test_com/foo",
298		},
299		{
300			input:  "test:8080/foo",
301			domain: "test:8080",
302			name:   "foo",
303		},
304		{
305			input:  "test.com:8080/foo",
306			domain: "test.com:8080",
307			name:   "foo",
308		},
309		{
310			input:  "test-com:8080/foo",
311			domain: "test-com:8080",
312			name:   "foo",
313		},
314		{
315			input:  "xn--n3h.com:18080/foo",
316			domain: "xn--n3h.com:18080",
317			name:   "foo",
318		},
319	}
320	for _, testcase := range testcases {
321		failf := func(format string, v ...interface{}) {
322			t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
323			t.Fail()
324		}
325
326		named, err := WithName(testcase.input)
327		if err != nil {
328			failf("error parsing name: %s", err)
329		}
330		domain, name := SplitHostname(named)
331		if domain != testcase.domain {
332			failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
333		}
334		if name != testcase.name {
335			failf("unexpected name: got %q, expected %q", name, testcase.name)
336		}
337	}
338}
339
340type serializationType struct {
341	Description string
342	Field       Field
343}
344
345func TestSerialization(t *testing.T) {
346	testcases := []struct {
347		description string
348		input       string
349		name        string
350		tag         string
351		digest      string
352		err         error
353	}{
354		{
355			description: "empty value",
356			err:         ErrNameEmpty,
357		},
358		{
359			description: "just a name",
360			input:       "example.com:8000/named",
361			name:        "example.com:8000/named",
362		},
363		{
364			description: "name with a tag",
365			input:       "example.com:8000/named:tagged",
366			name:        "example.com:8000/named",
367			tag:         "tagged",
368		},
369		{
370			description: "name with digest",
371			input:       "other.com/named@sha256:1234567890098765432112345667890098765432112345667890098765432112",
372			name:        "other.com/named",
373			digest:      "sha256:1234567890098765432112345667890098765432112345667890098765432112",
374		},
375	}
376	for _, testcase := range testcases {
377		failf := func(format string, v ...interface{}) {
378			t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
379			t.Fail()
380		}
381
382		m := map[string]string{
383			"Description": testcase.description,
384			"Field":       testcase.input,
385		}
386		b, err := json.Marshal(m)
387		if err != nil {
388			failf("error marshalling: %v", err)
389		}
390		t := serializationType{}
391
392		if err := json.Unmarshal(b, &t); err != nil {
393			if testcase.err == nil {
394				failf("error unmarshalling: %v", err)
395			}
396			if err != testcase.err {
397				failf("wrong error, expected %v, got %v", testcase.err, err)
398			}
399
400			continue
401		} else if testcase.err != nil {
402			failf("expected error unmarshalling: %v", testcase.err)
403		}
404
405		if t.Description != testcase.description {
406			failf("wrong description, expected %q, got %q", testcase.description, t.Description)
407		}
408
409		ref := t.Field.Reference()
410
411		if named, ok := ref.(Named); ok {
412			if named.Name() != testcase.name {
413				failf("unexpected repository: got %q, expected %q", named.Name(), testcase.name)
414			}
415		} else if testcase.name != "" {
416			failf("expected named type, got %T", ref)
417		}
418
419		tagged, ok := ref.(Tagged)
420		if testcase.tag != "" {
421			if ok {
422				if tagged.Tag() != testcase.tag {
423					failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
424				}
425			} else {
426				failf("expected tagged type, got %T", ref)
427			}
428		} else if ok {
429			failf("unexpected tagged type")
430		}
431
432		digested, ok := ref.(Digested)
433		if testcase.digest != "" {
434			if ok {
435				if digested.Digest().String() != testcase.digest {
436					failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
437				}
438			} else {
439				failf("expected digested type, got %T", ref)
440			}
441		} else if ok {
442			failf("unexpected digested type")
443		}
444
445		t = serializationType{
446			Description: testcase.description,
447			Field:       AsField(ref),
448		}
449
450		b2, err := json.Marshal(t)
451		if err != nil {
452			failf("error marshing serialization type: %v", err)
453		}
454
455		if string(b) != string(b2) {
456			failf("unexpected serialized value: expected %q, got %q", string(b), string(b2))
457		}
458
459		// Ensure t.Field is not implementing "Reference" directly, getting
460		// around the Reference type system
461		var fieldInterface interface{} = t.Field
462		if _, ok := fieldInterface.(Reference); ok {
463			failf("field should not implement Reference interface")
464		}
465
466	}
467}
468
469func TestWithTag(t *testing.T) {
470	testcases := []struct {
471		name     string
472		digest   digest.Digest
473		tag      string
474		combined string
475	}{
476		{
477			name:     "test.com/foo",
478			tag:      "tag",
479			combined: "test.com/foo:tag",
480		},
481		{
482			name:     "foo",
483			tag:      "tag2",
484			combined: "foo:tag2",
485		},
486		{
487			name:     "test.com:8000/foo",
488			tag:      "tag4",
489			combined: "test.com:8000/foo:tag4",
490		},
491		{
492			name:     "test.com:8000/foo",
493			tag:      "TAG5",
494			combined: "test.com:8000/foo:TAG5",
495		},
496		{
497			name:     "test.com:8000/foo",
498			digest:   "sha256:1234567890098765432112345667890098765",
499			tag:      "TAG5",
500			combined: "test.com:8000/foo:TAG5@sha256:1234567890098765432112345667890098765",
501		},
502	}
503	for _, testcase := range testcases {
504		failf := func(format string, v ...interface{}) {
505			t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
506			t.Fail()
507		}
508
509		named, err := WithName(testcase.name)
510		if err != nil {
511			failf("error parsing name: %s", err)
512		}
513		if testcase.digest != "" {
514			canonical, err := WithDigest(named, testcase.digest)
515			if err != nil {
516				failf("error adding digest")
517			}
518			named = canonical
519		}
520
521		tagged, err := WithTag(named, testcase.tag)
522		if err != nil {
523			failf("WithTag failed: %s", err)
524		}
525		if tagged.String() != testcase.combined {
526			failf("unexpected: got %q, expected %q", tagged.String(), testcase.combined)
527		}
528	}
529}
530
531func TestWithDigest(t *testing.T) {
532	testcases := []struct {
533		name     string
534		digest   digest.Digest
535		tag      string
536		combined string
537	}{
538		{
539			name:     "test.com/foo",
540			digest:   "sha256:1234567890098765432112345667890098765",
541			combined: "test.com/foo@sha256:1234567890098765432112345667890098765",
542		},
543		{
544			name:     "foo",
545			digest:   "sha256:1234567890098765432112345667890098765",
546			combined: "foo@sha256:1234567890098765432112345667890098765",
547		},
548		{
549			name:     "test.com:8000/foo",
550			digest:   "sha256:1234567890098765432112345667890098765",
551			combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765",
552		},
553		{
554			name:     "test.com:8000/foo",
555			digest:   "sha256:1234567890098765432112345667890098765",
556			tag:      "latest",
557			combined: "test.com:8000/foo:latest@sha256:1234567890098765432112345667890098765",
558		},
559	}
560	for _, testcase := range testcases {
561		failf := func(format string, v ...interface{}) {
562			t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
563			t.Fail()
564		}
565
566		named, err := WithName(testcase.name)
567		if err != nil {
568			failf("error parsing name: %s", err)
569		}
570		if testcase.tag != "" {
571			tagged, err := WithTag(named, testcase.tag)
572			if err != nil {
573				failf("error adding tag")
574			}
575			named = tagged
576		}
577		digested, err := WithDigest(named, testcase.digest)
578		if err != nil {
579			failf("WithDigest failed: %s", err)
580		}
581		if digested.String() != testcase.combined {
582			failf("unexpected: got %q, expected %q", digested.String(), testcase.combined)
583		}
584	}
585}
586
587func TestParseNamed(t *testing.T) {
588	testcases := []struct {
589		input  string
590		domain string
591		name   string
592		err    error
593	}{
594		{
595			input:  "test.com/foo",
596			domain: "test.com",
597			name:   "foo",
598		},
599		{
600			input:  "test:8080/foo",
601			domain: "test:8080",
602			name:   "foo",
603		},
604		{
605			input: "test_com/foo",
606			err:   ErrNameNotCanonical,
607		},
608		{
609			input: "test.com",
610			err:   ErrNameNotCanonical,
611		},
612		{
613			input: "foo",
614			err:   ErrNameNotCanonical,
615		},
616		{
617			input: "library/foo",
618			err:   ErrNameNotCanonical,
619		},
620		{
621			input:  "docker.io/library/foo",
622			domain: "docker.io",
623			name:   "library/foo",
624		},
625		// Ambiguous case, parser will add "library/" to foo
626		{
627			input: "docker.io/foo",
628			err:   ErrNameNotCanonical,
629		},
630	}
631	for _, testcase := range testcases {
632		failf := func(format string, v ...interface{}) {
633			t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
634			t.Fail()
635		}
636
637		named, err := ParseNamed(testcase.input)
638		if err != nil && testcase.err == nil {
639			failf("error parsing name: %s", err)
640			continue
641		} else if err == nil && testcase.err != nil {
642			failf("parsing succeded: expected error %v", testcase.err)
643			continue
644		} else if err != testcase.err {
645			failf("unexpected error %v, expected %v", err, testcase.err)
646			continue
647		} else if err != nil {
648			continue
649		}
650
651		domain, name := SplitHostname(named)
652		if domain != testcase.domain {
653			failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
654		}
655		if name != testcase.name {
656			failf("unexpected name: got %q, expected %q", name, testcase.name)
657		}
658	}
659}
660