cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociserver/registry_test.go (about) 1 // Copyright 2018 Google LLC All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ociserver_test 16 17 import ( 18 "fmt" 19 "io" 20 "net/http" 21 "net/http/httptest" 22 "strings" 23 "testing" 24 25 "cuelabs.dev/go/oci/ociregistry/ocimem" 26 "cuelabs.dev/go/oci/ociregistry/ociserver" 27 "github.com/go-quicktest/qt" 28 "github.com/opencontainers/go-digest" 29 ) 30 31 const ( 32 weirdIndex = `{ 33 "manifests": [ 34 { 35 "size": 3, 36 "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 37 "mediaType":"application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" 38 },{ 39 "size": 3, 40 "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 41 "mediaType":"application/xml" 42 },{ 43 "size": 3, 44 "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 45 "mediaType":"application/vnd.oci.image.manifest.v1+json" 46 } 47 ] 48 }` 49 ) 50 51 func TestCalls(t *testing.T) { 52 tcs := []struct { 53 skip bool 54 55 Description string 56 57 // Request / setup 58 Method string 59 Body string // request body to send 60 URL string 61 Digests map[string]string 62 Manifests map[string]string 63 BlobStream map[string]string 64 RequestHeader map[string]string 65 66 // Response 67 WantCode int 68 WantHeader map[string]string 69 WantBody string // response body to expect 70 }{ 71 { 72 Description: "v2_returns_200", 73 Method: "GET", 74 URL: "/v2", 75 WantCode: http.StatusOK, 76 WantHeader: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"}, 77 }, 78 { 79 Description: "v2_slash_returns_200", 80 Method: "GET", 81 URL: "/v2/", 82 WantCode: http.StatusOK, 83 WantHeader: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"}, 84 }, 85 { 86 Description: "v2_bad_returns_404", 87 Method: "GET", 88 URL: "/v2/bad", 89 WantCode: http.StatusNotFound, 90 WantHeader: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"}, 91 WantBody: `{"errors":[{"code":"UNKNOWN","message":"page not found"}]}`, 92 }, 93 { 94 Description: "GET_non_existent_blob", 95 Method: "GET", 96 URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 97 WantCode: http.StatusNotFound, 98 WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`, 99 }, 100 { 101 Description: "HEAD_non_existent_blob", 102 Method: "HEAD", 103 URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 104 WantCode: http.StatusNotFound, 105 }, 106 { 107 Description: "GET_bad_digest", 108 Method: "GET", 109 URL: "/v2/foo/blobs/sha256:asd", 110 WantCode: http.StatusBadRequest, 111 WantBody: `{"errors":[{"code":"UNKNOWN","message":"badly formed digest"}]}`, 112 }, 113 { 114 Description: "HEAD_bad_digest", 115 Method: "HEAD", 116 URL: "/v2/foo/blobs/sha256:asd", 117 WantCode: http.StatusBadRequest, 118 }, 119 { 120 Description: "bad_blob_verb", 121 Method: "FOO", 122 URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 123 WantCode: http.StatusMethodNotAllowed, 124 WantBody: `{"errors":[{"code":"UNKNOWN","message":"method not allowed"}]}`, 125 }, 126 { 127 Description: "GET_containerless_blob", 128 Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"}, 129 Method: "GET", 130 URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 131 WantCode: http.StatusOK, 132 WantHeader: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, 133 WantBody: "foo", 134 }, 135 { 136 Description: "GET_blob", 137 Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"}, 138 Method: "GET", 139 URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 140 WantCode: http.StatusOK, 141 WantHeader: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, 142 WantBody: "foo", 143 }, 144 { 145 Description: "GET_blob_range_defined_range", 146 Digests: map[string]string{ 147 "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9": "hello world", 148 }, 149 Method: "GET", 150 RequestHeader: map[string]string{ 151 "Range": "bytes=1-4", 152 }, 153 URL: "/v2/foo/blobs/sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 154 WantCode: http.StatusPartialContent, 155 WantHeader: map[string]string{ 156 "Docker-Content-Digest": "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 157 "Content-Length": "4", 158 "Content-Range": "bytes 1-4/11", 159 }, 160 WantBody: "ello", 161 }, 162 { 163 Description: "GET_blob_range_undefined_range_end", 164 Digests: map[string]string{ 165 "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9": "hello world", 166 }, 167 Method: "GET", 168 RequestHeader: map[string]string{ 169 "Range": "bytes=3-", 170 }, 171 URL: "/v2/foo/blobs/sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 172 WantCode: http.StatusPartialContent, 173 WantHeader: map[string]string{ 174 "Docker-Content-Digest": "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 175 "Content-Length": "8", 176 "Content-Range": "bytes 3-10/11", 177 }, 178 WantBody: "lo world", 179 }, 180 { 181 Description: "GET_blob_range_invalid-range-start", 182 Digests: map[string]string{ 183 "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9": "hello world", 184 }, 185 Method: "GET", 186 RequestHeader: map[string]string{ 187 "Range": "bytes=20-30", 188 }, 189 URL: "/v2/foo/blobs/sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 190 // TODO change ocimem to return an error that results in a 416 status. 191 WantCode: http.StatusInternalServerError, 192 WantBody: `{"errors":[{"code":"UNKNOWN","message":"invalid range [20, 11]; have [0, 11]"}]}`, 193 }, 194 { 195 Description: "HEAD_blob", 196 Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"}, 197 Method: "HEAD", 198 URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 199 WantCode: http.StatusOK, 200 WantHeader: map[string]string{ 201 "Content-Length": "3", 202 "Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 203 }, 204 }, 205 { 206 Description: "DELETE_blob", 207 Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"}, 208 Method: "DELETE", 209 URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 210 WantCode: http.StatusAccepted, 211 }, 212 { 213 Description: "blob_url_with_no_container", 214 Method: "GET", 215 URL: "/v2/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 216 WantCode: http.StatusNotFound, 217 WantBody: `{"errors":[{"code":"UNKNOWN","message":"page not found"}]}`, 218 }, 219 { 220 Description: "uploadurl", 221 Method: "POST", 222 URL: "/v2/foo/blobs/uploads/", 223 WantCode: http.StatusAccepted, 224 WantHeader: map[string]string{"Range": "0-0"}, 225 }, 226 { 227 Description: "uploadurl", 228 Method: "POST", 229 URL: "/v2/foo/blobs/uploads/", 230 WantCode: http.StatusAccepted, 231 WantHeader: map[string]string{"Range": "0-0"}, 232 }, 233 { 234 Description: "upload_put_missing_digest", 235 Method: "PUT", 236 URL: "/v2/foo/blobs/uploads/MQ", 237 WantCode: http.StatusBadRequest, 238 WantBody: `{"errors":[{"code":"UNKNOWN","message":"badly formed digest"}]}`, 239 }, 240 { 241 Description: "monolithic_upload_good_digest", 242 Method: "POST", 243 URL: "/v2/foo/blobs/uploads?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 244 WantCode: http.StatusCreated, 245 Body: "foo", 246 WantHeader: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, 247 }, 248 { 249 Description: "monolithic_upload_bad_digest", 250 Method: "POST", 251 URL: "/v2/foo/blobs/uploads?digest=sha256:fake", 252 Body: "foo", 253 WantCode: http.StatusBadRequest, 254 WantBody: `{"errors":[{"code":"UNKNOWN","message":"badly formed digest"}]}`, 255 }, 256 { 257 Description: "upload_good_digest", 258 Method: "PUT", 259 URL: "/v2/foo/blobs/uploads/MQ?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 260 WantCode: http.StatusCreated, 261 Body: "foo", 262 WantHeader: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, 263 }, 264 { 265 Description: "upload_bad_digest", 266 Method: "PUT", 267 URL: "/v2/foo/blobs/uploads/MQ?digest=sha256:baddigest", 268 WantCode: http.StatusBadRequest, 269 Body: "foo", 270 WantBody: `{"errors":[{"code":"UNKNOWN","message":"badly formed digest"}]}`, 271 }, 272 { 273 Description: "stream_upload", 274 Method: "PATCH", 275 URL: "/v2/foo/blobs/uploads/MQ", 276 WantCode: http.StatusAccepted, 277 Body: "foo", 278 RequestHeader: map[string]string{ 279 "Content-Range": "0-2", 280 }, 281 WantHeader: map[string]string{ 282 "Range": "0-2", 283 "Location": "/v2/foo/blobs/uploads/MQ", 284 }, 285 }, 286 { 287 skip: true, 288 Description: "stream_duplicate_upload", 289 Method: "PATCH", 290 URL: "/v2/foo/blobs/uploads/MQ", 291 WantCode: http.StatusBadRequest, 292 Body: "foo", 293 BlobStream: map[string]string{"MQ": "foo"}, 294 }, 295 { 296 Description: "stream_finish_upload", 297 Method: "PUT", 298 URL: "/v2/foo/blobs/uploads/MQ?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", 299 BlobStream: map[string]string{"MQ": "foo"}, 300 WantCode: http.StatusCreated, 301 WantHeader: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, 302 }, 303 { 304 Description: "get_missing_manifest", 305 Method: "GET", 306 URL: "/v2/foo/manifests/latest", 307 WantCode: http.StatusNotFound, 308 WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`, 309 }, 310 { 311 Description: "head_missing_manifest", 312 Method: "HEAD", 313 URL: "/v2/foo/manifests/latest", 314 WantCode: http.StatusNotFound, 315 }, 316 { 317 Description: "get_missing_manifest_good_container", 318 Manifests: map[string]string{"foo/manifests/latest": "foo"}, 319 Method: "GET", 320 URL: "/v2/foo/manifests/bar", 321 WantCode: http.StatusNotFound, 322 WantBody: `{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown to registry"}]}`, 323 }, 324 { 325 Description: "head_missing_manifest_good_container", 326 Manifests: map[string]string{"foo/manifests/latest": "foo"}, 327 Method: "HEAD", 328 URL: "/v2/foo/manifests/bar", 329 WantCode: http.StatusNotFound, 330 }, 331 { 332 Description: "get_manifest_by_tag", 333 Manifests: map[string]string{"foo/manifests/latest": "foo"}, 334 Method: "GET", 335 URL: "/v2/foo/manifests/latest", 336 WantCode: http.StatusOK, 337 WantBody: "foo", 338 }, 339 { 340 Description: "get_manifest_by_digest", 341 Manifests: map[string]string{"foo/manifests/latest": "foo"}, 342 Method: "GET", 343 URL: "/v2/foo/manifests/" + digestOf("foo"), 344 WantCode: http.StatusOK, 345 WantBody: "foo", 346 }, 347 { 348 Description: "head_manifest", 349 Manifests: map[string]string{"foo/manifests/latest": "foo"}, 350 Method: "HEAD", 351 URL: "/v2/foo/manifests/latest", 352 WantCode: http.StatusOK, 353 }, 354 { 355 Description: "create_manifest", 356 Method: "PUT", 357 URL: "/v2/foo/manifests/latest", 358 WantCode: http.StatusCreated, 359 Body: "foo", 360 }, 361 { 362 Description: "create_index", 363 Method: "PUT", 364 URL: "/v2/foo/manifests/latest", 365 WantCode: http.StatusCreated, 366 Body: weirdIndex, 367 RequestHeader: map[string]string{ 368 "Content-Type": "application/vnd.oci.image.index.v1+json", 369 }, 370 Manifests: map[string]string{"foo/manifests/image": "foo"}, 371 }, 372 { 373 skip: true, 374 Description: "create_index_missing_child", 375 Method: "PUT", 376 URL: "/v2/foo/manifests/latest", 377 WantCode: http.StatusNotFound, 378 Body: weirdIndex, 379 RequestHeader: map[string]string{ 380 "Content-Type": "application/vnd.oci.image.index.v1+json", 381 }, 382 }, 383 { 384 skip: true, 385 Description: "bad_index_body", 386 Method: "PUT", 387 URL: "/v2/foo/manifests/latest", 388 WantCode: http.StatusBadRequest, 389 Body: "foo", 390 RequestHeader: map[string]string{ 391 "Content-Type": "application/vnd.oci.image.index.v1+json", 392 }, 393 }, 394 { 395 Description: "bad_manifest_method", 396 Method: "BAR", 397 URL: "/v2/foo/manifests/latest", 398 WantCode: http.StatusMethodNotAllowed, 399 WantBody: `{"errors":[{"code":"UNKNOWN","message":"method not allowed"}]}`, 400 }, 401 { 402 Description: "Chunk_upload_start", 403 Method: "PATCH", 404 URL: "/v2/foo/blobs/uploads/MQ", 405 RequestHeader: map[string]string{"Content-Range": "0-2"}, 406 WantCode: http.StatusAccepted, 407 Body: "foo", 408 WantHeader: map[string]string{ 409 "Range": "0-2", 410 "Location": "/v2/foo/blobs/uploads/MQ", 411 }, 412 }, 413 { 414 Description: "Chunk_upload_bad_content_range", 415 Method: "PATCH", 416 URL: "/v2/foo/blobs/uploads/MQ", 417 RequestHeader: map[string]string{"Content-Range": "0-bar"}, 418 // TODO the original had 405 response here. Which is correct? 419 WantCode: http.StatusBadRequest, 420 Body: "foo", 421 WantBody: `{"errors":[{"code":"UNSUPPORTED","message":"we don't understand your Content-Range"}]}`, 422 }, 423 { 424 Description: "Chunk_upload_overlaps_previous_data", 425 Method: "PATCH", 426 URL: "/v2/foo/blobs/uploads/MQ", 427 BlobStream: map[string]string{"MQ": "foo"}, 428 RequestHeader: map[string]string{"Content-Range": "2-4"}, 429 WantCode: http.StatusRequestedRangeNotSatisfiable, 430 Body: "bar", 431 WantBody: `{"errors":[{"code":"RANGE_INVALID","message":"cannot copy blob data: invalid offset 2 in resumed upload (actual offset 3): range invalid: invalid content range"}]}`, 432 }, 433 { 434 Description: "Chunk_upload_after_previous_data", 435 Method: "PATCH", 436 URL: "/v2/foo/blobs/uploads/MQ", 437 BlobStream: map[string]string{"MQ": "foo"}, 438 RequestHeader: map[string]string{"Content-Range": "3-5"}, 439 WantCode: http.StatusAccepted, 440 Body: "bar", 441 WantHeader: map[string]string{ 442 "Range": "0-5", 443 "Location": "/v2/foo/blobs/uploads/MQ", 444 }, 445 }, 446 { 447 Description: "DELETE_Unknown_name", 448 Method: "DELETE", 449 URL: "/v2/test/honk/manifests/latest", 450 WantCode: http.StatusNotFound, 451 WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`, 452 }, 453 { 454 Description: "DELETE_Unknown_manifest", 455 Manifests: map[string]string{"honk/manifests/latest": "honk"}, 456 Method: "DELETE", 457 URL: "/v2/honk/manifests/tag-honk", 458 WantCode: http.StatusNotFound, 459 WantBody: `{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown to registry: tag does not exist"}]}`, 460 }, 461 { 462 Description: "DELETE_existing_manifest", 463 Manifests: map[string]string{"foo/manifests/latest": "foo"}, 464 Method: "DELETE", 465 URL: "/v2/foo/manifests/latest", 466 WantCode: http.StatusAccepted, 467 }, 468 { 469 Description: "DELETE_existing_manifest_by_digest", 470 Manifests: map[string]string{"foo/manifests/latest": "foo"}, 471 Method: "DELETE", 472 URL: "/v2/foo/manifests/" + digestOf("foo"), 473 WantCode: http.StatusAccepted, 474 }, 475 { 476 Description: "list_tags", 477 Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"}, 478 Method: "GET", 479 URL: "/v2/foo/tags/list?n=1000", 480 WantCode: http.StatusOK, 481 WantBody: `{"name":"foo","tags":["latest","tag1"]}`, 482 }, 483 { 484 Description: "limit_tags", 485 Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"}, 486 Method: "GET", 487 URL: "/v2/foo/tags/list?n=1", 488 WantCode: http.StatusOK, 489 WantBody: `{"name":"foo","tags":["latest"]}`, 490 }, 491 { 492 Description: "offset_tags", 493 Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"}, 494 Method: "GET", 495 URL: "/v2/foo/tags/list?last=latest", 496 WantCode: http.StatusOK, 497 WantBody: `{"name":"foo","tags":["tag1"]}`, 498 }, 499 { 500 Description: "list_non_existing_tags", 501 Method: "GET", 502 URL: "/v2/foo/tags/list?n=1000", 503 WantCode: http.StatusNotFound, 504 WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`, 505 }, 506 { 507 Description: "list_repos", 508 Manifests: map[string]string{"foo/manifests/latest": "foo", "bar/manifests/latest": "bar"}, 509 Method: "GET", 510 URL: "/v2/_catalog?n=1000", 511 WantCode: http.StatusOK, 512 WantBody: `{"repositories":["bar","foo"]}`, 513 }, 514 { 515 Description: "fetch_references", 516 Method: "GET", 517 URL: "/v2/foo/referrers/" + digestOf("foo"), 518 WantCode: http.StatusOK, 519 Manifests: map[string]string{ 520 "foo/manifests/image": "foo", 521 "foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"" + digestOf("foo") + "\"}}", 522 }, 523 WantBody: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":null}`, 524 }, 525 { 526 Description: "fetch_references,_subject_pointing_elsewhere", 527 Method: "GET", 528 URL: "/v2/foo/referrers/" + digestOf("foo"), 529 WantCode: http.StatusOK, 530 Manifests: map[string]string{ 531 "foo/manifests/image": "foo", 532 "foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"" + digestOf("nonexistant") + "\"}}", 533 }, 534 WantBody: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":null}`, 535 }, 536 { 537 Description: "fetch_references,_no_results", 538 Method: "GET", 539 URL: "/v2/foo/referrers/" + digestOf("foo"), 540 WantCode: http.StatusOK, 541 Manifests: map[string]string{ 542 "foo/manifests/image": "foo", 543 }, 544 WantBody: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":null}`, 545 }, 546 { 547 Description: "fetch_references,_missing_repo", 548 Method: "GET", 549 URL: "/v2/does-not-exist/referrers/" + digestOf("foo"), 550 WantCode: http.StatusNotFound, 551 WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`, 552 }, 553 { 554 Description: "fetch_references,_bad_target_(tag_vs._digest)", 555 Method: "GET", 556 URL: "/v2/foo/referrers/latest", 557 WantCode: http.StatusBadRequest, 558 WantBody: `{"errors":[{"code":"UNKNOWN","message":"badly formed digest"}]}`, 559 }, 560 { 561 skip: true, 562 Description: "fetch_references,_bad_method", 563 Method: "POST", 564 URL: "/v2/foo/referrers/" + digestOf("foo"), 565 WantCode: http.StatusBadRequest, 566 }, 567 } 568 569 for _, tc := range tcs { 570 571 testf := func(t *testing.T) { 572 if tc.skip { 573 t.Skip("skipping") 574 } 575 r := ociserver.New(ocimem.New(), nil) 576 s := httptest.NewServer(r) 577 defer s.Close() 578 579 for manifest, contents := range tc.Manifests { 580 req, _ := http.NewRequest("PUT", s.URL+"/v2/"+manifest, strings.NewReader(contents)) 581 req.Header.Set("Content-Type", "application/octet-stream") // TODO better media type 582 t.Log(req.Method, req.URL) 583 resp, err := s.Client().Do(req) 584 if err != nil { 585 t.Fatalf("Error uploading manifest: %v", err) 586 } 587 if resp.StatusCode != http.StatusCreated { 588 body, _ := io.ReadAll(resp.Body) 589 t.Fatalf("Error uploading manifest got status: %d %s", resp.StatusCode, body) 590 } 591 t.Logf("created manifest with digest %v", resp.Header.Get("Docker-Content-Digest")) 592 } 593 594 for digest, contents := range tc.Digests { 595 req, _ := http.NewRequest( 596 "POST", 597 fmt.Sprintf("%s/v2/foo/blobs/uploads/?digest=%s", s.URL, digest), 598 strings.NewReader(contents), 599 ) 600 req.Header.Set("Content-Length", fmt.Sprint(len(contents))) // TODO better media type 601 t.Log(req.Method, req.URL) 602 resp, err := s.Client().Do(req) 603 if err != nil { 604 t.Fatalf("Error uploading digest: %v", err) 605 } 606 if resp.StatusCode != http.StatusCreated { 607 body, _ := io.ReadAll(resp.Body) 608 t.Fatalf("Error uploading digest got status: %d %s", resp.StatusCode, body) 609 } 610 } 611 612 for upload, contents := range tc.BlobStream { 613 req, err := http.NewRequest( 614 "PATCH", 615 fmt.Sprintf("%s/v2/foo/blobs/uploads/%s", s.URL, upload), 616 io.NopCloser(strings.NewReader(contents)), 617 ) 618 if err != nil { 619 t.Fatal(err) 620 } 621 req.Header.Add("Content-Range", fmt.Sprintf("0-%d", len(contents)-1)) 622 t.Log(req.Method, req.URL) 623 resp, err := s.Client().Do(req) 624 if err != nil { 625 t.Fatalf("Error streaming blob: %v", err) 626 } 627 if resp.StatusCode != http.StatusAccepted { 628 body, _ := io.ReadAll(resp.Body) 629 t.Fatalf("Error streaming blob: %d %s", resp.StatusCode, body) 630 } 631 632 } 633 634 req, err := http.NewRequest(tc.Method, s.URL+tc.URL, strings.NewReader(tc.Body)) 635 qt.Assert(t, qt.IsNil(err)) 636 for k, v := range tc.RequestHeader { 637 req.Header.Set(k, v) 638 } 639 t.Logf("%s %v", req.Method, req.URL) 640 resp, err := s.Client().Do(req) 641 if err != nil { 642 t.Fatalf("Error getting %q: %v", tc.URL, err) 643 } 644 defer resp.Body.Close() 645 body, err := io.ReadAll(resp.Body) 646 if err != nil { 647 t.Errorf("Reading response body: %v", err) 648 } 649 if resp.StatusCode != tc.WantCode { 650 t.Fatalf("Incorrect status code, got %d, want %d; body: %s", resp.StatusCode, tc.WantCode, body) 651 } 652 653 for k, v := range tc.WantHeader { 654 r := resp.Header.Get(k) 655 if r != v { 656 t.Errorf("Incorrect header %q received, got %q, want %q", k, r, v) 657 } 658 } 659 660 if string(body) != tc.WantBody { 661 t.Logf("\n WantBody: `%s`,", body) 662 t.Errorf("Incorrect response body.\ngot:\n\t%q\n\twant:\n\t%q", body, tc.WantBody) 663 } 664 } 665 t.Run(tc.Description, testf) 666 } 667 } 668 669 func digestOf(s string) string { 670 return string(digest.FromString(s)) 671 }