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 }