github.com/rish1988/moby@v25.0.2+incompatible/registry/search_test.go (about)

     1  package registry
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"net/http/httputil"
     9  	"testing"
    10  
    11  	"github.com/docker/distribution/registry/client/transport"
    12  	"github.com/docker/docker/api/types/filters"
    13  	"github.com/docker/docker/api/types/registry"
    14  	"github.com/docker/docker/errdefs"
    15  	"gotest.tools/v3/assert"
    16  )
    17  
    18  func spawnTestRegistrySession(t *testing.T) *session {
    19  	authConfig := &registry.AuthConfig{}
    20  	endpoint, err := newV1Endpoint(makeIndex("/v1/"), nil)
    21  	if err != nil {
    22  		t.Fatal(err)
    23  	}
    24  	userAgent := "docker test client"
    25  	var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log}
    26  	tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...)
    27  	client := httpClient(tr)
    28  
    29  	if err := authorizeClient(client, authConfig, endpoint); err != nil {
    30  		t.Fatal(err)
    31  	}
    32  	r := newSession(client, endpoint)
    33  
    34  	// In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true`
    35  	// header while authenticating, in order to retrieve a token that can be later used to
    36  	// perform authenticated actions.
    37  	//
    38  	// The mock v1 registry does not support that, (TODO(tiborvass): support it), instead,
    39  	// it will consider authenticated any request with the header `X-Docker-Token: fake-token`.
    40  	//
    41  	// Because we know that the client's transport is an `*authTransport` we simply cast it,
    42  	// in order to set the internal cached token to the fake token, and thus send that fake token
    43  	// upon every subsequent requests.
    44  	r.client.Transport.(*authTransport).token = []string{"fake-token"}
    45  	return r
    46  }
    47  
    48  type debugTransport struct {
    49  	http.RoundTripper
    50  	log func(...interface{})
    51  }
    52  
    53  func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    54  	dump, err := httputil.DumpRequestOut(req, false)
    55  	if err != nil {
    56  		tr.log("could not dump request")
    57  	}
    58  	tr.log(string(dump))
    59  	resp, err := tr.RoundTripper.RoundTrip(req)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	dump, err = httputil.DumpResponse(resp, false)
    64  	if err != nil {
    65  		tr.log("could not dump response")
    66  	}
    67  	tr.log(string(dump))
    68  	return resp, err
    69  }
    70  
    71  func TestSearchRepositories(t *testing.T) {
    72  	r := spawnTestRegistrySession(t)
    73  	results, err := r.searchRepositories("fakequery", 25)
    74  	if err != nil {
    75  		t.Fatal(err)
    76  	}
    77  	if results == nil {
    78  		t.Fatal("Expected non-nil SearchResults object")
    79  	}
    80  	assert.Equal(t, results.NumResults, 1, "Expected 1 search results")
    81  	assert.Equal(t, results.Query, "fakequery", "Expected 'fakequery' as query")
    82  	assert.Equal(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars")
    83  }
    84  
    85  func TestSearchErrors(t *testing.T) {
    86  	errorCases := []struct {
    87  		filtersArgs       filters.Args
    88  		shouldReturnError bool
    89  		expectedError     string
    90  	}{
    91  		{
    92  			expectedError:     "Unexpected status code 500",
    93  			shouldReturnError: true,
    94  		},
    95  		{
    96  			filtersArgs:   filters.NewArgs(filters.Arg("type", "custom")),
    97  			expectedError: "invalid filter 'type'",
    98  		},
    99  		{
   100  			filtersArgs:   filters.NewArgs(filters.Arg("is-automated", "invalid")),
   101  			expectedError: "invalid filter 'is-automated=[invalid]'",
   102  		},
   103  		{
   104  			filtersArgs: filters.NewArgs(
   105  				filters.Arg("is-automated", "true"),
   106  				filters.Arg("is-automated", "false"),
   107  			),
   108  			expectedError: "invalid filter 'is-automated",
   109  		},
   110  		{
   111  			filtersArgs:   filters.NewArgs(filters.Arg("is-official", "invalid")),
   112  			expectedError: "invalid filter 'is-official=[invalid]'",
   113  		},
   114  		{
   115  			filtersArgs: filters.NewArgs(
   116  				filters.Arg("is-official", "true"),
   117  				filters.Arg("is-official", "false"),
   118  			),
   119  			expectedError: "invalid filter 'is-official",
   120  		},
   121  		{
   122  			filtersArgs:   filters.NewArgs(filters.Arg("stars", "invalid")),
   123  			expectedError: "invalid filter 'stars=invalid'",
   124  		},
   125  		{
   126  			filtersArgs: filters.NewArgs(
   127  				filters.Arg("stars", "1"),
   128  				filters.Arg("stars", "invalid"),
   129  			),
   130  			expectedError: "invalid filter 'stars=invalid'",
   131  		},
   132  	}
   133  	for _, tc := range errorCases {
   134  		tc := tc
   135  		t.Run(tc.expectedError, func(t *testing.T) {
   136  			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   137  				if !tc.shouldReturnError {
   138  					t.Errorf("unexpected HTTP request")
   139  				}
   140  				http.Error(w, "no search for you", http.StatusInternalServerError)
   141  			}))
   142  			defer srv.Close()
   143  
   144  			// Construct the search term by cutting the 'http://' prefix off srv.URL.
   145  			term := srv.URL[7:] + "/term"
   146  
   147  			reg, err := NewService(ServiceOptions{})
   148  			assert.NilError(t, err)
   149  			_, err = reg.Search(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{})
   150  			assert.ErrorContains(t, err, tc.expectedError)
   151  			if tc.shouldReturnError {
   152  				assert.Check(t, errdefs.IsUnknown(err), "got: %T: %v", err, err)
   153  				return
   154  			}
   155  			assert.Check(t, errdefs.IsInvalidParameter(err), "got: %T: %v", err, err)
   156  		})
   157  	}
   158  }
   159  
   160  func TestSearch(t *testing.T) {
   161  	const term = "term"
   162  	successCases := []struct {
   163  		name            string
   164  		filtersArgs     filters.Args
   165  		registryResults []registry.SearchResult
   166  		expectedResults []registry.SearchResult
   167  	}{
   168  		{
   169  			name:            "empty results",
   170  			registryResults: []registry.SearchResult{},
   171  			expectedResults: []registry.SearchResult{},
   172  		},
   173  		{
   174  			name: "no filter",
   175  			registryResults: []registry.SearchResult{
   176  				{
   177  					Name:        "name",
   178  					Description: "description",
   179  				},
   180  			},
   181  			expectedResults: []registry.SearchResult{
   182  				{
   183  					Name:        "name",
   184  					Description: "description",
   185  				},
   186  			},
   187  		},
   188  		{
   189  			name:        "is-automated=true, no results",
   190  			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
   191  			registryResults: []registry.SearchResult{
   192  				{
   193  					Name:        "name",
   194  					Description: "description",
   195  				},
   196  			},
   197  			expectedResults: []registry.SearchResult{},
   198  		},
   199  		{
   200  			name:        "is-automated=true",
   201  			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
   202  			registryResults: []registry.SearchResult{
   203  				{
   204  					Name:        "name",
   205  					Description: "description",
   206  					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
   207  				},
   208  			},
   209  			expectedResults: []registry.SearchResult{
   210  				{
   211  					Name:        "name",
   212  					Description: "description",
   213  					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
   214  				},
   215  			},
   216  		},
   217  		{
   218  			name:        "is-automated=false, no results",
   219  			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
   220  			registryResults: []registry.SearchResult{
   221  				{
   222  					Name:        "name",
   223  					Description: "description",
   224  					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
   225  				},
   226  			},
   227  			expectedResults: []registry.SearchResult{},
   228  		},
   229  		{
   230  			name:        "is-automated=false",
   231  			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
   232  			registryResults: []registry.SearchResult{
   233  				{
   234  					Name:        "name",
   235  					Description: "description",
   236  				},
   237  			},
   238  			expectedResults: []registry.SearchResult{
   239  				{
   240  					Name:        "name",
   241  					Description: "description",
   242  				},
   243  			},
   244  		},
   245  		{
   246  			name:        "is-official=true, no results",
   247  			filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
   248  			registryResults: []registry.SearchResult{
   249  				{
   250  					Name:        "name",
   251  					Description: "description",
   252  				},
   253  			},
   254  			expectedResults: []registry.SearchResult{},
   255  		},
   256  		{
   257  			name:        "is-official=true",
   258  			filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
   259  			registryResults: []registry.SearchResult{
   260  				{
   261  					Name:        "name",
   262  					Description: "description",
   263  					IsOfficial:  true,
   264  				},
   265  			},
   266  			expectedResults: []registry.SearchResult{
   267  				{
   268  					Name:        "name",
   269  					Description: "description",
   270  					IsOfficial:  true,
   271  				},
   272  			},
   273  		},
   274  		{
   275  			name:        "is-official=false, no results",
   276  			filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
   277  			registryResults: []registry.SearchResult{
   278  				{
   279  					Name:        "name",
   280  					Description: "description",
   281  					IsOfficial:  true,
   282  				},
   283  			},
   284  			expectedResults: []registry.SearchResult{},
   285  		},
   286  		{
   287  			name:        "is-official=false",
   288  			filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
   289  			registryResults: []registry.SearchResult{
   290  				{
   291  					Name:        "name",
   292  					Description: "description",
   293  					IsOfficial:  false,
   294  				},
   295  			},
   296  			expectedResults: []registry.SearchResult{
   297  				{
   298  					Name:        "name",
   299  					Description: "description",
   300  					IsOfficial:  false,
   301  				},
   302  			},
   303  		},
   304  		{
   305  			name:        "stars=0",
   306  			filtersArgs: filters.NewArgs(filters.Arg("stars", "0")),
   307  			registryResults: []registry.SearchResult{
   308  				{
   309  					Name:        "name",
   310  					Description: "description",
   311  					StarCount:   0,
   312  				},
   313  			},
   314  			expectedResults: []registry.SearchResult{
   315  				{
   316  					Name:        "name",
   317  					Description: "description",
   318  					StarCount:   0,
   319  				},
   320  			},
   321  		},
   322  		{
   323  			name:        "stars=0, no results",
   324  			filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
   325  			registryResults: []registry.SearchResult{
   326  				{
   327  					Name:        "name",
   328  					Description: "description",
   329  					StarCount:   0,
   330  				},
   331  			},
   332  			expectedResults: []registry.SearchResult{},
   333  		},
   334  		{
   335  			name:        "stars=1",
   336  			filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
   337  			registryResults: []registry.SearchResult{
   338  				{
   339  					Name:        "name0",
   340  					Description: "description0",
   341  					StarCount:   0,
   342  				},
   343  				{
   344  					Name:        "name1",
   345  					Description: "description1",
   346  					StarCount:   1,
   347  				},
   348  			},
   349  			expectedResults: []registry.SearchResult{
   350  				{
   351  					Name:        "name1",
   352  					Description: "description1",
   353  					StarCount:   1,
   354  				},
   355  			},
   356  		},
   357  		{
   358  			name: "stars=1, is-official=true, is-automated=true",
   359  			filtersArgs: filters.NewArgs(
   360  				filters.Arg("stars", "1"),
   361  				filters.Arg("is-official", "true"),
   362  				filters.Arg("is-automated", "true"),
   363  			),
   364  			registryResults: []registry.SearchResult{
   365  				{
   366  					Name:        "name0",
   367  					Description: "description0",
   368  					StarCount:   0,
   369  					IsOfficial:  true,
   370  					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
   371  				},
   372  				{
   373  					Name:        "name1",
   374  					Description: "description1",
   375  					StarCount:   1,
   376  					IsOfficial:  false,
   377  					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
   378  				},
   379  				{
   380  					Name:        "name2",
   381  					Description: "description2",
   382  					StarCount:   1,
   383  					IsOfficial:  true,
   384  				},
   385  				{
   386  					Name:        "name3",
   387  					Description: "description3",
   388  					StarCount:   2,
   389  					IsOfficial:  true,
   390  					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
   391  				},
   392  			},
   393  			expectedResults: []registry.SearchResult{
   394  				{
   395  					Name:        "name3",
   396  					Description: "description3",
   397  					StarCount:   2,
   398  					IsOfficial:  true,
   399  					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
   400  				},
   401  			},
   402  		},
   403  	}
   404  	for _, tc := range successCases {
   405  		tc := tc
   406  		t.Run(tc.name, func(t *testing.T) {
   407  			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   408  				w.Header().Set("Content-type", "application/json")
   409  				json.NewEncoder(w).Encode(registry.SearchResults{
   410  					Query:      term,
   411  					NumResults: len(tc.registryResults),
   412  					Results:    tc.registryResults,
   413  				})
   414  			}))
   415  			defer srv.Close()
   416  
   417  			// Construct the search term by cutting the 'http://' prefix off srv.URL.
   418  			searchTerm := srv.URL[7:] + "/" + term
   419  
   420  			reg, err := NewService(ServiceOptions{})
   421  			assert.NilError(t, err)
   422  			results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{})
   423  			assert.NilError(t, err)
   424  			assert.DeepEqual(t, results, tc.expectedResults)
   425  		})
   426  	}
   427  }