blob: 11a1c8c7ea72cab3696183f72294a188254da8de [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package internal
import (
"context"
"math/rand"
"testing"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
. "github.com/smartystreets/goconvey/convey"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/data/rand/mathrand"
)
func TestCacheKey(t *testing.T) {
t.Parallel()
Convey("ToMapKey empty", t, func() {
k := CacheKey{}
So(k.ToMapKey(), ShouldEqual, "\x00")
})
Convey("ToMapKey works", t, func() {
k := CacheKey{
Key: "a",
Scopes: []string{"x", "y", "z"},
}
So(k.ToMapKey(), ShouldEqual, "a\x00x\x00y\x00z\x00")
})
Convey("EqualCacheKeys works", t, func() {
So(EqualCacheKeys(nil, nil), ShouldBeTrue)
So(EqualCacheKeys(&CacheKey{}, nil), ShouldBeFalse)
So(EqualCacheKeys(nil, &CacheKey{}), ShouldBeFalse)
So(EqualCacheKeys(
&CacheKey{
Key: "k",
Scopes: []string{"a", "b"},
},
&CacheKey{
Key: "k",
Scopes: []string{"a", "b"},
}),
ShouldBeTrue)
So(EqualCacheKeys(
&CacheKey{
Key: "k1",
Scopes: []string{"a", "b"},
},
&CacheKey{
Key: "k2",
Scopes: []string{"a", "b"},
}),
ShouldBeFalse)
So(EqualCacheKeys(
&CacheKey{
Key: "k",
Scopes: []string{"a1", "b"},
},
&CacheKey{
Key: "k",
Scopes: []string{"a2", "b"},
}),
ShouldBeFalse)
So(EqualCacheKeys(
&CacheKey{
Key: "k",
Scopes: []string{"a"},
},
&CacheKey{
Key: "k",
Scopes: []string{"a", "b"},
}),
ShouldBeFalse)
})
}
func TestTokenHelpers(t *testing.T) {
t.Parallel()
ctx := mathrand.Set(context.Background(), rand.New(rand.NewSource(123)))
ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeLocal)
exp := testclock.TestRecentTimeLocal.Add(time.Hour)
Convey("TokenExpiresIn works", t, func() {
// Invalid tokens.
So(TokenExpiresIn(ctx, nil, time.Minute), ShouldBeTrue)
So(TokenExpiresIn(ctx, &Token{
Token: oauth2.Token{
AccessToken: "",
Expiry: exp,
},
}, time.Minute), ShouldBeTrue)
// If expiry is not set, the token is non-expirable.
So(TokenExpiresIn(ctx, &Token{
Token: oauth2.Token{AccessToken: "abc"},
}, 10*time.Hour), ShouldBeFalse)
So(TokenExpiresIn(ctx, &Token{
Token: oauth2.Token{
AccessToken: "abc",
Expiry: exp,
},
}, time.Minute), ShouldBeFalse)
tc.Add(59*time.Minute + 1*time.Second)
So(TokenExpiresIn(ctx, &Token{
Token: oauth2.Token{
AccessToken: "abc",
Expiry: exp,
},
}, time.Minute), ShouldBeTrue)
})
Convey("TokenExpiresInRnd works", t, func() {
// Invalid tokens.
So(TokenExpiresInRnd(ctx, nil, time.Minute), ShouldBeTrue)
So(TokenExpiresInRnd(ctx, &Token{
Token: oauth2.Token{
AccessToken: "",
Expiry: exp,
},
}, time.Minute), ShouldBeTrue)
// If expiry is not set, the token is non-expirable.
So(TokenExpiresInRnd(ctx, &Token{
Token: oauth2.Token{AccessToken: "abc"},
}, 10*time.Hour), ShouldBeFalse)
// Generate a histogram of positive TokenExpiresInRnd responses per second,
// for the duration of 10 min, assuming each TokenExpiresInRnd is called
// 100 times per second.
tokenLifetime := 5 * time.Minute
requestedLifetime := time.Minute
tok := &Token{
Token: oauth2.Token{
AccessToken: "abc",
Expiry: clock.Now(ctx).Add(tokenLifetime),
},
}
hist := make([]int, 600)
for s := 0; s < 600; s++ {
for i := 0; i < 100; i++ {
if TokenExpiresInRnd(ctx, tok, requestedLifetime) {
hist[s]++
}
tc.Add(10 * time.Millisecond)
}
}
// The histogram should have a shape:
// * 0 samples until somewhere around 3 min 30 sec
// (which is tokenLifetime - requestedLifetime - expiryRandInterval).
// * increasingly more non zero samples in the following
// expiryRandInterval seconds (3 min 30 sec - 4 min 00 sec).
// * 100% samples after tokenLifetime - requestedLifetime (> 4 min).
firstNonZero := -1
firstFull := -1
for i, val := range hist {
switch {
case val != 0 && firstNonZero == -1:
firstNonZero = i
case val == 100 && firstFull == -1:
firstFull = i
}
}
// The first non-zero sample is at 3 min 30 sec (+1, by chance).
So(firstNonZero, ShouldEqual, 3*60+30+1)
// The first 100% sample is at 4 min (-1, by chance).
So(firstFull, ShouldEqual, 4*60-1)
// The in-between contains linearly increasing chance of early expiry, as
// can totally be seen from this assertion.
So(hist[firstNonZero:firstFull], ShouldResemble, []int{
8, 6, 8, 23, 20, 27, 28, 29, 25, 36, 43, 41, 53, 49,
49, 57, 59, 62, 69, 69, 76, 72, 82, 73, 85, 83, 93, 93,
})
})
}
func TestIsBadKeyError(t *testing.T) {
Convey("Correctly sniffs out bad key error", t, func() {
brokenKeyJSON := `{
"type": "service_account",
"project_id": "blah",
"private_key_id": "zzzzzzzzzzzzzzzzzz",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhBROKENq\nEpldRN3tNnLoLNy6\n-----END PRIVATE KEY-----\n",
"client_email": "blah@blah.iam.gserviceaccount.com",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs"
}`
cfg, err := google.JWTConfigFromJSON([]byte(brokenKeyJSON), "scope")
So(err, ShouldBeNil)
_, err = cfg.TokenSource(context.Background()).Token()
So(err, ShouldNotBeNil)
So(isBadKeyError(err), ShouldBeTrue)
})
}