| package registry |
| |
| import ( |
| "context" |
| "encoding/json" |
| "net/http" |
| "net/http/httptest" |
| "net/http/httputil" |
| "testing" |
| |
| cerrdefs "github.com/containerd/errdefs" |
| "github.com/docker/distribution/registry/client/transport" |
| "github.com/docker/docker/api/types/filters" |
| "github.com/docker/docker/api/types/registry" |
| "gotest.tools/v3/assert" |
| ) |
| |
| func spawnTestRegistrySession(t *testing.T) *session { |
| authConfig := ®istry.AuthConfig{} |
| endpoint, err := newV1Endpoint(context.Background(), makeIndex("/v1/"), nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| userAgent := "docker test client" |
| var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log} |
| tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...) |
| client := httpClient(tr) |
| |
| if err := authorizeClient(context.Background(), client, authConfig, endpoint); err != nil { |
| t.Fatal(err) |
| } |
| r := newSession(client, endpoint) |
| |
| // In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true` |
| // header while authenticating, in order to retrieve a token that can be later used to |
| // perform authenticated actions. |
| // |
| // The mock v1 registry does not support that, (TODO(tiborvass): support it), instead, |
| // it will consider authenticated any request with the header `X-Docker-Token: fake-token`. |
| // |
| // Because we know that the client's transport is an `*authTransport` we simply cast it, |
| // in order to set the internal cached token to the fake token, and thus send that fake token |
| // upon every subsequent requests. |
| r.client.Transport.(*authTransport).token = []string{"fake-token"} |
| return r |
| } |
| |
| type debugTransport struct { |
| http.RoundTripper |
| log func(...interface{}) |
| } |
| |
| func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { |
| dump, err := httputil.DumpRequestOut(req, false) |
| if err != nil { |
| tr.log("could not dump request") |
| } |
| tr.log(string(dump)) |
| resp, err := tr.RoundTripper.RoundTrip(req) |
| if err != nil { |
| return nil, err |
| } |
| dump, err = httputil.DumpResponse(resp, false) |
| if err != nil { |
| tr.log("could not dump response") |
| } |
| tr.log(string(dump)) |
| return resp, err |
| } |
| |
| func TestSearchRepositories(t *testing.T) { |
| r := spawnTestRegistrySession(t) |
| results, err := r.searchRepositories(context.Background(), "fakequery", 25) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if results == nil { |
| t.Fatal("Expected non-nil SearchResults object") |
| } |
| assert.Equal(t, results.NumResults, 1, "Expected 1 search results") |
| assert.Equal(t, results.Query, "fakequery", "Expected 'fakequery' as query") |
| assert.Equal(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars") |
| } |
| |
| func TestSearchErrors(t *testing.T) { |
| errorCases := []struct { |
| filtersArgs filters.Args |
| shouldReturnError bool |
| expectedError string |
| }{ |
| { |
| expectedError: "Unexpected status code 500", |
| shouldReturnError: true, |
| }, |
| { |
| filtersArgs: filters.NewArgs(filters.Arg("type", "custom")), |
| expectedError: "invalid filter 'type'", |
| }, |
| { |
| filtersArgs: filters.NewArgs(filters.Arg("is-automated", "invalid")), |
| expectedError: "invalid filter 'is-automated=[invalid]'", |
| }, |
| { |
| filtersArgs: filters.NewArgs( |
| filters.Arg("is-automated", "true"), |
| filters.Arg("is-automated", "false"), |
| ), |
| expectedError: "invalid filter 'is-automated", |
| }, |
| { |
| filtersArgs: filters.NewArgs(filters.Arg("is-official", "invalid")), |
| expectedError: "invalid filter 'is-official=[invalid]'", |
| }, |
| { |
| filtersArgs: filters.NewArgs( |
| filters.Arg("is-official", "true"), |
| filters.Arg("is-official", "false"), |
| ), |
| expectedError: "invalid filter 'is-official", |
| }, |
| { |
| filtersArgs: filters.NewArgs(filters.Arg("stars", "invalid")), |
| expectedError: "invalid filter 'stars=invalid'", |
| }, |
| { |
| filtersArgs: filters.NewArgs( |
| filters.Arg("stars", "1"), |
| filters.Arg("stars", "invalid"), |
| ), |
| expectedError: "invalid filter 'stars=invalid'", |
| }, |
| } |
| for _, tc := range errorCases { |
| t.Run(tc.expectedError, func(t *testing.T) { |
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| if !tc.shouldReturnError { |
| t.Errorf("unexpected HTTP request") |
| } |
| http.Error(w, "no search for you", http.StatusInternalServerError) |
| })) |
| defer srv.Close() |
| |
| // Construct the search term by cutting the 'http://' prefix off srv.URL. |
| term := srv.URL[7:] + "/term" |
| |
| reg, err := NewService(ServiceOptions{}) |
| assert.NilError(t, err) |
| _, err = reg.Search(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{}) |
| assert.ErrorContains(t, err, tc.expectedError) |
| if tc.shouldReturnError { |
| assert.Check(t, cerrdefs.IsUnknown(err), "got: %T: %v", err, err) |
| return |
| } |
| assert.Check(t, cerrdefs.IsInvalidArgument(err), "got: %T: %v", err, err) |
| }) |
| } |
| } |
| |
| func TestSearch(t *testing.T) { |
| const term = "term" |
| successCases := []struct { |
| name string |
| filtersArgs filters.Args |
| registryResults []registry.SearchResult |
| expectedResults []registry.SearchResult |
| }{ |
| { |
| name: "empty results", |
| registryResults: []registry.SearchResult{}, |
| expectedResults: []registry.SearchResult{}, |
| }, |
| { |
| name: "no filter", |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| }, |
| }, |
| expectedResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| }, |
| }, |
| }, |
| { |
| name: "is-automated=true, no results", |
| filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| }, |
| }, |
| expectedResults: []registry.SearchResult{}, |
| }, |
| { |
| name: "is-automated=true", |
| filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
| }, |
| }, |
| expectedResults: []registry.SearchResult{}, |
| }, |
| { |
| name: "is-automated=false, IsAutomated reset to false", |
| filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
| }, |
| }, |
| expectedResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| IsAutomated: false, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
| }, |
| }, |
| }, |
| { |
| name: "is-automated=false", |
| filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| }, |
| }, |
| expectedResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| }, |
| }, |
| }, |
| { |
| name: "is-official=true, no results", |
| filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| }, |
| }, |
| expectedResults: []registry.SearchResult{}, |
| }, |
| { |
| name: "is-official=true", |
| filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| IsOfficial: true, |
| }, |
| }, |
| expectedResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| IsOfficial: true, |
| }, |
| }, |
| }, |
| { |
| name: "is-official=false, no results", |
| filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| IsOfficial: true, |
| }, |
| }, |
| expectedResults: []registry.SearchResult{}, |
| }, |
| { |
| name: "is-official=false", |
| filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| IsOfficial: false, |
| }, |
| }, |
| expectedResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| IsOfficial: false, |
| }, |
| }, |
| }, |
| { |
| name: "stars=0", |
| filtersArgs: filters.NewArgs(filters.Arg("stars", "0")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| StarCount: 0, |
| }, |
| }, |
| expectedResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| StarCount: 0, |
| }, |
| }, |
| }, |
| { |
| name: "stars=0, no results", |
| filtersArgs: filters.NewArgs(filters.Arg("stars", "1")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name", |
| Description: "description", |
| StarCount: 0, |
| }, |
| }, |
| expectedResults: []registry.SearchResult{}, |
| }, |
| { |
| name: "stars=1", |
| filtersArgs: filters.NewArgs(filters.Arg("stars", "1")), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name0", |
| Description: "description0", |
| StarCount: 0, |
| }, |
| { |
| Name: "name1", |
| Description: "description1", |
| StarCount: 1, |
| }, |
| }, |
| expectedResults: []registry.SearchResult{ |
| { |
| Name: "name1", |
| Description: "description1", |
| StarCount: 1, |
| }, |
| }, |
| }, |
| { |
| name: "stars=1, is-official=true, is-automated=true", |
| filtersArgs: filters.NewArgs( |
| filters.Arg("stars", "1"), |
| filters.Arg("is-official", "true"), |
| filters.Arg("is-automated", "true"), |
| ), |
| registryResults: []registry.SearchResult{ |
| { |
| Name: "name0", |
| Description: "description0", |
| StarCount: 0, |
| IsOfficial: true, |
| IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
| }, |
| { |
| Name: "name1", |
| Description: "description1", |
| StarCount: 1, |
| IsOfficial: false, |
| IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
| }, |
| { |
| Name: "name2", |
| Description: "description2", |
| StarCount: 1, |
| IsOfficial: true, |
| }, |
| { |
| Name: "name3", |
| Description: "description3", |
| StarCount: 2, |
| IsOfficial: true, |
| IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). |
| }, |
| }, |
| expectedResults: []registry.SearchResult{}, |
| }, |
| } |
| for _, tc := range successCases { |
| t.Run(tc.name, func(t *testing.T) { |
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-type", "application/json") |
| json.NewEncoder(w).Encode(registry.SearchResults{ |
| Query: term, |
| NumResults: len(tc.registryResults), |
| Results: tc.registryResults, |
| }) |
| })) |
| defer srv.Close() |
| |
| // Construct the search term by cutting the 'http://' prefix off srv.URL. |
| searchTerm := srv.URL[7:] + "/" + term |
| |
| reg, err := NewService(ServiceOptions{}) |
| assert.NilError(t, err) |
| results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{}) |
| assert.NilError(t, err) |
| assert.DeepEqual(t, results, tc.expectedResults) |
| }) |
| } |
| } |