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 := ®istry.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 }