github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/charmhub/http_test.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charmhub
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/http/httptest"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/juju/errors"
    17  	jujuhttp "github.com/juju/http/v2"
    18  	"github.com/juju/testing"
    19  	jc "github.com/juju/testing/checkers"
    20  	"go.uber.org/mock/gomock"
    21  	gc "gopkg.in/check.v1"
    22  )
    23  
    24  type APIRequesterSuite struct {
    25  	testing.IsolationSuite
    26  }
    27  
    28  var _ = gc.Suite(&APIRequesterSuite{})
    29  
    30  func (s *APIRequesterSuite) TestDo(c *gc.C) {
    31  	ctrl := gomock.NewController(c)
    32  	defer ctrl.Finish()
    33  
    34  	req := MustNewRequest(c, "http://api.foo.bar")
    35  
    36  	mockHTTPClient := NewMockHTTPClient(ctrl)
    37  	mockHTTPClient.EXPECT().Do(req).Return(emptyResponse(), nil)
    38  
    39  	requester := newAPIRequester(mockHTTPClient, &FakeLogger{})
    40  	resp, err := requester.Do(req)
    41  	c.Assert(err, jc.ErrorIsNil)
    42  	c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
    43  }
    44  
    45  func (s *APIRequesterSuite) TestDoWithFailure(c *gc.C) {
    46  	ctrl := gomock.NewController(c)
    47  	defer ctrl.Finish()
    48  
    49  	req := MustNewRequest(c, "http://api.foo.bar")
    50  
    51  	mockHTTPClient := NewMockHTTPClient(ctrl)
    52  	mockHTTPClient.EXPECT().Do(req).Return(emptyResponse(), errors.Errorf("boom"))
    53  
    54  	requester := newAPIRequester(mockHTTPClient, &FakeLogger{})
    55  	_, err := requester.Do(req)
    56  	c.Assert(err, gc.Not(jc.ErrorIsNil))
    57  }
    58  
    59  func (s *APIRequesterSuite) TestDoWithInvalidContentType(c *gc.C) {
    60  	ctrl := gomock.NewController(c)
    61  	defer ctrl.Finish()
    62  
    63  	req := MustNewRequest(c, "http://api.foo.bar")
    64  
    65  	mockHTTPClient := NewMockHTTPClient(ctrl)
    66  	mockHTTPClient.EXPECT().Do(req).Return(invalidContentTypeResponse(), nil)
    67  
    68  	requester := newAPIRequester(mockHTTPClient, &FakeLogger{})
    69  	_, err := requester.Do(req)
    70  	c.Assert(err, gc.Not(jc.ErrorIsNil))
    71  }
    72  
    73  func (s *APIRequesterSuite) TestDoWithNotFoundResponse(c *gc.C) {
    74  	ctrl := gomock.NewController(c)
    75  	defer ctrl.Finish()
    76  
    77  	req := MustNewRequest(c, "http://api.foo.bar")
    78  
    79  	mockHTTPClient := NewMockHTTPClient(ctrl)
    80  	mockHTTPClient.EXPECT().Do(req).Return(notFoundResponse(), nil)
    81  
    82  	requester := newAPIRequester(mockHTTPClient, &FakeLogger{})
    83  	resp, err := requester.Do(req)
    84  	c.Assert(err, jc.ErrorIsNil)
    85  	c.Assert(resp.StatusCode, gc.Equals, http.StatusNotFound)
    86  }
    87  
    88  func (s *APIRequesterSuite) TestDoRetrySuccess(c *gc.C) {
    89  	ctrl := gomock.NewController(c)
    90  	defer ctrl.Finish()
    91  
    92  	req := MustNewRequest(c, "http://api.foo.bar")
    93  
    94  	mockHTTPClient := NewMockHTTPClient(ctrl)
    95  	mockHTTPClient.EXPECT().Do(req).Return(nil, io.EOF)
    96  	mockHTTPClient.EXPECT().Do(req).Return(emptyResponse(), nil)
    97  
    98  	requester := newAPIRequester(mockHTTPClient, &FakeLogger{})
    99  	requester.retryDelay = time.Microsecond
   100  	resp, err := requester.Do(req)
   101  	c.Assert(err, jc.ErrorIsNil)
   102  	c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
   103  }
   104  
   105  func (s *APIRequesterSuite) TestDoRetrySuccessBody(c *gc.C) {
   106  	ctrl := gomock.NewController(c)
   107  	defer ctrl.Finish()
   108  
   109  	req, err := http.NewRequest("POST", "http://api.foo.bar", strings.NewReader("body"))
   110  	c.Assert(err, jc.ErrorIsNil)
   111  
   112  	mockHTTPClient := NewMockHTTPClient(ctrl)
   113  	mockHTTPClient.EXPECT().Do(req).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   114  		b, err := io.ReadAll(req.Body)
   115  		c.Assert(err, jc.ErrorIsNil)
   116  		c.Assert(string(b), gc.Equals, "body")
   117  		return nil, io.EOF
   118  	})
   119  	mockHTTPClient.EXPECT().Do(req).DoAndReturn(func(req *http.Request) (*http.Response, error) {
   120  		b, err := io.ReadAll(req.Body)
   121  		c.Assert(err, jc.ErrorIsNil)
   122  		c.Assert(string(b), gc.Equals, "body")
   123  		return emptyResponse(), nil
   124  	})
   125  
   126  	requester := newAPIRequester(mockHTTPClient, &FakeLogger{})
   127  	requester.retryDelay = time.Microsecond
   128  	resp, err := requester.Do(req)
   129  	c.Assert(err, jc.ErrorIsNil)
   130  	c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
   131  }
   132  
   133  func (s *APIRequesterSuite) TestDoRetryMaxAttempts(c *gc.C) {
   134  	ctrl := gomock.NewController(c)
   135  	defer ctrl.Finish()
   136  
   137  	req := MustNewRequest(c, "http://api.foo.bar")
   138  
   139  	mockHTTPClient := NewMockHTTPClient(ctrl)
   140  	mockHTTPClient.EXPECT().Do(req).Return(nil, io.EOF)
   141  	mockHTTPClient.EXPECT().Do(req).Return(nil, io.EOF)
   142  
   143  	start := time.Now()
   144  	requester := newAPIRequester(mockHTTPClient, &FakeLogger{})
   145  	requester.retryDelay = time.Microsecond
   146  	_, err := requester.Do(req)
   147  	c.Assert(err, gc.ErrorMatches, `attempt count exceeded: EOF`)
   148  	elapsed := time.Since(start)
   149  	c.Assert(elapsed >= (1+2+4)*time.Microsecond, gc.Equals, true)
   150  }
   151  
   152  func (s *APIRequesterSuite) TestDoRetryContextCanceled(c *gc.C) {
   153  	ctrl := gomock.NewController(c)
   154  	defer ctrl.Finish()
   155  
   156  	ctx, cancel := context.WithCancel(context.Background())
   157  	cancel() // cancel right away
   158  	req, err := http.NewRequestWithContext(ctx, "GET", "http://api.foo.bar", nil)
   159  	c.Assert(err, jc.ErrorIsNil)
   160  
   161  	mockHTTPClient := NewMockHTTPClient(ctrl)
   162  	mockHTTPClient.EXPECT().Do(req).Return(nil, io.EOF)
   163  
   164  	start := time.Now()
   165  	requester := newAPIRequester(mockHTTPClient, &FakeLogger{})
   166  	requester.retryDelay = time.Second
   167  	_, err = requester.Do(req)
   168  	c.Assert(err, gc.ErrorMatches, `retry stopped`)
   169  	elapsed := time.Since(start)
   170  	c.Assert(elapsed < 250*time.Millisecond, gc.Equals, true)
   171  }
   172  
   173  type RESTSuite struct {
   174  	testing.IsolationSuite
   175  }
   176  
   177  var _ = gc.Suite(&RESTSuite{})
   178  
   179  func (s *RESTSuite) TestGet(c *gc.C) {
   180  	ctrl := gomock.NewController(c)
   181  	defer ctrl.Finish()
   182  
   183  	var recievedURL string
   184  
   185  	mockHTTPClient := NewMockHTTPClient(ctrl)
   186  	mockHTTPClient.EXPECT().Do(gomock.Any()).Do(func(req *http.Request) {
   187  		recievedURL = req.URL.String()
   188  	}).Return(emptyResponse(), nil)
   189  
   190  	base := MustMakePath(c, "http://api.foo.bar")
   191  
   192  	client := newHTTPRESTClient(mockHTTPClient)
   193  
   194  	var result interface{}
   195  	_, err := client.Get(context.Background(), base, &result)
   196  	c.Assert(err, jc.ErrorIsNil)
   197  	c.Assert(recievedURL, gc.Equals, "http://api.foo.bar")
   198  }
   199  
   200  func (s *RESTSuite) TestGetWithInvalidContext(c *gc.C) {
   201  	ctrl := gomock.NewController(c)
   202  	defer ctrl.Finish()
   203  
   204  	mockHTTPClient := NewMockHTTPClient(ctrl)
   205  	client := newHTTPRESTClient(mockHTTPClient)
   206  
   207  	base := MustMakePath(c, "http://api.foo.bar")
   208  
   209  	var result interface{}
   210  	_, err := client.Get(nil, base, &result)
   211  	c.Assert(err, gc.Not(jc.ErrorIsNil))
   212  }
   213  
   214  func (s *RESTSuite) TestGetWithFailure(c *gc.C) {
   215  	ctrl := gomock.NewController(c)
   216  	defer ctrl.Finish()
   217  
   218  	mockHTTPClient := NewMockHTTPClient(ctrl)
   219  	mockHTTPClient.EXPECT().Do(gomock.Any()).Return(emptyResponse(), errors.Errorf("boom"))
   220  
   221  	client := newHTTPRESTClient(mockHTTPClient)
   222  
   223  	base := MustMakePath(c, "http://api.foo.bar")
   224  
   225  	var result interface{}
   226  	_, err := client.Get(context.Background(), base, &result)
   227  	c.Assert(err, gc.Not(jc.ErrorIsNil))
   228  }
   229  
   230  func (s *RESTSuite) TestGetWithFailureRetry(c *gc.C) {
   231  	var called int
   232  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   233  		called++
   234  		w.WriteHeader(http.StatusTooManyRequests)
   235  	}))
   236  	defer server.Close()
   237  
   238  	httpClient := requestHTTPClient(nil, jujuhttp.RetryPolicy{
   239  		Attempts: 3,
   240  		Delay:    testing.ShortWait,
   241  		MaxDelay: testing.LongWait,
   242  	})(&FakeLogger{})
   243  	client := newHTTPRESTClient(httpClient)
   244  
   245  	base := MustMakePath(c, server.URL)
   246  
   247  	var result interface{}
   248  	_, err := client.Get(context.Background(), base, &result)
   249  	c.Assert(err, gc.Not(jc.ErrorIsNil))
   250  	c.Assert(called, gc.Equals, 3)
   251  }
   252  
   253  func (s *RESTSuite) TestGetWithFailureWithoutRetry(c *gc.C) {
   254  	var called int
   255  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   256  		called++
   257  		w.WriteHeader(http.StatusInternalServerError)
   258  	}))
   259  	defer server.Close()
   260  
   261  	httpClient := requestHTTPClient(nil, jujuhttp.RetryPolicy{
   262  		Attempts: 3,
   263  		Delay:    testing.ShortWait,
   264  		MaxDelay: testing.LongWait,
   265  	})(&FakeLogger{})
   266  	client := newHTTPRESTClient(httpClient)
   267  
   268  	base := MustMakePath(c, server.URL)
   269  
   270  	var result interface{}
   271  	_, err := client.Get(context.Background(), base, &result)
   272  	c.Assert(err, gc.Not(jc.ErrorIsNil))
   273  	c.Assert(called, gc.Equals, 1)
   274  }
   275  
   276  func (s *RESTSuite) TestGetWithNoRetry(c *gc.C) {
   277  	var called int
   278  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   279  		called++
   280  		w.Header().Set("content-type", "application/json")
   281  		w.WriteHeader(http.StatusOK)
   282  		fmt.Fprintln(w, "{}")
   283  	}))
   284  	defer server.Close()
   285  
   286  	httpClient := requestHTTPClient(nil, jujuhttp.RetryPolicy{
   287  		Attempts: 3,
   288  		Delay:    testing.ShortWait,
   289  		MaxDelay: testing.LongWait,
   290  	})(&FakeLogger{})
   291  	client := newHTTPRESTClient(httpClient)
   292  
   293  	base := MustMakePath(c, server.URL)
   294  
   295  	var result interface{}
   296  	_, err := client.Get(context.Background(), base, &result)
   297  	c.Assert(err, jc.ErrorIsNil)
   298  	c.Assert(called, gc.Equals, 1)
   299  }
   300  
   301  func (s *RESTSuite) TestGetWithUnmarshalFailure(c *gc.C) {
   302  	ctrl := gomock.NewController(c)
   303  	defer ctrl.Finish()
   304  
   305  	mockHTTPClient := NewMockHTTPClient(ctrl)
   306  	mockHTTPClient.EXPECT().Do(gomock.Any()).Return(invalidResponse(), nil)
   307  
   308  	client := newHTTPRESTClient(mockHTTPClient)
   309  
   310  	base := MustMakePath(c, "http://api.foo.bar")
   311  
   312  	var result interface{}
   313  	_, err := client.Get(context.Background(), base, &result)
   314  	c.Assert(err, gc.Not(jc.ErrorIsNil))
   315  }
   316  
   317  func emptyResponse() *http.Response {
   318  	return &http.Response{
   319  		Header:     MakeContentTypeHeader("application/json"),
   320  		StatusCode: http.StatusOK,
   321  		Body:       MakeNopCloser(bytes.NewBufferString("{}")),
   322  	}
   323  }
   324  
   325  func invalidResponse() *http.Response {
   326  	return &http.Response{
   327  		Header:     MakeContentTypeHeader("application/json"),
   328  		StatusCode: http.StatusOK,
   329  		Body:       MakeNopCloser(bytes.NewBufferString("/\\!")),
   330  	}
   331  }
   332  
   333  func invalidContentTypeResponse() *http.Response {
   334  	return &http.Response{
   335  		Header:     MakeContentTypeHeader("text/plain"),
   336  		StatusCode: http.StatusNotFound,
   337  		Body:       MakeNopCloser(bytes.NewBufferString("")),
   338  	}
   339  }
   340  
   341  func notFoundResponse() *http.Response {
   342  	return &http.Response{
   343  		Header:     MakeContentTypeHeader("application/json"),
   344  		StatusCode: http.StatusNotFound,
   345  		Body: MakeNopCloser(bytes.NewBufferString(`
   346  {
   347  	"code":"404",
   348  	"message":"not-found"
   349  }
   350  		`)),
   351  	}
   352  }