package registry import ( "context" "encoding/json" "net/http" "net/http/httptest" "net/http/httputil" "testing" "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/registry" "github.com/docker/docker/errdefs" "gotest.tools/v3/assert" ) func spawnTestRegistrySession(t *testing.T) *session { authConfig := ®istry.AuthConfig{} endpoint, err := newV1Endpoint(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(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("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 { tc := tc 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, errdefs.IsUnknown(err), "got: %T: %v", err, err) return } assert.Check(t, errdefs.IsInvalidParameter(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 { tc := tc 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) }) } }