go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/client_test.go (about)

     1  // Copyright 2016 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package auth
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"io"
    21  	"net/http"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"golang.org/x/oauth2"
    27  
    28  	"go.chromium.org/luci/auth"
    29  	"go.chromium.org/luci/auth/identity"
    30  	"go.chromium.org/luci/common/clock"
    31  
    32  	"go.chromium.org/luci/server/auth/signing"
    33  	"go.chromium.org/luci/server/auth/signing/signingtest"
    34  
    35  	. "github.com/smartystreets/goconvey/convey"
    36  )
    37  
    38  func TestGetRPCTransport(t *testing.T) {
    39  	t.Parallel()
    40  
    41  	const ownServiceAccountName = "service-own-sa@example.com"
    42  
    43  	Convey("GetRPCTransport works", t, func() {
    44  		ctx := context.Background()
    45  		mock := &clientRPCTransportMock{}
    46  		ctx = ModifyConfig(ctx, func(cfg Config) Config {
    47  			cfg.AccessTokenProvider = mock.getAccessToken
    48  			cfg.AnonymousTransport = mock.getTransport
    49  			cfg.Signer = signingtest.NewSigner(&signing.ServiceInfo{
    50  				ServiceAccountName: ownServiceAccountName,
    51  			})
    52  			return cfg
    53  		})
    54  
    55  		Convey("in NoAuth mode", func(c C) {
    56  			t, err := GetRPCTransport(ctx, NoAuth)
    57  			So(err, ShouldBeNil)
    58  			_, err = t.RoundTrip(makeReq("https://example.com"))
    59  			So(err, ShouldBeNil)
    60  
    61  			So(len(mock.calls), ShouldEqual, 0)
    62  			So(len(mock.reqs[0].Header), ShouldEqual, 0)
    63  		})
    64  
    65  		Convey("in AsSelf mode", func(c C) {
    66  			t, err := GetRPCTransport(ctx, AsSelf, WithScopes("A", "B"))
    67  			So(err, ShouldBeNil)
    68  			_, err = t.RoundTrip(makeReq("https://example.com"))
    69  			So(err, ShouldBeNil)
    70  
    71  			So(mock.calls[0], ShouldResemble, []string{"A", "B"})
    72  			So(mock.reqs[0].Header, ShouldResemble, http.Header{
    73  				"Authorization": {"Bearer as-self-token:A,B"},
    74  			})
    75  		})
    76  
    77  		Convey("in AsSelf mode with default scopes", func(c C) {
    78  			t, err := GetRPCTransport(ctx, AsSelf)
    79  			So(err, ShouldBeNil)
    80  			_, err = t.RoundTrip(makeReq("https://example.com"))
    81  			So(err, ShouldBeNil)
    82  
    83  			So(mock.calls[0], ShouldResemble, []string{"https://www.googleapis.com/auth/userinfo.email"})
    84  			So(mock.reqs[0].Header, ShouldResemble, http.Header{
    85  				"Authorization": {"Bearer as-self-token:https://www.googleapis.com/auth/userinfo.email"},
    86  			})
    87  		})
    88  
    89  		Convey("in AsSelf mode with ID token, static aud", func(c C) {
    90  			mocks := &rpcMocks{
    91  				MintIDTokenForServiceAccount: func(ic context.Context, p MintIDTokenParams) (*Token, error) {
    92  					So(p, ShouldResemble, MintIDTokenParams{
    93  						ServiceAccount: ownServiceAccountName,
    94  						Audience:       "https://example.com/aud",
    95  						MinTTL:         2 * time.Minute,
    96  					})
    97  					return &Token{
    98  						Token:  "id-token",
    99  						Expiry: clock.Now(ic).Add(time.Hour),
   100  					}, nil
   101  				},
   102  			}
   103  
   104  			t, err := GetRPCTransport(ctx, AsSelf, WithIDTokenAudience("https://example.com/aud"), mocks)
   105  			So(err, ShouldBeNil)
   106  			_, err = t.RoundTrip(makeReq("https://another.example.com"))
   107  			So(err, ShouldBeNil)
   108  
   109  			So(mock.reqs[0].Header, ShouldResemble, http.Header{
   110  				"Authorization": {"Bearer id-token"},
   111  			})
   112  		})
   113  
   114  		Convey("in AsSelf mode with ID token, pattern aud", func(c C) {
   115  			mocks := &rpcMocks{
   116  				MintIDTokenForServiceAccount: func(ic context.Context, p MintIDTokenParams) (*Token, error) {
   117  					So(p, ShouldResemble, MintIDTokenParams{
   118  						ServiceAccount: ownServiceAccountName,
   119  						Audience:       "https://another.example.com:443/aud",
   120  						MinTTL:         2 * time.Minute,
   121  					})
   122  					return &Token{
   123  						Token:  "id-token",
   124  						Expiry: clock.Now(ic).Add(time.Hour),
   125  					}, nil
   126  				},
   127  			}
   128  
   129  			t, err := GetRPCTransport(ctx, AsSelf, WithIDTokenAudience("https://${host}/aud"), mocks)
   130  			So(err, ShouldBeNil)
   131  			_, err = t.RoundTrip(makeReq("https://another.example.com:443"))
   132  			So(err, ShouldBeNil)
   133  
   134  			So(mock.reqs[0].Header, ShouldResemble, http.Header{
   135  				"Authorization": {"Bearer id-token"},
   136  			})
   137  		})
   138  
   139  		Convey("in AsUser mode, authenticated", func(c C) {
   140  			ctx := WithState(ctx, &state{
   141  				user: &User{Identity: "user:abc@example.com"},
   142  			})
   143  
   144  			t, err := GetRPCTransport(ctx, AsUser, WithDelegationTags("a:b", "c:d"), &rpcMocks{
   145  				MintDelegationToken: func(ic context.Context, p DelegationTokenParams) (*Token, error) {
   146  					c.So(p, ShouldResemble, DelegationTokenParams{
   147  						TargetHost: "example.com",
   148  						Tags:       []string{"a:b", "c:d"},
   149  						MinTTL:     10 * time.Minute,
   150  					})
   151  					return &Token{Token: "deleg_tok"}, nil
   152  				},
   153  			})
   154  			So(err, ShouldBeNil)
   155  			_, err = t.RoundTrip(makeReq("https://example.com/some-path/sd"))
   156  			So(err, ShouldBeNil)
   157  
   158  			So(mock.calls[0], ShouldResemble, []string{"https://www.googleapis.com/auth/userinfo.email"})
   159  			So(mock.reqs[0].Header, ShouldResemble, http.Header{
   160  				"Authorization":         {"Bearer as-self-token:https://www.googleapis.com/auth/userinfo.email"},
   161  				"X-Delegation-Token-V1": {"deleg_tok"},
   162  			})
   163  		})
   164  
   165  		Convey("in AsProject mode", func(c C) {
   166  			callExampleCom := func(ctx context.Context) {
   167  				t, err := GetRPCTransport(ctx, AsProject, WithProject("infra"), &rpcMocks{
   168  					MintProjectToken: func(ic context.Context, p ProjectTokenParams) (*Token, error) {
   169  						c.So(p, ShouldResemble, ProjectTokenParams{
   170  							MinTTL:      2 * time.Minute,
   171  							LuciProject: "infra",
   172  							OAuthScopes: defaultOAuthScopes,
   173  						})
   174  						return &Token{
   175  							Token:  "scoped tok",
   176  							Expiry: clock.Now(ctx).Add(time.Hour),
   177  						}, nil
   178  					},
   179  				})
   180  				So(err, ShouldBeNil)
   181  				_, err = t.RoundTrip(makeReq("https://example.com/some-path/sd"))
   182  				So(err, ShouldBeNil)
   183  			}
   184  
   185  			Convey("external service", func() {
   186  				callExampleCom(WithState(ctx, &state{
   187  					db: &fakeDB{internalService: "not-example.com"},
   188  				}))
   189  				So(mock.reqs[0].Header, ShouldResemble, http.Header{
   190  					"Authorization": {"Bearer scoped tok"},
   191  				})
   192  			})
   193  
   194  			Convey("internal service", func() {
   195  				callExampleCom(WithState(ctx, &state{
   196  					db: &fakeDB{internalService: "example.com"},
   197  				}))
   198  				So(mock.reqs[0].Header, ShouldResemble, http.Header{
   199  					"Authorization":  {"Bearer as-self-token:https://www.googleapis.com/auth/userinfo.email"},
   200  					"X-Luci-Project": {"infra"},
   201  				})
   202  			})
   203  		})
   204  
   205  		Convey("in AsUser mode, anonymous", func(c C) {
   206  			ctx := WithState(ctx, &state{
   207  				user: &User{Identity: identity.AnonymousIdentity},
   208  			})
   209  
   210  			t, err := GetRPCTransport(ctx, AsUser, &rpcMocks{
   211  				MintDelegationToken: func(ic context.Context, p DelegationTokenParams) (*Token, error) {
   212  					panic("must not be called")
   213  				},
   214  			})
   215  			So(err, ShouldBeNil)
   216  			_, err = t.RoundTrip(makeReq("https://example.com"))
   217  			So(err, ShouldBeNil)
   218  			So(mock.reqs[0].Header, ShouldResemble, http.Header{})
   219  		})
   220  
   221  		Convey("in AsUser mode, with existing token", func(c C) {
   222  			ctx := WithState(ctx, &state{
   223  				user: &User{Identity: identity.AnonymousIdentity},
   224  			})
   225  
   226  			t, err := GetRPCTransport(ctx, AsUser, WithDelegationToken("deleg_tok"), &rpcMocks{
   227  				MintDelegationToken: func(ic context.Context, p DelegationTokenParams) (*Token, error) {
   228  					panic("must not be called")
   229  				},
   230  			})
   231  			So(err, ShouldBeNil)
   232  			_, err = t.RoundTrip(makeReq("https://example.com"))
   233  			So(err, ShouldBeNil)
   234  
   235  			So(mock.calls[0], ShouldResemble, []string{"https://www.googleapis.com/auth/userinfo.email"})
   236  			So(mock.reqs[0].Header, ShouldResemble, http.Header{
   237  				"Authorization":         {"Bearer as-self-token:https://www.googleapis.com/auth/userinfo.email"},
   238  				"X-Delegation-Token-V1": {"deleg_tok"},
   239  			})
   240  		})
   241  
   242  		Convey("in AsUser mode with both delegation tags and token", func(c C) {
   243  			_, err := GetRPCTransport(
   244  				ctx, AsUser, WithDelegationToken("deleg_tok"), WithDelegationTags("a:b"))
   245  			So(err, ShouldNotBeNil)
   246  		})
   247  
   248  		Convey("in NoAuth mode with delegation tags, should error", func(c C) {
   249  			_, err := GetRPCTransport(ctx, NoAuth, WithDelegationTags("a:b"))
   250  			So(err, ShouldNotBeNil)
   251  		})
   252  
   253  		Convey("in NoAuth mode with scopes, should error", func(c C) {
   254  			_, err := GetRPCTransport(ctx, NoAuth, WithScopes("A"))
   255  			So(err, ShouldNotBeNil)
   256  		})
   257  
   258  		Convey("in NoAuth mode with ID token, should error", func(c C) {
   259  			_, err := GetRPCTransport(ctx, NoAuth, WithIDTokenAudience("aud"))
   260  			So(err, ShouldNotBeNil)
   261  		})
   262  
   263  		Convey("in AsSelf mode with ID token and scopes, should error", func(c C) {
   264  			_, err := GetRPCTransport(ctx, AsSelf, WithScopes("A"), WithIDTokenAudience("aud"))
   265  			So(err, ShouldNotBeNil)
   266  		})
   267  
   268  		Convey("in AsSelf mode with bad aud pattern, should error", func(c C) {
   269  			_, err := GetRPCTransport(ctx, AsSelf, WithIDTokenAudience("${huh}"))
   270  			So(err, ShouldNotBeNil)
   271  		})
   272  
   273  		Convey("in AsCredentialsForwarder mode, anonymous", func(c C) {
   274  			ctx := WithState(ctx, &state{
   275  				user:       &User{Identity: identity.AnonymousIdentity},
   276  				endUserErr: ErrNoForwardableCreds,
   277  			})
   278  
   279  			t, err := GetRPCTransport(ctx, AsCredentialsForwarder)
   280  			So(err, ShouldBeNil)
   281  			_, err = t.RoundTrip(makeReq("https://example.com"))
   282  			So(err, ShouldBeNil)
   283  
   284  			// No credentials passed.
   285  			So(mock.reqs[0].Header, ShouldHaveLength, 0)
   286  		})
   287  
   288  		Convey("in AsCredentialsForwarder mode, non-anonymous", func(c C) {
   289  			ctx := WithState(ctx, &state{
   290  				user: &User{Identity: "user:a@example.com"},
   291  				endUserTok: &oauth2.Token{
   292  					TokenType:   "Bearer",
   293  					AccessToken: "abc.def",
   294  				},
   295  				endUserExtraHeaders: map[string]string{"X-Extra": "val"},
   296  			})
   297  
   298  			t, err := GetRPCTransport(ctx, AsCredentialsForwarder)
   299  			So(err, ShouldBeNil)
   300  			_, err = t.RoundTrip(makeReq("https://example.com"))
   301  			So(err, ShouldBeNil)
   302  
   303  			// Passed the token and the extra header.
   304  			So(mock.reqs[0].Header, ShouldResemble, http.Header{
   305  				"Authorization": {"Bearer abc.def"},
   306  				"X-Extra":       {"val"},
   307  			})
   308  		})
   309  
   310  		Convey("in AsCredentialsForwarder mode, non-forwardable", func(c C) {
   311  			ctx := WithState(ctx, &state{
   312  				user:       &User{Identity: "user:a@example.com"},
   313  				endUserErr: ErrNoForwardableCreds,
   314  			})
   315  
   316  			_, err := GetRPCTransport(ctx, AsCredentialsForwarder)
   317  			So(err, ShouldEqual, ErrNoForwardableCreds)
   318  		})
   319  
   320  		Convey("in AsActor mode with account", func(c C) {
   321  			mocks := &rpcMocks{
   322  				MintAccessTokenForServiceAccount: func(ic context.Context, p MintAccessTokenParams) (*Token, error) {
   323  					So(p, ShouldResemble, MintAccessTokenParams{
   324  						ServiceAccount: "abc@example.com",
   325  						Scopes:         []string{auth.OAuthScopeEmail},
   326  						MinTTL:         2 * time.Minute,
   327  					})
   328  					return &Token{
   329  						Token:  "blah-blah",
   330  						Expiry: clock.Now(ic).Add(time.Hour),
   331  					}, nil
   332  				},
   333  			}
   334  
   335  			t, err := GetRPCTransport(ctx, AsActor, WithServiceAccount("abc@example.com"), mocks)
   336  			So(err, ShouldBeNil)
   337  
   338  			_, err = t.RoundTrip(makeReq("https://example.com"))
   339  			So(err, ShouldBeNil)
   340  			So(mock.reqs[0].Header, ShouldResemble, http.Header{
   341  				"Authorization": {"Bearer blah-blah"},
   342  			})
   343  		})
   344  
   345  		Convey("in AsActor mode without account, error", func(c C) {
   346  			_, err := GetRPCTransport(ctx, AsActor)
   347  			So(err, ShouldNotBeNil)
   348  		})
   349  
   350  		Convey("in AsProject mode without project, error", func(c C) {
   351  			_, err := GetRPCTransport(ctx, AsProject)
   352  			So(err, ShouldNotBeNil)
   353  		})
   354  
   355  		Convey("in AsSessionUser mode without session", func(c C) {
   356  			_, err := GetRPCTransport(ctx, AsSessionUser)
   357  			So(err, ShouldEqual, nil)
   358  		})
   359  
   360  		Convey("in AsSessionUser mode", func(c C) {
   361  			ctx := WithState(ctx, &state{
   362  				user: &User{Identity: "user:abc@example.com"},
   363  				session: &fakeSession{
   364  					accessToken: &oauth2.Token{
   365  						TokenType:   "Bearer",
   366  						AccessToken: "access_token",
   367  					},
   368  					idToken: &oauth2.Token{
   369  						TokenType:   "Bearer",
   370  						AccessToken: "id_token",
   371  					},
   372  				},
   373  			})
   374  
   375  			Convey("OAuth2 token", func() {
   376  				t, err := GetRPCTransport(ctx, AsSessionUser)
   377  				So(err, ShouldBeNil)
   378  				_, err = t.RoundTrip(makeReq("https://example.com"))
   379  				So(err, ShouldBeNil)
   380  				So(mock.reqs[0].Header, ShouldResemble, http.Header{
   381  					"Authorization": {"Bearer access_token"},
   382  				})
   383  			})
   384  
   385  			Convey("ID token", func() {
   386  				t, err := GetRPCTransport(ctx, AsSessionUser, WithIDToken())
   387  				So(err, ShouldBeNil)
   388  				_, err = t.RoundTrip(makeReq("https://example.com"))
   389  				So(err, ShouldBeNil)
   390  				So(mock.reqs[0].Header, ShouldResemble, http.Header{
   391  					"Authorization": {"Bearer id_token"},
   392  				})
   393  			})
   394  
   395  			Convey("Trying to override scopes", func() {
   396  				_, err := GetRPCTransport(ctx, AsSessionUser, WithScopes("a"))
   397  				So(err, ShouldNotBeNil)
   398  			})
   399  
   400  			Convey("Trying to override aud", func() {
   401  				_, err := GetRPCTransport(ctx, AsSessionUser, WithIDTokenAudience("aud"))
   402  				So(err, ShouldNotBeNil)
   403  			})
   404  		})
   405  
   406  		Convey("when headers are needed, Request context is used", func() {
   407  			root := ctx
   408  
   409  			// Contexts with different auth state.
   410  			ctx1 := WithState(root, &state{user: &User{Identity: "user:abc@example.com"}})
   411  			ctx2 := WithState(root, &state{user: &User{Identity: "user:abc@example.com"}})
   412  
   413  			// Use a mode which actually uses transport context to compute headers.
   414  			run := func(c C, reqCtx, transCtx context.Context) (usedCtx context.Context) {
   415  				mocks := &rpcMocks{
   416  					MintAccessTokenForServiceAccount: func(ic context.Context, _ MintAccessTokenParams) (*Token, error) {
   417  						usedCtx = ic
   418  						return &Token{
   419  							Token:  "blah",
   420  							Expiry: clock.Now(ic).Add(time.Hour),
   421  						}, nil
   422  					},
   423  				}
   424  				t, err := GetRPCTransport(transCtx, AsActor, WithServiceAccount("abc@example.com"), mocks)
   425  				c.So(err, ShouldBeNil)
   426  				req := makeReq("https://example.com")
   427  				if reqCtx != nil {
   428  					req = req.WithContext(reqCtx)
   429  				}
   430  				_, err = t.RoundTrip(req)
   431  				c.So(err, ShouldBeNil)
   432  				return
   433  			}
   434  
   435  			Convey("no request context", func(c C) {
   436  				So(run(c, nil, ctx1), ShouldEqual, ctx1)
   437  			})
   438  
   439  			Convey("same context", func(c C) {
   440  				So(run(c, ctx1, ctx1), ShouldEqual, ctx1)
   441  			})
   442  
   443  			Convey("uses request context", func(c C) {
   444  				reqCtx, cancel := context.WithTimeout(ctx1, time.Minute)
   445  				defer cancel()
   446  				transCtx, cancel := context.WithTimeout(ctx1, time.Hour)
   447  				defer cancel()
   448  				So(run(c, reqCtx, transCtx), ShouldEqual, reqCtx)
   449  			})
   450  
   451  			Convey("OK on two background contexts", func(c C) {
   452  				reqCtx, cancel := context.WithTimeout(root, time.Minute)
   453  				defer cancel()
   454  				transCtx, cancel := context.WithTimeout(root, time.Hour)
   455  				defer cancel()
   456  				So(run(c, reqCtx, transCtx), ShouldEqual, reqCtx)
   457  			})
   458  
   459  			Convey("request ctx is user, transport is background", func(c C) {
   460  				reqCtx, cancel := context.WithTimeout(ctx1, time.Minute)
   461  				reqCtxDeadline, _ := reqCtx.Deadline()
   462  				defer cancel()
   463  				transCtx, cancel := context.WithTimeout(root, time.Hour)
   464  				defer cancel()
   465  				// Used `reqCtx` for the deadline, but have background auth state.
   466  				usedCtx := run(c, reqCtx, transCtx)
   467  				usedDeadline, _ := usedCtx.Deadline()
   468  				So(usedDeadline.Equal(reqCtxDeadline), ShouldBeTrue)
   469  				So(GetState(usedCtx), ShouldResemble, GetState(transCtx))
   470  			})
   471  
   472  			Convey("request ctx is background, transport is user", func(c C) {
   473  				So(func() {
   474  					reqCtx, cancel := context.WithTimeout(root, time.Minute)
   475  					defer cancel()
   476  					transCtx, cancel := context.WithTimeout(ctx1, time.Hour)
   477  					defer cancel()
   478  					run(c, reqCtx, transCtx)
   479  				}, ShouldPanic)
   480  			})
   481  
   482  			Convey("panics on contexts with different auth state", func(c C) {
   483  				So(func() {
   484  					reqCtx, cancel := context.WithTimeout(ctx1, time.Minute)
   485  					defer cancel()
   486  					transCtx, cancel := context.WithTimeout(ctx2, time.Hour)
   487  					defer cancel()
   488  					run(c, reqCtx, transCtx)
   489  				}, ShouldPanic)
   490  			})
   491  		})
   492  	})
   493  }
   494  
   495  func TestTokenSource(t *testing.T) {
   496  	t.Parallel()
   497  
   498  	Convey("GetTokenSource works", t, func() {
   499  		ctx := context.Background()
   500  		mock := &clientRPCTransportMock{}
   501  		ctx = ModifyConfig(ctx, func(cfg Config) Config {
   502  			cfg.AccessTokenProvider = mock.getAccessToken
   503  			cfg.AnonymousTransport = mock.getTransport
   504  			return cfg
   505  		})
   506  
   507  		Convey("With no scopes", func() {
   508  			ts, err := GetTokenSource(ctx, AsSelf)
   509  			So(err, ShouldBeNil)
   510  			tok, err := ts.Token()
   511  			So(err, ShouldBeNil)
   512  			So(tok, ShouldResemble, &oauth2.Token{
   513  				AccessToken: "as-self-token:https://www.googleapis.com/auth/userinfo.email",
   514  				TokenType:   "Bearer",
   515  			})
   516  		})
   517  
   518  		Convey("With a specific list of scopes", func() {
   519  			ts, err := GetTokenSource(ctx, AsSelf, WithScopes("foo", "bar", "baz"))
   520  			So(err, ShouldBeNil)
   521  			tok, err := ts.Token()
   522  			So(err, ShouldBeNil)
   523  			So(tok, ShouldResemble, &oauth2.Token{
   524  				AccessToken: "as-self-token:foo,bar,baz",
   525  				TokenType:   "Bearer",
   526  			})
   527  		})
   528  
   529  		Convey("With ID token, static aud", func() {
   530  			_, err := GetTokenSource(ctx, AsSelf, WithIDTokenAudience("https://host.example.com"))
   531  			So(err, ShouldBeNil)
   532  		})
   533  
   534  		Convey("With ID token, pattern aud", func() {
   535  			_, err := GetTokenSource(ctx, AsSelf, WithIDTokenAudience("https://${host}"))
   536  			So(err, ShouldNotBeNil)
   537  		})
   538  
   539  		Convey("NoAuth is not allowed", func() {
   540  			ts, err := GetTokenSource(ctx, NoAuth)
   541  			So(ts, ShouldBeNil)
   542  			So(err, ShouldNotBeNil)
   543  		})
   544  
   545  		Convey("AsUser is not allowed", func() {
   546  			ts, err := GetTokenSource(ctx, AsUser)
   547  			So(ts, ShouldBeNil)
   548  			So(err, ShouldNotBeNil)
   549  		})
   550  	})
   551  }
   552  
   553  func TestParseAudPattern(t *testing.T) {
   554  	t.Parallel()
   555  
   556  	Convey("Works", t, func() {
   557  		cb, err := parseAudPattern("https://${host}/zzz")
   558  		So(err, ShouldBeNil)
   559  
   560  		s, err := cb(&http.Request{
   561  			Host: "something.example.com:443",
   562  		})
   563  		So(err, ShouldBeNil)
   564  		So(s, ShouldEqual, "https://something.example.com:443/zzz")
   565  	})
   566  
   567  	Convey("Static", t, func() {
   568  		cb, err := parseAudPattern("no-vars-here")
   569  		So(cb, ShouldBeNil)
   570  		So(err, ShouldBeNil)
   571  	})
   572  
   573  	Convey("Malformed", t, func() {
   574  		cb, err := parseAudPattern("aaa-${host)-bbb")
   575  		So(cb, ShouldBeNil)
   576  		So(err, ShouldNotBeNil)
   577  	})
   578  
   579  	Convey("Unknown var", t, func() {
   580  		cb, err := parseAudPattern("aaa-${unknown}-bbb")
   581  		So(cb, ShouldBeNil)
   582  		So(err, ShouldNotBeNil)
   583  	})
   584  }
   585  
   586  func makeReq(url string) *http.Request {
   587  	req, err := http.NewRequest("GET", url, nil)
   588  	if err != nil {
   589  		panic(err)
   590  	}
   591  	return req
   592  }
   593  
   594  type fakeSession struct {
   595  	accessToken *oauth2.Token
   596  	idToken     *oauth2.Token
   597  }
   598  
   599  func (s *fakeSession) AccessToken(ctx context.Context) (*oauth2.Token, error) {
   600  	return s.accessToken, nil
   601  }
   602  
   603  func (s *fakeSession) IDToken(ctx context.Context) (*oauth2.Token, error) {
   604  	return s.idToken, nil
   605  }
   606  
   607  type clientRPCTransportMock struct {
   608  	calls [][]string
   609  	reqs  []*http.Request
   610  
   611  	cb func(req *http.Request, body string) string
   612  }
   613  
   614  func (m *clientRPCTransportMock) getAccessToken(ctx context.Context, scopes []string) (*oauth2.Token, error) {
   615  	m.calls = append(m.calls, scopes)
   616  	return &oauth2.Token{
   617  		AccessToken: "as-self-token:" + strings.Join(scopes, ","),
   618  		TokenType:   "Bearer",
   619  	}, nil
   620  }
   621  
   622  func (m *clientRPCTransportMock) getTransport(ctx context.Context) http.RoundTripper {
   623  	return m
   624  }
   625  
   626  func (m *clientRPCTransportMock) RoundTrip(req *http.Request) (*http.Response, error) {
   627  	m.reqs = append(m.reqs, req)
   628  	code := 500
   629  	resp := "internal error"
   630  	if req.Body != nil {
   631  		body, err := io.ReadAll(req.Body)
   632  		req.Body.Close()
   633  		if err != nil {
   634  			return nil, err
   635  		}
   636  		if m.cb != nil {
   637  			code = 200
   638  			resp = m.cb(req, string(body))
   639  		}
   640  	}
   641  	return &http.Response{
   642  		StatusCode: code,
   643  		Body:       io.NopCloser(bytes.NewReader([]byte(resp))),
   644  	}, nil
   645  }