zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/cli/client/utils_internal_test.go (about) 1 //go:build search 2 // +build search 3 4 package client 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "io" 11 "net/http" 12 "sync" 13 "testing" 14 15 "github.com/gorilla/mux" 16 godigest "github.com/opencontainers/go-digest" 17 ispec "github.com/opencontainers/image-spec/specs-go/v1" 18 . "github.com/smartystreets/goconvey/convey" 19 20 test "zotregistry.dev/zot/pkg/test/common" 21 ) 22 23 func getDefaultSearchConf(baseURL string) SearchConfig { 24 verifyTLS := false 25 debug := false 26 verbose := true 27 outputFormat := "text" 28 29 return SearchConfig{ 30 ServURL: baseURL, 31 ResultWriter: io.Discard, 32 VerifyTLS: verifyTLS, 33 Debug: debug, 34 Verbose: verbose, 35 OutputFormat: outputFormat, 36 } 37 } 38 39 type RouteHandler struct { 40 Route string 41 // HandlerFunc is the HTTP handler function that receives a writer for output and an HTTP request as input. 42 HandlerFunc http.HandlerFunc 43 // AllowedMethods specifies the HTTP methods allowed for the current route. 44 AllowedMethods []string 45 } 46 47 // Routes is a map that associates HTTP paths to their corresponding HTTP handlers. 48 type HTTPRoutes []RouteHandler 49 50 func StartTestHTTPServer(routes HTTPRoutes, port string) *http.Server { 51 baseURL := test.GetBaseURL(port) 52 mux := mux.NewRouter() 53 54 mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { 55 _, err := w.Write([]byte("{}")) 56 if err != nil { 57 return 58 } 59 }).Methods(http.MethodGet) 60 61 for _, routeHandler := range routes { 62 mux.HandleFunc(routeHandler.Route, routeHandler.HandlerFunc).Methods(routeHandler.AllowedMethods...) 63 } 64 65 server := &http.Server{ //nolint:gosec 66 Addr: fmt.Sprintf(":%s", port), 67 Handler: mux, 68 } 69 70 go func() { 71 if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { 72 return 73 } 74 }() 75 76 test.WaitTillServerReady(baseURL + "/test") 77 78 return server 79 } 80 81 func TestDoHTTPRequest(t *testing.T) { 82 Convey("doHTTPRequest nil result pointer", t, func() { 83 port := test.GetFreePort() 84 server := StartTestHTTPServer(nil, port) 85 defer server.Close() 86 87 url := fmt.Sprintf("http://127.0.0.1:%s/asd", port) 88 req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, nil) 89 So(err, ShouldBeNil) 90 91 So(func() { _, _ = doHTTPRequest(req, false, false, nil, io.Discard) }, ShouldNotPanic) 92 }) 93 94 Convey("doHTTPRequest bad return json", t, func() { 95 port := test.GetFreePort() 96 server := StartTestHTTPServer(HTTPRoutes{ 97 { 98 Route: "/test", 99 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 100 _, err := w.Write([]byte("bad json")) 101 if err != nil { 102 return 103 } 104 }, 105 AllowedMethods: []string{http.MethodGet}, 106 }, 107 }, port) 108 defer server.Close() 109 110 url := fmt.Sprintf("http://127.0.0.1:%s/test", port) 111 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 112 So(err, ShouldBeNil) 113 114 So(func() { _, _ = doHTTPRequest(req, false, false, &ispec.Manifest{}, io.Discard) }, ShouldNotPanic) 115 }) 116 117 Convey("makeGraphQLRequest bad request context", t, func() { 118 err := makeGraphQLRequest(nil, "", "", "", "", false, false, nil, io.Discard) //nolint:staticcheck 119 So(err, ShouldNotBeNil) 120 }) 121 122 Convey("makeHEADRequest bad request context", t, func() { 123 _, err := makeHEADRequest(nil, "", "", "", false, false) //nolint:staticcheck 124 So(err, ShouldNotBeNil) 125 }) 126 127 Convey("makeGETRequest bad request context", t, func() { 128 _, err := makeGETRequest(nil, "", "", "", false, false, nil, io.Discard) //nolint:staticcheck 129 So(err, ShouldNotBeNil) 130 }) 131 132 Convey("fetchImageManifestStruct errors", t, func() { 133 port := test.GetFreePort() 134 baseURL := test.GetBaseURL(port) 135 searchConf := getDefaultSearchConf(baseURL) 136 137 // 404 erorr will appear 138 server := StartTestHTTPServer(HTTPRoutes{}, port) 139 defer server.Close() 140 141 URL := baseURL + "/v2/repo/manifests/tag" 142 143 _, err := fetchImageManifestStruct(context.Background(), &httpJob{ 144 url: URL, 145 username: "", 146 password: "", 147 imageName: "repo", 148 tagName: "tag", 149 config: searchConf, 150 }) 151 152 So(err, ShouldNotBeNil) 153 }) 154 155 Convey("fetchManifestStruct errors", t, func() { 156 port := test.GetFreePort() 157 baseURL := test.GetBaseURL(port) 158 searchConf := getDefaultSearchConf(baseURL) 159 160 Convey("makeGETRequest manifest error, context is done", func() { 161 server := StartTestHTTPServer(HTTPRoutes{}, port) 162 defer server.Close() 163 164 ctx, cancel := context.WithCancel(context.Background()) 165 166 cancel() 167 168 _, err := fetchManifestStruct(ctx, "repo", "tag", searchConf, 169 "", "") 170 171 So(err, ShouldNotBeNil) 172 }) 173 174 Convey("makeGETRequest manifest error, context is not done", func() { 175 server := StartTestHTTPServer(HTTPRoutes{}, port) 176 defer server.Close() 177 178 _, err := fetchManifestStruct(context.Background(), "repo", "tag", searchConf, 179 "", "") 180 181 So(err, ShouldNotBeNil) 182 }) 183 184 Convey("makeGETRequest config error, context is not done", func() { 185 server := StartTestHTTPServer(HTTPRoutes{ 186 { 187 Route: "/v2/{name}/manifests/{reference}", 188 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 189 _, err := w.Write([]byte(`{"config":{"digest":"digest","size":0}}`)) 190 if err != nil { 191 return 192 } 193 }, 194 AllowedMethods: []string{http.MethodGet}, 195 }, 196 }, port) 197 defer server.Close() 198 199 _, err := fetchManifestStruct(context.Background(), "repo", "tag", searchConf, 200 "", "") 201 202 So(err, ShouldNotBeNil) 203 }) 204 205 Convey("Platforms on config", func() { 206 server := StartTestHTTPServer(HTTPRoutes{ 207 { 208 Route: "/v2/{name}/manifests/{reference}", 209 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 210 _, err := w.Write([]byte(` 211 { 212 "config":{ 213 "digest":"digest", 214 "size":0, 215 "platform" : { 216 "os": "", 217 "architecture": "", 218 "variant": "" 219 } 220 } 221 } 222 `)) 223 if err != nil { 224 return 225 } 226 }, 227 AllowedMethods: []string{http.MethodGet}, 228 }, 229 { 230 Route: "/v2/{name}/blobs/{digest}", 231 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 232 _, err := w.Write([]byte(` 233 { 234 "architecture": "arch", 235 "os": "os", 236 "variant": "var" 237 } 238 `)) 239 if err != nil { 240 return 241 } 242 }, 243 AllowedMethods: []string{http.MethodGet}, 244 }, 245 }, port) 246 defer server.Close() 247 248 _, err := fetchManifestStruct(context.Background(), "repo", "tag", searchConf, 249 "", "") 250 251 So(err, ShouldBeNil) 252 }) 253 254 Convey("isNotationSigned error", func() { 255 isSigned := isNotationSigned(context.Background(), "repo", "digest", searchConf, 256 "", "") 257 So(isSigned, ShouldBeFalse) 258 }) 259 260 Convey("fetchImageIndexStruct no errors", func() { 261 server := StartTestHTTPServer(HTTPRoutes{ 262 { 263 Route: "/v2/{name}/manifests/{reference}", 264 HandlerFunc: func(writer http.ResponseWriter, req *http.Request) { 265 vars := mux.Vars(req) 266 267 if vars["reference"] == "indexRef" { 268 writer.Header().Add("docker-content-digest", godigest.FromString("t").String()) 269 _, err := writer.Write([]byte(` 270 { 271 "manifests": [ 272 { 273 "digest": "manifestRef", 274 "platform": { 275 "architecture": "arch", 276 "os": "os", 277 "variant": "var" 278 } 279 } 280 ] 281 } 282 `)) 283 if err != nil { 284 return 285 } 286 } else if vars["reference"] == "manifestRef" { 287 _, err := writer.Write([]byte(` 288 { 289 "config":{ 290 "digest":"digest", 291 "size":0 292 } 293 } 294 `)) 295 if err != nil { 296 return 297 } 298 } 299 }, 300 AllowedMethods: []string{http.MethodGet}, 301 }, 302 { 303 Route: "/v2/{name}/blobs/{digest}", 304 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 305 _, err := w.Write([]byte(`{}`)) 306 if err != nil { 307 return 308 } 309 }, 310 AllowedMethods: []string{http.MethodGet}, 311 }, 312 }, port) 313 defer server.Close() 314 315 URL := baseURL + "/v2/repo/manifests/indexRef" 316 317 imageStruct, err := fetchImageIndexStruct(context.Background(), &httpJob{ 318 url: URL, 319 username: "", 320 password: "", 321 imageName: "repo", 322 tagName: "tag", 323 config: searchConf, 324 }) 325 So(err, ShouldBeNil) 326 So(imageStruct, ShouldNotBeNil) 327 }) 328 329 Convey("fetchImageIndexStruct makeGETRequest errors context done", func() { 330 server := StartTestHTTPServer(HTTPRoutes{}, port) 331 defer server.Close() 332 333 ctx, cancel := context.WithCancel(context.Background()) 334 335 cancel() 336 337 URL := baseURL + "/v2/repo/manifests/indexRef" 338 339 imageStruct, err := fetchImageIndexStruct(ctx, &httpJob{ 340 url: URL, 341 username: "", 342 password: "", 343 imageName: "repo", 344 tagName: "tag", 345 config: searchConf, 346 }) 347 So(err, ShouldNotBeNil) 348 So(imageStruct, ShouldBeNil) 349 }) 350 351 Convey("fetchImageIndexStruct makeGETRequest errors context not done", func() { 352 server := StartTestHTTPServer(HTTPRoutes{}, port) 353 defer server.Close() 354 355 URL := baseURL + "/v2/repo/manifests/indexRef" 356 357 imageStruct, err := fetchImageIndexStruct(context.Background(), &httpJob{ 358 url: URL, 359 username: "", 360 password: "", 361 imageName: "repo", 362 tagName: "tag", 363 config: searchConf, 364 }) 365 So(err, ShouldNotBeNil) 366 So(imageStruct, ShouldBeNil) 367 }) 368 }) 369 } 370 371 func TestDoJobErrors(t *testing.T) { 372 port := test.GetFreePort() 373 baseURL := test.GetBaseURL(port) 374 searchConf := getDefaultSearchConf(baseURL) 375 376 reqPool := &requestsPool{ 377 jobs: make(chan *httpJob), 378 done: make(chan struct{}), 379 wtgrp: &sync.WaitGroup{}, 380 outputCh: make(chan stringResult), 381 } 382 383 Convey("Do Job errors", t, func() { 384 reqPool.wtgrp.Add(1) 385 386 Convey("Do Job makeHEADRequest error context done", func() { 387 server := StartTestHTTPServer(HTTPRoutes{}, port) 388 defer server.Close() 389 390 URL := baseURL + "/v2/repo/manifests/manifestRef" 391 392 ctx, cancel := context.WithCancel(context.Background()) 393 394 cancel() 395 396 reqPool.doJob(ctx, &httpJob{ 397 url: URL, 398 username: "", 399 password: "", 400 imageName: "", 401 tagName: "", 402 config: searchConf, 403 }) 404 }) 405 406 Convey("Do Job makeHEADRequest error context not done", func() { 407 server := StartTestHTTPServer(HTTPRoutes{}, port) 408 defer server.Close() 409 410 URL := baseURL + "/v2/repo/manifests/manifestRef" 411 412 ctx := context.Background() 413 414 go reqPool.doJob(ctx, &httpJob{ 415 url: URL, 416 username: "", 417 password: "", 418 imageName: "", 419 tagName: "", 420 config: searchConf, 421 }) 422 423 result := <-reqPool.outputCh 424 So(result.Err, ShouldNotBeNil) 425 So(result.StrValue, ShouldResemble, "") 426 }) 427 428 Convey("Do Job fetchManifestStruct errors context canceled", func() { 429 server := StartTestHTTPServer(HTTPRoutes{ 430 { 431 Route: "/v2/{name}/manifests/{reference}", 432 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 433 w.Header().Add("Content-Type", ispec.MediaTypeImageManifest) 434 _, err := w.Write([]byte("")) 435 if err != nil { 436 return 437 } 438 }, 439 AllowedMethods: []string{http.MethodHead}, 440 }, 441 }, port) 442 defer server.Close() 443 444 URL := baseURL + "/v2/repo/manifests/manifestRef" 445 446 ctx, cancel := context.WithCancel(context.Background()) 447 448 cancel() 449 // context not canceled 450 451 reqPool.doJob(ctx, &httpJob{ 452 url: URL, 453 username: "", 454 password: "", 455 imageName: "", 456 tagName: "", 457 config: searchConf, 458 }) 459 }) 460 461 Convey("Do Job fetchManifestStruct errors context not canceled", func() { 462 server := StartTestHTTPServer(HTTPRoutes{ 463 { 464 Route: "/v2/{name}/manifests/{reference}", 465 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 466 w.Header().Add("Content-Type", ispec.MediaTypeImageManifest) 467 _, err := w.Write([]byte("")) 468 if err != nil { 469 return 470 } 471 }, 472 AllowedMethods: []string{http.MethodHead}, 473 }, 474 }, port) 475 defer server.Close() 476 477 URL := baseURL + "/v2/repo/manifests/manifestRef" 478 479 ctx := context.Background() 480 481 go reqPool.doJob(ctx, &httpJob{ 482 url: URL, 483 username: "", 484 password: "", 485 imageName: "", 486 tagName: "", 487 config: searchConf, 488 }) 489 490 result := <-reqPool.outputCh 491 So(result.Err, ShouldNotBeNil) 492 So(result.StrValue, ShouldResemble, "") 493 }) 494 495 Convey("Do Job fetchIndexStruct errors context canceled", func() { 496 server := StartTestHTTPServer(HTTPRoutes{ 497 { 498 Route: "/v2/{name}/manifests/{reference}", 499 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 500 w.Header().Add("Content-Type", ispec.MediaTypeImageIndex) 501 _, err := w.Write([]byte("")) 502 if err != nil { 503 return 504 } 505 }, 506 AllowedMethods: []string{http.MethodHead}, 507 }, 508 }, port) 509 defer server.Close() 510 511 URL := baseURL + "/v2/repo/manifests/indexRef" 512 513 ctx, cancel := context.WithCancel(context.Background()) 514 515 cancel() 516 // context not canceled 517 518 reqPool.doJob(ctx, &httpJob{ 519 url: URL, 520 username: "", 521 password: "", 522 imageName: "", 523 tagName: "", 524 config: searchConf, 525 }) 526 }) 527 528 Convey("Do Job fetchIndexStruct errors context not canceled", func() { 529 server := StartTestHTTPServer(HTTPRoutes{ 530 { 531 Route: "/v2/{name}/manifests/{reference}", 532 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 533 w.Header().Add("Content-Type", ispec.MediaTypeImageIndex) 534 _, err := w.Write([]byte("")) 535 if err != nil { 536 return 537 } 538 }, 539 AllowedMethods: []string{http.MethodHead}, 540 }, 541 }, port) 542 defer server.Close() 543 544 URL := baseURL + "/v2/repo/manifests/indexRef" 545 546 ctx := context.Background() 547 548 go reqPool.doJob(ctx, &httpJob{ 549 url: URL, 550 username: "", 551 password: "", 552 imageName: "", 553 tagName: "", 554 config: searchConf, 555 }) 556 557 result := <-reqPool.outputCh 558 So(result.Err, ShouldNotBeNil) 559 So(result.StrValue, ShouldResemble, "") 560 }) 561 Convey("Do Job fetchIndexStruct not supported content type", func() { 562 server := StartTestHTTPServer(HTTPRoutes{ 563 { 564 Route: "/v2/{name}/manifests/{reference}", 565 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 566 w.Header().Add("Content-Type", "some-media-type") 567 _, err := w.Write([]byte("")) 568 if err != nil { 569 return 570 } 571 }, 572 AllowedMethods: []string{http.MethodHead}, 573 }, 574 }, port) 575 defer server.Close() 576 577 URL := baseURL + "/v2/repo/manifests/indexRef" 578 579 ctx := context.Background() 580 581 reqPool.doJob(ctx, &httpJob{ 582 url: URL, 583 username: "", 584 password: "", 585 imageName: "", 586 tagName: "", 587 config: searchConf, 588 }) 589 }) 590 591 Convey("Media type is MediaTypeImageIndex image.string erorrs", func() { 592 server := StartTestHTTPServer(HTTPRoutes{ 593 { 594 Route: "/v2/{name}/manifests/{reference}", 595 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 596 w.Header().Add("Content-Type", ispec.MediaTypeImageIndex) 597 w.Header().Add("docker-content-digest", godigest.FromString("t").String()) 598 599 _, err := w.Write([]byte("")) 600 if err != nil { 601 return 602 } 603 }, 604 AllowedMethods: []string{http.MethodHead}, 605 }, 606 { 607 Route: "/v2/{name}/manifests/{reference}", 608 HandlerFunc: func(writer http.ResponseWriter, req *http.Request) { 609 vars := mux.Vars(req) 610 611 if vars["reference"] == "indexRef" { 612 writer.Header().Add("docker-content-digest", godigest.FromString("t").String()) 613 614 _, err := writer.Write([]byte(`{"manifests": [{"digest": "manifestRef"}]}`)) 615 if err != nil { 616 return 617 } 618 } 619 620 if vars["reference"] == "manifestRef" { 621 writer.Header().Add("docker-content-digest", godigest.FromString("t").String()) 622 623 _, err := writer.Write([]byte(`{"config": {"digest": "confDigest"}}`)) 624 if err != nil { 625 return 626 } 627 } 628 }, 629 AllowedMethods: []string{http.MethodGet}, 630 }, 631 { 632 Route: "/v2/{name}/blobs/{digest}", 633 HandlerFunc: func(w http.ResponseWriter, r *http.Request) { 634 _, err := w.Write([]byte(`{}`)) 635 if err != nil { 636 return 637 } 638 }, 639 AllowedMethods: []string{http.MethodGet}, 640 }, 641 }, port) 642 defer server.Close() 643 URL := baseURL + "/v2/repo/manifests/indexRef" 644 645 go reqPool.doJob(context.Background(), &httpJob{ 646 url: URL, 647 username: "", 648 password: "", 649 imageName: "repo", 650 tagName: "indexRef", 651 config: searchConf, 652 }) 653 654 result := <-reqPool.outputCh 655 So(result.Err, ShouldNotBeNil) 656 So(result.StrValue, ShouldResemble, "") 657 }) 658 }) 659 }