reference_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. package reference
  2. import (
  3. "encoding/json"
  4. "strconv"
  5. "strings"
  6. "testing"
  7. "github.com/docker/distribution/digest"
  8. )
  9. func TestReferenceParse(t *testing.T) {
  10. // referenceTestcases is a unified set of testcases for
  11. // testing the parsing of references
  12. referenceTestcases := []struct {
  13. // input is the repository name or name component testcase
  14. input string
  15. // err is the error expected from Parse, or nil
  16. err error
  17. // repository is the string representation for the reference
  18. repository string
  19. // hostname is the hostname expected in the reference
  20. hostname string
  21. // tag is the tag for the reference
  22. tag string
  23. // digest is the digest for the reference (enforces digest reference)
  24. digest string
  25. }{
  26. {
  27. input: "test_com",
  28. repository: "test_com",
  29. },
  30. {
  31. input: "test.com:tag",
  32. repository: "test.com",
  33. tag: "tag",
  34. },
  35. {
  36. input: "test.com:5000",
  37. repository: "test.com",
  38. tag: "5000",
  39. },
  40. {
  41. input: "test.com/repo:tag",
  42. hostname: "test.com",
  43. repository: "test.com/repo",
  44. tag: "tag",
  45. },
  46. {
  47. input: "test:5000/repo",
  48. hostname: "test:5000",
  49. repository: "test:5000/repo",
  50. },
  51. {
  52. input: "test:5000/repo:tag",
  53. hostname: "test:5000",
  54. repository: "test:5000/repo",
  55. tag: "tag",
  56. },
  57. {
  58. input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
  59. hostname: "test:5000",
  60. repository: "test:5000/repo",
  61. digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
  62. },
  63. {
  64. input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
  65. hostname: "test:5000",
  66. repository: "test:5000/repo",
  67. tag: "tag",
  68. digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
  69. },
  70. {
  71. input: "test:5000/repo",
  72. hostname: "test:5000",
  73. repository: "test:5000/repo",
  74. },
  75. {
  76. input: "",
  77. err: ErrNameEmpty,
  78. },
  79. {
  80. input: ":justtag",
  81. err: ErrReferenceInvalidFormat,
  82. },
  83. {
  84. input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
  85. err: ErrReferenceInvalidFormat,
  86. },
  87. {
  88. input: "repo@sha256:ffffffffffffffffffffffffffffffffff",
  89. err: digest.ErrDigestInvalidLength,
  90. },
  91. {
  92. input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
  93. err: digest.ErrDigestUnsupported,
  94. },
  95. {
  96. input: strings.Repeat("a/", 128) + "a:tag",
  97. err: ErrNameTooLong,
  98. },
  99. {
  100. input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max",
  101. hostname: "a",
  102. repository: strings.Repeat("a/", 127) + "a",
  103. tag: "tag-puts-this-over-max",
  104. },
  105. {
  106. input: "aa/asdf$$^/aa",
  107. err: ErrReferenceInvalidFormat,
  108. },
  109. {
  110. input: "sub-dom1.foo.com/bar/baz/quux",
  111. hostname: "sub-dom1.foo.com",
  112. repository: "sub-dom1.foo.com/bar/baz/quux",
  113. },
  114. {
  115. input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag",
  116. hostname: "sub-dom1.foo.com",
  117. repository: "sub-dom1.foo.com/bar/baz/quux",
  118. tag: "some-long-tag",
  119. },
  120. {
  121. input: "b.gcr.io/test.example.com/my-app:test.example.com",
  122. hostname: "b.gcr.io",
  123. repository: "b.gcr.io/test.example.com/my-app",
  124. tag: "test.example.com",
  125. },
  126. {
  127. input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode
  128. hostname: "xn--n3h.com",
  129. repository: "xn--n3h.com/myimage",
  130. tag: "xn--n3h.com",
  131. },
  132. {
  133. input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode
  134. hostname: "xn--7o8h.com",
  135. repository: "xn--7o8h.com/myimage",
  136. tag: "xn--7o8h.com",
  137. digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
  138. },
  139. {
  140. input: "foo_bar.com:8080",
  141. repository: "foo_bar.com",
  142. tag: "8080",
  143. },
  144. {
  145. input: "foo/foo_bar.com:8080",
  146. hostname: "foo",
  147. repository: "foo/foo_bar.com",
  148. tag: "8080",
  149. },
  150. }
  151. for _, testcase := range referenceTestcases {
  152. failf := func(format string, v ...interface{}) {
  153. t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
  154. t.Fail()
  155. }
  156. repo, err := Parse(testcase.input)
  157. if testcase.err != nil {
  158. if err == nil {
  159. failf("missing expected error: %v", testcase.err)
  160. } else if testcase.err != err {
  161. failf("mismatched error: got %v, expected %v", err, testcase.err)
  162. }
  163. continue
  164. } else if err != nil {
  165. failf("unexpected parse error: %v", err)
  166. continue
  167. }
  168. if repo.String() != testcase.input {
  169. failf("mismatched repo: got %q, expected %q", repo.String(), testcase.input)
  170. }
  171. if named, ok := repo.(Named); ok {
  172. if named.Name() != testcase.repository {
  173. failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository)
  174. }
  175. hostname, _ := SplitHostname(named)
  176. if hostname != testcase.hostname {
  177. failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname)
  178. }
  179. } else if testcase.repository != "" || testcase.hostname != "" {
  180. failf("expected named type, got %T", repo)
  181. }
  182. tagged, ok := repo.(Tagged)
  183. if testcase.tag != "" {
  184. if ok {
  185. if tagged.Tag() != testcase.tag {
  186. failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
  187. }
  188. } else {
  189. failf("expected tagged type, got %T", repo)
  190. }
  191. } else if ok {
  192. failf("unexpected tagged type")
  193. }
  194. digested, ok := repo.(Digested)
  195. if testcase.digest != "" {
  196. if ok {
  197. if digested.Digest().String() != testcase.digest {
  198. failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
  199. }
  200. } else {
  201. failf("expected digested type, got %T", repo)
  202. }
  203. } else if ok {
  204. failf("unexpected digested type")
  205. }
  206. }
  207. }
  208. // TestWithNameFailure tests cases where WithName should fail. Cases where it
  209. // should succeed are covered by TestSplitHostname, below.
  210. func TestWithNameFailure(t *testing.T) {
  211. testcases := []struct {
  212. input string
  213. err error
  214. }{
  215. {
  216. input: "",
  217. err: ErrNameEmpty,
  218. },
  219. {
  220. input: ":justtag",
  221. err: ErrReferenceInvalidFormat,
  222. },
  223. {
  224. input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
  225. err: ErrReferenceInvalidFormat,
  226. },
  227. {
  228. input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
  229. err: ErrReferenceInvalidFormat,
  230. },
  231. {
  232. input: strings.Repeat("a/", 128) + "a:tag",
  233. err: ErrNameTooLong,
  234. },
  235. {
  236. input: "aa/asdf$$^/aa",
  237. err: ErrReferenceInvalidFormat,
  238. },
  239. }
  240. for _, testcase := range testcases {
  241. failf := func(format string, v ...interface{}) {
  242. t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
  243. t.Fail()
  244. }
  245. _, err := WithName(testcase.input)
  246. if err == nil {
  247. failf("no error parsing name. expected: %s", testcase.err)
  248. }
  249. }
  250. }
  251. func TestSplitHostname(t *testing.T) {
  252. testcases := []struct {
  253. input string
  254. hostname string
  255. name string
  256. }{
  257. {
  258. input: "test.com/foo",
  259. hostname: "test.com",
  260. name: "foo",
  261. },
  262. {
  263. input: "test_com/foo",
  264. hostname: "",
  265. name: "test_com/foo",
  266. },
  267. {
  268. input: "test:8080/foo",
  269. hostname: "test:8080",
  270. name: "foo",
  271. },
  272. {
  273. input: "test.com:8080/foo",
  274. hostname: "test.com:8080",
  275. name: "foo",
  276. },
  277. {
  278. input: "test-com:8080/foo",
  279. hostname: "test-com:8080",
  280. name: "foo",
  281. },
  282. {
  283. input: "xn--n3h.com:18080/foo",
  284. hostname: "xn--n3h.com:18080",
  285. name: "foo",
  286. },
  287. }
  288. for _, testcase := range testcases {
  289. failf := func(format string, v ...interface{}) {
  290. t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
  291. t.Fail()
  292. }
  293. named, err := WithName(testcase.input)
  294. if err != nil {
  295. failf("error parsing name: %s", err)
  296. }
  297. hostname, name := SplitHostname(named)
  298. if hostname != testcase.hostname {
  299. failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname)
  300. }
  301. if name != testcase.name {
  302. failf("unexpected name: got %q, expected %q", name, testcase.name)
  303. }
  304. }
  305. }
  306. type serializationType struct {
  307. Description string
  308. Field Field
  309. }
  310. func TestSerialization(t *testing.T) {
  311. testcases := []struct {
  312. description string
  313. input string
  314. name string
  315. tag string
  316. digest string
  317. err error
  318. }{
  319. {
  320. description: "empty value",
  321. err: ErrNameEmpty,
  322. },
  323. {
  324. description: "just a name",
  325. input: "example.com:8000/named",
  326. name: "example.com:8000/named",
  327. },
  328. {
  329. description: "name with a tag",
  330. input: "example.com:8000/named:tagged",
  331. name: "example.com:8000/named",
  332. tag: "tagged",
  333. },
  334. {
  335. description: "name with digest",
  336. input: "other.com/named@sha256:1234567890098765432112345667890098765432112345667890098765432112",
  337. name: "other.com/named",
  338. digest: "sha256:1234567890098765432112345667890098765432112345667890098765432112",
  339. },
  340. }
  341. for _, testcase := range testcases {
  342. failf := func(format string, v ...interface{}) {
  343. t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
  344. t.Fail()
  345. }
  346. m := map[string]string{
  347. "Description": testcase.description,
  348. "Field": testcase.input,
  349. }
  350. b, err := json.Marshal(m)
  351. if err != nil {
  352. failf("error marshalling: %v", err)
  353. }
  354. t := serializationType{}
  355. if err := json.Unmarshal(b, &t); err != nil {
  356. if testcase.err == nil {
  357. failf("error unmarshalling: %v", err)
  358. }
  359. if err != testcase.err {
  360. failf("wrong error, expected %v, got %v", testcase.err, err)
  361. }
  362. continue
  363. } else if testcase.err != nil {
  364. failf("expected error unmarshalling: %v", testcase.err)
  365. }
  366. if t.Description != testcase.description {
  367. failf("wrong description, expected %q, got %q", testcase.description, t.Description)
  368. }
  369. ref := t.Field.Reference()
  370. if named, ok := ref.(Named); ok {
  371. if named.Name() != testcase.name {
  372. failf("unexpected repository: got %q, expected %q", named.Name(), testcase.name)
  373. }
  374. } else if testcase.name != "" {
  375. failf("expected named type, got %T", ref)
  376. }
  377. tagged, ok := ref.(Tagged)
  378. if testcase.tag != "" {
  379. if ok {
  380. if tagged.Tag() != testcase.tag {
  381. failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
  382. }
  383. } else {
  384. failf("expected tagged type, got %T", ref)
  385. }
  386. } else if ok {
  387. failf("unexpected tagged type")
  388. }
  389. digested, ok := ref.(Digested)
  390. if testcase.digest != "" {
  391. if ok {
  392. if digested.Digest().String() != testcase.digest {
  393. failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
  394. }
  395. } else {
  396. failf("expected digested type, got %T", ref)
  397. }
  398. } else if ok {
  399. failf("unexpected digested type")
  400. }
  401. t = serializationType{
  402. Description: testcase.description,
  403. Field: AsField(ref),
  404. }
  405. b2, err := json.Marshal(t)
  406. if err != nil {
  407. failf("error marshing serialization type: %v", err)
  408. }
  409. if string(b) != string(b2) {
  410. failf("unexpected serialized value: expected %q, got %q", string(b), string(b2))
  411. }
  412. // Ensure t.Field is not implementing "Reference" directly, getting
  413. // around the Reference type system
  414. var fieldInterface interface{} = t.Field
  415. if _, ok := fieldInterface.(Reference); ok {
  416. failf("field should not implement Reference interface")
  417. }
  418. }
  419. }
  420. func TestWithTag(t *testing.T) {
  421. testcases := []struct {
  422. name string
  423. tag string
  424. combined string
  425. }{
  426. {
  427. name: "test.com/foo",
  428. tag: "tag",
  429. combined: "test.com/foo:tag",
  430. },
  431. {
  432. name: "foo",
  433. tag: "tag2",
  434. combined: "foo:tag2",
  435. },
  436. {
  437. name: "test.com:8000/foo",
  438. tag: "tag4",
  439. combined: "test.com:8000/foo:tag4",
  440. },
  441. {
  442. name: "test.com:8000/foo",
  443. tag: "TAG5",
  444. combined: "test.com:8000/foo:TAG5",
  445. },
  446. }
  447. for _, testcase := range testcases {
  448. failf := func(format string, v ...interface{}) {
  449. t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
  450. t.Fail()
  451. }
  452. named, err := WithName(testcase.name)
  453. if err != nil {
  454. failf("error parsing name: %s", err)
  455. }
  456. tagged, err := WithTag(named, testcase.tag)
  457. if err != nil {
  458. failf("WithTag failed: %s", err)
  459. }
  460. if tagged.String() != testcase.combined {
  461. failf("unexpected: got %q, expected %q", tagged.String(), testcase.combined)
  462. }
  463. }
  464. }
  465. func TestWithDigest(t *testing.T) {
  466. testcases := []struct {
  467. name string
  468. digest digest.Digest
  469. combined string
  470. }{
  471. {
  472. name: "test.com/foo",
  473. digest: "sha256:1234567890098765432112345667890098765",
  474. combined: "test.com/foo@sha256:1234567890098765432112345667890098765",
  475. },
  476. {
  477. name: "foo",
  478. digest: "sha256:1234567890098765432112345667890098765",
  479. combined: "foo@sha256:1234567890098765432112345667890098765",
  480. },
  481. {
  482. name: "test.com:8000/foo",
  483. digest: "sha256:1234567890098765432112345667890098765",
  484. combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765",
  485. },
  486. }
  487. for _, testcase := range testcases {
  488. failf := func(format string, v ...interface{}) {
  489. t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
  490. t.Fail()
  491. }
  492. named, err := WithName(testcase.name)
  493. if err != nil {
  494. failf("error parsing name: %s", err)
  495. }
  496. digested, err := WithDigest(named, testcase.digest)
  497. if err != nil {
  498. failf("WithDigest failed: %s", err)
  499. }
  500. if digested.String() != testcase.combined {
  501. failf("unexpected: got %q, expected %q", digested.String(), testcase.combined)
  502. }
  503. }
  504. }