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  }