go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/appengine/gaeauth/client/client_test.go (about) 1 // Copyright 2015 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 client 16 17 import ( 18 "context" 19 "fmt" 20 "math/rand" 21 "testing" 22 "time" 23 24 "golang.org/x/oauth2" 25 26 "go.chromium.org/luci/gae/impl/memory" 27 "go.chromium.org/luci/gae/service/info" 28 29 "go.chromium.org/luci/common/clock" 30 "go.chromium.org/luci/common/clock/testclock" 31 "go.chromium.org/luci/common/data/rand/mathrand" 32 "go.chromium.org/luci/server/caching" 33 34 . "github.com/smartystreets/goconvey/convey" 35 ) 36 37 func TestGetAccessToken(t *testing.T) { 38 Convey("GetAccessToken works", t, func() { 39 ctx := testContext() 40 41 // Getting initial token. 42 ctx = mockAccessTokenRPC(ctx, []string{"A", "B"}, "access_token_1", testclock.TestRecentTimeUTC.Add(time.Hour)) 43 tok, err := GetAccessToken(ctx, []string{"B", "B", "A"}) 44 So(err, ShouldBeNil) 45 So(tok, ShouldResemble, &oauth2.Token{ 46 AccessToken: "access_token_1", 47 TokenType: "Bearer", 48 Expiry: testclock.TestRecentTimeUTC.Add(time.Hour).Add(-expirationMinLifetime), 49 }) 50 51 // Some time later same cached token is used. 52 clock.Get(ctx).(testclock.TestClock).Add(30 * time.Minute) 53 54 ctx = mockAccessTokenRPC(ctx, []string{"A", "B"}, "access_token_none", testclock.TestRecentTimeUTC.Add(time.Hour)) 55 tok, err = GetAccessToken(ctx, []string{"B", "B", "A"}) 56 So(err, ShouldBeNil) 57 So(tok, ShouldResemble, &oauth2.Token{ 58 AccessToken: "access_token_1", 59 TokenType: "Bearer", 60 Expiry: testclock.TestRecentTimeUTC.Add(time.Hour).Add(-expirationMinLifetime), 61 }) 62 63 // Closer to expiration, the token is updated, at some random invocation, 64 // (depends on the seed, defines the loop limit in the test). 65 clock.Get(ctx).(testclock.TestClock).Add(26 * time.Minute) 66 for i := 0; ; i++ { 67 ctx = mockAccessTokenRPC(ctx, []string{"A", "B"}, fmt.Sprintf("access_token_%d", i+2), testclock.TestRecentTimeUTC.Add(2*time.Hour)) 68 tok, err = GetAccessToken(ctx, []string{"B", "B", "A"}) 69 So(err, ShouldBeNil) 70 if tok.AccessToken != "access_token_1" { 71 break // got refreshed token! 72 } 73 So(i, ShouldBeLessThan, 1000) // the test is hanging, this means randomization doesn't work 74 } 75 So(tok, ShouldResemble, &oauth2.Token{ 76 AccessToken: "access_token_3", 77 TokenType: "Bearer", 78 Expiry: testclock.TestRecentTimeUTC.Add(2 * time.Hour).Add(-expirationMinLifetime), 79 }) 80 81 // No randomization for token that are long expired. 82 clock.Get(ctx).(testclock.TestClock).Add(2 * time.Hour) 83 ctx = mockAccessTokenRPC(ctx, []string{"A", "B"}, "access_token_new", testclock.TestRecentTimeUTC.Add(5*time.Hour)) 84 tok, err = GetAccessToken(ctx, []string{"B", "B", "A"}) 85 So(err, ShouldBeNil) 86 So(tok.AccessToken, ShouldEqual, "access_token_new") 87 }) 88 } 89 90 type mockedInfo struct { 91 info.RawInterface 92 93 scopes []string 94 tok string 95 exp time.Time 96 } 97 98 func (m *mockedInfo) AccessToken(scopes ...string) (string, time.Time, error) { 99 So(scopes, ShouldResemble, m.scopes) 100 return m.tok, m.exp, nil 101 } 102 103 func testContext() context.Context { 104 ctx := context.Background() 105 ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC) 106 ctx = memory.Use(ctx) 107 ctx = mathrand.Set(ctx, rand.New(rand.NewSource(2))) 108 ctx = caching.WithEmptyProcessCache(ctx) 109 return ctx 110 } 111 112 func mockAccessTokenRPC(ctx context.Context, scopes []string, tok string, exp time.Time) context.Context { 113 return info.AddFilters(ctx, func(ci context.Context, i info.RawInterface) info.RawInterface { 114 return &mockedInfo{i, scopes, tok, exp} 115 }) 116 }