github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/docker/registry/internal/transports_test.go (about) 1 // Copyright 2021 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package internal_test 5 6 import ( 7 "encoding/base64" 8 "io" 9 "net/http" 10 "net/url" 11 "strings" 12 13 "github.com/juju/errors" 14 "github.com/juju/testing" 15 jc "github.com/juju/testing/checkers" 16 "go.uber.org/mock/gomock" 17 gc "gopkg.in/check.v1" 18 19 "github.com/juju/juju/docker/registry/internal" 20 "github.com/juju/juju/docker/registry/mocks" 21 ) 22 23 type transportSuite struct { 24 testing.IsolationSuite 25 } 26 27 var _ = gc.Suite(&transportSuite{}) 28 29 func (s *transportSuite) TestErrorTransport(c *gc.C) { 30 ctrl := gomock.NewController(c) 31 defer ctrl.Finish() 32 mockRoundTripper := mocks.NewMockRoundTripper(ctrl) 33 34 url, err := url.Parse(`https://example.com`) 35 c.Assert(err, jc.ErrorIsNil) 36 37 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) { 38 resps := &http.Response{ 39 Request: req, 40 StatusCode: http.StatusForbidden, 41 Body: io.NopCloser(strings.NewReader(`invalid input`)), 42 } 43 return resps, nil 44 }) 45 t := internal.NewErrorTransport(mockRoundTripper) 46 _, err = t.RoundTrip(&http.Request{URL: url}) 47 c.Assert(err, gc.ErrorMatches, `non-successful response status=403`) 48 } 49 50 func (s *transportSuite) TestBasicTransport(c *gc.C) { 51 ctrl := gomock.NewController(c) 52 defer ctrl.Finish() 53 mockRoundTripper := mocks.NewMockRoundTripper(ctrl) 54 55 url, err := url.Parse(`https://example.com`) 56 c.Assert(err, jc.ErrorIsNil) 57 58 // username + password. 59 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 60 func(req *http.Request) (*http.Response, error) { 61 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("username:pwd"))}}) 62 return &http.Response{ 63 Request: req, 64 StatusCode: http.StatusOK, 65 Body: io.NopCloser(strings.NewReader(``)), 66 }, nil 67 }, 68 ) 69 t := internal.NewBasicTransport(mockRoundTripper, "username", "pwd", "") 70 _, err = t.RoundTrip(&http.Request{ 71 Header: http.Header{}, 72 URL: url, 73 }) 74 c.Assert(err, jc.ErrorIsNil) 75 76 // auth token. 77 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 78 func(req *http.Request) (*http.Response, error) { 79 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + `dXNlcm5hbWU6cHdkMQ==`}}) 80 return &http.Response{ 81 Request: req, 82 StatusCode: http.StatusOK, 83 Body: io.NopCloser(strings.NewReader(``)), 84 }, nil 85 }, 86 ) 87 t = internal.NewBasicTransport(mockRoundTripper, "", "", "dXNlcm5hbWU6cHdkMQ==") 88 _, err = t.RoundTrip(&http.Request{ 89 Header: http.Header{}, 90 URL: url, 91 }) 92 c.Assert(err, jc.ErrorIsNil) 93 94 // no credentials. 95 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 96 func(req *http.Request) (*http.Response, error) { 97 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 98 return &http.Response{ 99 Request: req, 100 StatusCode: http.StatusOK, 101 Body: io.NopCloser(strings.NewReader(``)), 102 }, nil 103 }, 104 ) 105 t = internal.NewBasicTransport(mockRoundTripper, "", "", "") 106 _, err = t.RoundTrip(&http.Request{ 107 Header: http.Header{}, 108 URL: url, 109 }) 110 c.Assert(err, jc.ErrorIsNil) 111 } 112 113 func (s *transportSuite) TestTokenTransportOAuthTokenProvided(c *gc.C) { 114 ctrl := gomock.NewController(c) 115 defer ctrl.Finish() 116 mockRoundTripper := mocks.NewMockRoundTripper(ctrl) 117 118 url, err := url.Parse(`https://example.com`) 119 c.Assert(err, jc.ErrorIsNil) 120 121 gomock.InOrder( 122 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 123 func(req *http.Request) (*http.Response, error) { 124 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + `OAuth-jwt-token`}}) 125 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 126 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil 127 }, 128 ), 129 ) 130 t := internal.NewTokenTransport(mockRoundTripper, "", "", "", "OAuth-jwt-token", false) 131 _, err = t.RoundTrip(&http.Request{ 132 Header: http.Header{}, 133 URL: url, 134 }) 135 c.Assert(err, jc.ErrorIsNil) 136 } 137 138 func (s *transportSuite) TestTokenTransportTokenRefresh(c *gc.C) { 139 ctrl := gomock.NewController(c) 140 defer ctrl.Finish() 141 mockRoundTripper := mocks.NewMockRoundTripper(ctrl) 142 143 url, err := url.Parse(`https://example.com`) 144 c.Assert(err, jc.ErrorIsNil) 145 146 gomock.InOrder( 147 // 1st try failed - bearer token was missing. 148 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 149 func(req *http.Request) (*http.Response, error) { 150 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 151 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 152 return &http.Response{ 153 Request: req, 154 StatusCode: http.StatusUnauthorized, 155 Body: io.NopCloser(nil), 156 Header: http.Header{ 157 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 158 `Bearer realm="https://auth.example.com/token",service="registry.example.com",scope="repository:jujuqa/jujud-operator:pull"`, 159 }, 160 }, 161 }, nil 162 }, 163 ), 164 // Refresh OAuth Token. 165 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 166 func(req *http.Request) (*http.Response, error) { 167 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + `dXNlcm5hbWU6cHdkMQ==`}}) 168 c.Assert(req.URL.String(), gc.Equals, `https://auth.example.com/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=registry.example.com`) 169 return &http.Response{ 170 Request: req, 171 StatusCode: http.StatusOK, 172 Body: io.NopCloser(strings.NewReader(`{"token": "OAuth-jwt-token", "access_token": "OAuth-jwt-token","expires_in": 300}`)), 173 }, nil 174 }, 175 ), 176 // retry. 177 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 178 func(req *http.Request) (*http.Response, error) { 179 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + `OAuth-jwt-token`}}) 180 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 181 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil 182 }, 183 ), 184 ) 185 t := internal.NewTokenTransport(mockRoundTripper, "", "", "dXNlcm5hbWU6cHdkMQ==", "", false) 186 _, err = t.RoundTrip(&http.Request{ 187 Header: http.Header{}, 188 URL: url, 189 }) 190 c.Assert(err, jc.ErrorIsNil) 191 } 192 193 func (s *transportSuite) TestTokenTransportTokenRefreshFailedRealmMissing(c *gc.C) { 194 ctrl := gomock.NewController(c) 195 defer ctrl.Finish() 196 mockRoundTripper := mocks.NewMockRoundTripper(ctrl) 197 198 url, err := url.Parse(`https://example.com`) 199 c.Assert(err, jc.ErrorIsNil) 200 201 gomock.InOrder( 202 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 203 func(req *http.Request) (*http.Response, error) { 204 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 205 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 206 return &http.Response{ 207 Request: req, 208 StatusCode: http.StatusUnauthorized, 209 Body: io.NopCloser(nil), 210 Header: http.Header{ 211 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 212 `Bearer service="registry.example.com",scope="repository:jujuqa/jujud-operator:pull"`, 213 }, 214 }, 215 }, nil 216 }, 217 ), 218 ) 219 t := internal.NewTokenTransport(mockRoundTripper, "", "", "dXNlcm5hbWU6cHdkMQ==", "", false) 220 _, err = t.RoundTrip(&http.Request{ 221 Header: http.Header{}, 222 URL: url, 223 }) 224 c.Assert(err, gc.ErrorMatches, `refreshing OAuth token: no realm specified for token auth challenge`) 225 } 226 227 func (s *transportSuite) TestTokenTransportTokenRefreshFailedServiceMissing(c *gc.C) { 228 ctrl := gomock.NewController(c) 229 defer ctrl.Finish() 230 mockRoundTripper := mocks.NewMockRoundTripper(ctrl) 231 232 url, err := url.Parse(`https://example.com`) 233 c.Assert(err, jc.ErrorIsNil) 234 235 gomock.InOrder( 236 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 237 func(req *http.Request) (*http.Response, error) { 238 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 239 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 240 return &http.Response{ 241 Request: req, 242 StatusCode: http.StatusUnauthorized, 243 Body: io.NopCloser(nil), 244 Header: http.Header{ 245 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 246 `Bearer realm="https://auth.example.com/token",scope="repository:jujuqa/jujud-operator:pull"`, 247 }, 248 }, 249 }, nil 250 }, 251 ), 252 ) 253 t := internal.NewTokenTransport(mockRoundTripper, "", "", "dXNlcm5hbWU6cHdkMQ==", "", false) 254 _, err = t.RoundTrip(&http.Request{ 255 Header: http.Header{}, 256 URL: url, 257 }) 258 c.Assert(err, gc.ErrorMatches, `refreshing OAuth token: no service specified for token auth challenge`) 259 } 260 261 func (s *transportSuite) TestUnwrapNetError(c *gc.C) { 262 originalErr := errors.NotFoundf("jujud-operator:2.6.6") 263 c.Assert(errors.IsNotFound(originalErr), jc.IsTrue) 264 var urlErr error = &url.Error{ 265 Op: "Get", 266 URL: "https://example.com", 267 Err: originalErr, 268 } 269 unwrapedErr := internal.UnwrapNetError(urlErr) 270 c.Assert(unwrapedErr, gc.NotNil) 271 c.Assert(unwrapedErr, jc.Satisfies, errors.IsNotFound) 272 c.Assert(unwrapedErr, gc.ErrorMatches, `Get "https://example.com": jujud-operator:2.6.6 not found`) 273 } 274 275 func (s *transportSuite) TestChallengeTransportTokenRefresh(c *gc.C) { 276 ctrl := gomock.NewController(c) 277 defer ctrl.Finish() 278 mockRoundTripper := mocks.NewMockRoundTripper(ctrl) 279 280 url, err := url.Parse(`https://example.com`) 281 c.Assert(err, jc.ErrorIsNil) 282 283 gomock.InOrder( 284 // 1st try failed - bearer token was missing. 285 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 286 func(req *http.Request) (*http.Response, error) { 287 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 288 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 289 return &http.Response{ 290 Request: req, 291 StatusCode: http.StatusUnauthorized, 292 Body: io.NopCloser(nil), 293 Header: http.Header{ 294 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 295 `Bearer realm="https://auth.example.com/token",service="registry.example.com",scope="repository:jujuqa/jujud-operator:pull"`, 296 }, 297 }, 298 }, nil 299 }, 300 ), 301 // Refresh OAuth Token. 302 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 303 func(req *http.Request) (*http.Response, error) { 304 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + `dXNlcm5hbWU6cHdkMQ==`}}) 305 c.Assert(req.URL.String(), gc.Equals, `https://auth.example.com/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=registry.example.com`) 306 return &http.Response{ 307 Request: req, 308 StatusCode: http.StatusOK, 309 Body: io.NopCloser(strings.NewReader(`{"token": "OAuth-jwt-token", "access_token": "OAuth-jwt-token","expires_in": 300}`)), 310 }, nil 311 }, 312 ), 313 // retry. 314 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 315 func(req *http.Request) (*http.Response, error) { 316 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + `OAuth-jwt-token`}}) 317 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 318 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil 319 }, 320 ), 321 ) 322 t := internal.NewChallengeTransport(mockRoundTripper, "", "", "dXNlcm5hbWU6cHdkMQ==") 323 _, err = t.RoundTrip(&http.Request{ 324 Header: http.Header{}, 325 URL: url, 326 }) 327 c.Assert(err, jc.ErrorIsNil) 328 } 329 330 func (s *transportSuite) TestChallengeTransportBasic(c *gc.C) { 331 ctrl := gomock.NewController(c) 332 defer ctrl.Finish() 333 mockRoundTripper := mocks.NewMockRoundTripper(ctrl) 334 335 url, err := url.Parse(`https://example.com`) 336 c.Assert(err, jc.ErrorIsNil) 337 338 gomock.InOrder( 339 // 1st try failed - bearer token was missing. 340 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 341 func(req *http.Request) (*http.Response, error) { 342 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 343 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 344 return &http.Response{ 345 Request: req, 346 StatusCode: http.StatusUnauthorized, 347 Body: io.NopCloser(nil), 348 Header: http.Header{ 349 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 350 `Basic realm="my realm"`, 351 }, 352 }, 353 }, nil 354 }, 355 ), 356 // retry. 357 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 358 func(req *http.Request) (*http.Response, error) { 359 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + `dXNlcm5hbWU6cHdkMQ==`}}) 360 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 361 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil 362 }, 363 ), 364 ) 365 t := internal.NewChallengeTransport(mockRoundTripper, "", "", "dXNlcm5hbWU6cHdkMQ==") 366 _, err = t.RoundTrip(&http.Request{ 367 Header: http.Header{}, 368 URL: url, 369 }) 370 c.Assert(err, jc.ErrorIsNil) 371 } 372 373 func (s *transportSuite) TestChallengeTransportMulti(c *gc.C) { 374 ctrl := gomock.NewController(c) 375 defer ctrl.Finish() 376 mockRoundTripper := mocks.NewMockRoundTripper(ctrl) 377 378 url, err := url.Parse(`https://example.com`) 379 c.Assert(err, jc.ErrorIsNil) 380 381 gomock.InOrder( 382 // 1st try failed - bearer token was missing. 383 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 384 func(req *http.Request) (*http.Response, error) { 385 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 386 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 387 return &http.Response{ 388 Request: req, 389 StatusCode: http.StatusUnauthorized, 390 Body: io.NopCloser(nil), 391 Header: http.Header{ 392 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 393 `Basic realm="my realm"`, 394 `Bearer realm="https://auth.example.com/token",service="registry.example.com",scope="repository:jujuqa/jujud-operator:pull"`, 395 }, 396 }, 397 }, nil 398 }, 399 ), 400 // retry. 401 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 402 func(req *http.Request) (*http.Response, error) { 403 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + `dXNlcm5hbWU6cHdkMQ==`}}) 404 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 405 return &http.Response{StatusCode: http.StatusUnauthorized, Body: io.NopCloser(nil)}, nil 406 }, 407 ), 408 // Refresh OAuth Token. 409 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 410 func(req *http.Request) (*http.Response, error) { 411 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + `dXNlcm5hbWU6cHdkMQ==`}}) 412 c.Assert(req.URL.String(), gc.Equals, `https://auth.example.com/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=registry.example.com`) 413 return &http.Response{ 414 Request: req, 415 StatusCode: http.StatusOK, 416 Body: io.NopCloser(strings.NewReader(`{"token": "OAuth-jwt-token", "access_token": "OAuth-jwt-token","expires_in": 300}`)), 417 }, nil 418 }, 419 ), 420 // retry. 421 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 422 func(req *http.Request) (*http.Response, error) { 423 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + `OAuth-jwt-token`}}) 424 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 425 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil 426 }, 427 ), 428 429 // re-use last successful 430 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 431 func(req *http.Request) (*http.Response, error) { 432 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 433 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 434 return &http.Response{ 435 Request: req, 436 StatusCode: http.StatusUnauthorized, 437 Body: io.NopCloser(nil), 438 Header: http.Header{ 439 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 440 `Basic realm="my realm"`, 441 `Bearer realm="https://auth.example.com/token",service="registry.example.com",scope="repository:jujuqa/jujud-operator:pull"`, 442 }, 443 }, 444 }, nil 445 }, 446 ), 447 // Refresh OAuth Token. 448 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 449 func(req *http.Request) (*http.Response, error) { 450 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + `dXNlcm5hbWU6cHdkMQ==`}}) 451 c.Assert(req.URL.String(), gc.Equals, `https://auth.example.com/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=registry.example.com`) 452 return &http.Response{ 453 Request: req, 454 StatusCode: http.StatusOK, 455 Body: io.NopCloser(strings.NewReader(`{"token": "OAuth-jwt-token", "access_token": "OAuth-jwt-token","expires_in": 300}`)), 456 }, nil 457 }, 458 ), 459 // retry. 460 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 461 func(req *http.Request) (*http.Response, error) { 462 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + `OAuth-jwt-token`}}) 463 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 464 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil 465 }, 466 ), 467 468 // re-use last successful 469 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 470 func(req *http.Request) (*http.Response, error) { 471 c.Assert(req.Header, jc.DeepEquals, http.Header{}) 472 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 473 return &http.Response{ 474 Request: req, 475 StatusCode: http.StatusUnauthorized, 476 Body: io.NopCloser(nil), 477 Header: http.Header{ 478 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 479 `Basic realm="my realm"`, 480 `Bearer realm="https://auth.example.com/token",service="registry.example.com",scope="repository:jujuqa/jujud-operator:pull"`, 481 }, 482 }, 483 }, nil 484 }, 485 ), 486 // Refresh OAuth Token. 487 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 488 func(req *http.Request) (*http.Response, error) { 489 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + `dXNlcm5hbWU6cHdkMQ==`}}) 490 c.Assert(req.URL.String(), gc.Equals, `https://auth.example.com/token?scope=repository%3Ajujuqa%2Fjujud-operator%3Apull&service=registry.example.com`) 491 return &http.Response{ 492 Request: req, 493 StatusCode: http.StatusOK, 494 Body: io.NopCloser(strings.NewReader(`{"token": "OAuth-jwt-token", "access_token": "OAuth-jwt-token","expires_in": 300}`)), 495 }, nil 496 }, 497 ), 498 // still bad 499 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 500 func(req *http.Request) (*http.Response, error) { 501 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Bearer " + `OAuth-jwt-token`}}) 502 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 503 return &http.Response{ 504 Request: req, 505 StatusCode: http.StatusUnauthorized, 506 Body: io.NopCloser(nil), 507 Header: http.Header{ 508 http.CanonicalHeaderKey("WWW-Authenticate"): []string{ 509 `Basic realm="my realm"`, 510 `Bearer realm="https://auth.example.com/token",service="registry.example.com",scope="repository:jujuqa/jujud-operator:pull"`, 511 }, 512 }, 513 }, nil 514 }, 515 ), 516 // retry with basic again. 517 mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( 518 func(req *http.Request) (*http.Response, error) { 519 c.Assert(req.Header, jc.DeepEquals, http.Header{"Authorization": []string{"Basic " + `dXNlcm5hbWU6cHdkMQ==`}}) 520 c.Assert(req.URL.String(), gc.Equals, `https://example.com`) 521 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(nil)}, nil 522 }, 523 ), 524 ) 525 t := internal.NewChallengeTransport(mockRoundTripper, "", "", "dXNlcm5hbWU6cHdkMQ==") 526 _, err = t.RoundTrip(&http.Request{ 527 Header: http.Header{}, 528 URL: url, 529 }) 530 c.Assert(err, jc.ErrorIsNil) 531 532 // Reuse 533 _, err = t.RoundTrip(&http.Request{ 534 Header: http.Header{}, 535 URL: url, 536 }) 537 c.Assert(err, jc.ErrorIsNil) 538 539 // Reauth 540 _, err = t.RoundTrip(&http.Request{ 541 Header: http.Header{}, 542 URL: url, 543 }) 544 c.Assert(err, jc.ErrorIsNil) 545 }