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