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  }