diff --git a/pkg/api/mock.go b/pkg/api/mock.go index da9d9511..54309463 100644 --- a/pkg/api/mock.go +++ b/pkg/api/mock.go @@ -85,3 +85,19 @@ func NewDebugMock(o io.Writer, res ...mock.Response) *API { return api } + +// NewMockMatchingByEndpoint creates a new api.API from a list of Responses, matching by endpoint. +// Defaults to a dummy APIKey for authentication, which is not checked +func NewMockMatchingByEndpoint(res map[string][]mock.Response) *API { + api, err := NewAPI(Config{ + Client: mock.NewMatchingByEndpointClient(res), + Host: mockSchemaHost, + AuthWriter: auth.APIKey("dummy"), + }) + + if err != nil { + panic(err) + } + + return api +} diff --git a/pkg/api/mock/client.go b/pkg/api/mock/client.go index 3d501954..433f5666 100644 --- a/pkg/api/mock/client.go +++ b/pkg/api/mock/client.go @@ -20,10 +20,58 @@ package mock import ( "fmt" "net/http" + "regexp" "sync" "sync/atomic" ) +// NewMatchingByEndpointClient returns a pointer to http.Client with the mocked Transport. +func NewMatchingByEndpointClient(r map[string][]Response) *http.Client { + return &http.Client{ + Transport: NewMatchingByEndpointRoundTripper(r), + } +} + +// NewRoundTripper initializes a new roundtripper and accepts multiple Response +// structures as variadric arguments. +func NewMatchingByEndpointRoundTripper(r map[string][]Response) *MatchingByEndpointRoundTripper { + responsesByEndpoint := make(map[string]*RoundTripper) + + for endpoint, responses := range r { + responsesByEndpoint[endpoint] = &RoundTripper{ + Responses: responses, + } + } + + return &MatchingByEndpointRoundTripper{ + ResponsesByEndpoint: responsesByEndpoint, + } +} + +// MatchingByEndpointRoundTripper is aimed to be used as the Transport property in an http.Client +// in order to mock the responses that it would return in the normal execution, matching by endpoint. +// If the number of responses that are mocked for a specific endpoint are not enough, an error with the +// request iteration ID, method and full URL is returned. +type MatchingByEndpointRoundTripper struct { + ResponsesByEndpoint map[string]*RoundTripper +} + +// RoundTrip executes a single HTTP transaction, returning +// a Response for the provided Request, based on the Request's URL. +func (rt *MatchingByEndpointRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + for endpoint, rt := range rt.ResponsesByEndpoint { + endpointRegex := regexp.MustCompile(endpoint) + + if endpointRegex.MatchString(req.URL.Path) { + return rt.RoundTrip(req) + } + } + + return nil, fmt.Errorf( + "failed to obtain response for request: %s %s", req.Method, req.URL, + ) +} + // NewClient returns a pointer to http.Client with the mocked Transport. func NewClient(r ...Response) *http.Client { return &http.Client{ diff --git a/pkg/api/mock/client_test.go b/pkg/api/mock/client_test.go index 8fe87183..8beb6c00 100644 --- a/pkg/api/mock/client_test.go +++ b/pkg/api/mock/client_test.go @@ -27,6 +27,361 @@ import ( "github.com/elastic/cloud-sdk-go/pkg/multierror" ) +func TestMatchingByEndpointRoundTripper_RoundTrip(t *testing.T) { + validURL1, err := url.Parse("https://cloud.elastic.co/some/path") + urlMatch1 := "/some/path" + validURL2, err := url.Parse("https://cloud.elastic.co/other/path") + urlMatch2 := "/other/path" + if err != nil { + t.Fatal(err) + } + type fields struct { + ResponsesByEndpoint map[string]*RoundTripper + } + type args struct { + req []*http.Request + } + type want struct { + want *http.Response + err error + } + tests := []struct { + name string + fields fields + args args + want []want + }{ + { + name: "Single request mock with no body", + fields: fields{ResponsesByEndpoint: map[string]*RoundTripper{ + urlMatch1: { + Responses: []Response{ + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + }, + }}, + }, + args: args{req: []*http.Request{ + {URL: validURL1}, + }}, + want: []want{ + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + }, + }, + { + name: "Single request mock with body", + fields: fields{ResponsesByEndpoint: map[string]*RoundTripper{ + urlMatch1: { + Responses: []Response{ + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + }, + }}, + }, + args: args{req: []*http.Request{ + {URL: validURL1, Body: NewStringBody(`{"some":"body"}`)}, + }}, + want: []want{ + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + }, + }, + { + name: "Single request mock with body returns an error", + fields: fields{ResponsesByEndpoint: map[string]*RoundTripper{ + urlMatch1: { + Responses: []Response{ + { + Error: errors.New("some error"), + }, + }, + }}, + }, + args: args{req: []*http.Request{ + {URL: validURL1, Body: NewStringBody(`{"some":"body"}`)}, + }}, + want: []want{ + {err: errors.New("some error")}, + }, + }, + { + name: "Multiple request mock with no body", + fields: fields{ResponsesByEndpoint: map[string]*RoundTripper{ + urlMatch1: { + Responses: []Response{ + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something something"), + }, + }, + }, + }, + urlMatch2: { + Responses: []Response{ + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something"), + }, + }, + }, + }}, + }, + args: args{req: []*http.Request{ + {URL: validURL1}, + {URL: validURL2}, + {URL: validURL1}, + }}, + want: []want{ + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something"), + }, + }, + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something something"), + }, + }, + }, + }, + { + name: "Multiple request mock with body", + fields: fields{ResponsesByEndpoint: map[string]*RoundTripper{ + urlMatch1: { + Responses: []Response{ + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something something"), + }, + }, + }, + }, + urlMatch2: { + Responses: []Response{ + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something"), + }, + }, + }, + }}, + }, + args: args{req: []*http.Request{ + {URL: validURL1, Body: NewStringBody(`{"some":"body"}`)}, + {URL: validURL2, Body: NewStringBody(`{"some":"other body"}`)}, + {URL: validURL1, Body: NewStringBody(`{"some":"other other body"}`)}, + }}, + want: []want{ + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something"), + }, + }, + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something something"), + }, + }, + }, + }, + { + name: "Multiple request mock with body returns an error", + fields: fields{ResponsesByEndpoint: map[string]*RoundTripper{ + urlMatch1: { + Responses: []Response{ + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + { + Error: errors.New("some error"), + }, + }, + }, + urlMatch2: { + Responses: []Response{ + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something"), + }, + }, + }, + }}, + }, + args: args{req: []*http.Request{ + {URL: validURL1, Body: NewStringBody(`{"some":"body"}`)}, + {URL: validURL2, Body: NewStringBody(`{"some":"other body"}`)}, + {URL: validURL1, Body: NewStringBody(`{"some":"other other body"}`)}, + }}, + want: []want{ + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something"), + }, + }, + {err: errors.New("some error")}, + }, + }, + { + name: "Multiple request mock when there's no match for URL return an error", + fields: fields{ResponsesByEndpoint: map[string]*RoundTripper{ + urlMatch1: { + Responses: []Response{ + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + }, + }, + urlMatch2: { + Responses: []Response{ + { + Response: http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something"), + }, + }, + }, + }}, + }, + args: args{req: []*http.Request{ + {URL: validURL1, Body: NewStringBody(`{"some":"body"}`)}, + {URL: validURL2, Body: NewStringBody(`{"some":"other body"}`)}, + { + Body: NewStringBody(`{"some":"other other body"}`), + Method: "POST", + URL: &url.URL{ + Scheme: "https", + Host: "localhost", + Path: "/unknown/path", + }, + }, + }}, + want: []want{ + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something"), + }, + }, + { + want: &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: NewStringBody("something something"), + }, + }, + {err: errors.New("failed to obtain response for request: POST https://localhost/unknown/path")}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rt := &MatchingByEndpointRoundTripper{ + ResponsesByEndpoint: tt.fields.ResponsesByEndpoint, + } + var gotRes []want + for _, req := range tt.args.req { + got, err := rt.RoundTrip(req) + if got != nil { + defer got.Body.Close() + } + gotRes = append(gotRes, want{ + want: got, + err: err, + }) + } + if !reflect.DeepEqual(gotRes, tt.want) { + t.Errorf("MatchingByEndpointRoundTripper.RoundTrip() = %v, want %v", gotRes, tt.want) + } + }) + } +} + func TestRoundTripper_RoundTrip(t *testing.T) { validURL, err := url.Parse("https://cloud.elastic.co/somepath") if err != nil {