pkg/api/mock/client.go (83 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
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{
Transport: NewRoundTripper(r...),
}
}
// NewRoundTripper initializes a new roundtripper and accepts multiple Response
// structures as variadric arguments.
func NewRoundTripper(r ...Response) *RoundTripper {
return &RoundTripper{
Responses: r,
}
}
// RoundTripper 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.
// If the number of responses that are mocked are not enough, an error with the
// request iteration ID, method and full URL is returned.
type RoundTripper struct {
Responses []Response
iteration int32
mu sync.RWMutex
}
// Add accepts multiple Response structures as variadric arguments and appends
// those to the current list of Responses.
func (rt *RoundTripper) Add(res ...Response) *RoundTripper {
rt.mu.Lock()
defer rt.mu.Unlock()
rt.Responses = append(rt.Responses, res...)
return rt
}
// RoundTrip executes a single HTTP transaction, returning
// a Response for the provided Request.
//
// RoundTrip should not attempt to interpret the response. In
// particular, RoundTrip must return err == nil if it obtained
// a response, regardless of the response's HTTP status code.
// A non-nil err should be reserved for failure to obtain a
// response. Similarly, RoundTrip should not attempt to
// handle higher-level protocol details such as redirects,
// authentication, or cookies.
//
// RoundTrip should not modify the request, except for
// consuming and closing the Request's Body. RoundTrip may
// read fields of the request in a separate goroutine. Callers
// should not mutate or reuse the request until the Response's
// Body has been closed.
//
// RoundTrip must always close the body, including on errors,
// but depending on the implementation may do so in a separate
// goroutine even after RoundTrip returns. This means that
// callers wanting to reuse the body for subsequent requests
// must arrange to wait for the Close call before doing so.
//
// The Request's URL and Header fields must be initialized.
func (rt *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
rt.mu.RLock()
defer func() {
atomic.AddInt32(&rt.iteration, 1)
rt.mu.RUnlock()
}()
var iteration = atomic.LoadInt32(&rt.iteration)
if int(iteration) > len(rt.Responses)-1 {
return nil, fmt.Errorf(
"failed to obtain response in iteration %d: %s %s",
iteration+1, req.Method, req.URL,
)
}
r := rt.Responses[iteration]
if r.Error != nil {
return nil, r.Error
}
if r.Assert != nil {
if err := AssertRequest(r.Assert, req); err != nil {
return nil, err
}
}
return &r.Response, nil
}