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  }