c6c2c525a6
Also fix some tests that were broken by bumping golang and dependencies in the previous commits. Note that in addition to changes made to satisfy the linter which do not impact the behavior of the code, this commit also adds ReadHeaderTimeout to all usages of http.Server to satisfy the linter (and because it seemed like a good suggestion).
389 lines
13 KiB
Go
389 lines
13 KiB
Go
// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package execcredcache
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
|
|
|
"go.pinniped.dev/internal/testutil"
|
|
)
|
|
|
|
func TestNew(t *testing.T) {
|
|
t.Parallel()
|
|
tmp := testutil.TempDir(t) + "/credentials.yaml"
|
|
c := New(tmp)
|
|
require.NotNil(t, c)
|
|
require.Equal(t, tmp, c.path)
|
|
require.NotNil(t, c.errReporter)
|
|
c.errReporter(fmt.Errorf("some error"))
|
|
}
|
|
|
|
func TestGet(t *testing.T) {
|
|
t.Parallel()
|
|
now := time.Now().Round(1 * time.Second)
|
|
oneHourFromNow := metav1.NewTime(now.Add(1 * time.Hour))
|
|
|
|
type testKey struct{ K1, K2 string }
|
|
|
|
tests := []struct {
|
|
name string
|
|
makeTestFile func(t *testing.T, tmp string)
|
|
trylockFunc func(*testing.T) error
|
|
unlockFunc func(*testing.T) error
|
|
key testKey
|
|
want *clientauthenticationv1beta1.ExecCredential
|
|
wantErrors []string
|
|
wantTestFile func(t *testing.T, tmp string)
|
|
}{
|
|
{
|
|
name: "not found",
|
|
key: testKey{},
|
|
},
|
|
{
|
|
name: "file lock error",
|
|
makeTestFile: func(t *testing.T, tmp string) { require.NoError(t, os.WriteFile(tmp, []byte(""), 0600)) },
|
|
trylockFunc: func(t *testing.T) error { return fmt.Errorf("some lock error") },
|
|
unlockFunc: func(t *testing.T) error { require.Fail(t, "should not be called"); return nil },
|
|
key: testKey{},
|
|
wantErrors: []string{"could not lock cache file: some lock error"},
|
|
},
|
|
{
|
|
name: "invalid file",
|
|
makeTestFile: func(t *testing.T, tmp string) {
|
|
require.NoError(t, os.WriteFile(tmp, []byte("invalid yaml"), 0600))
|
|
},
|
|
key: testKey{},
|
|
wantErrors: []string{
|
|
"failed to read cache, resetting: invalid cache file: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type execcredcache.credCache",
|
|
},
|
|
},
|
|
{
|
|
name: "invalid file, fail to unlock",
|
|
makeTestFile: func(t *testing.T, tmp string) { require.NoError(t, os.WriteFile(tmp, []byte("invalid"), 0600)) },
|
|
trylockFunc: func(t *testing.T) error { return nil },
|
|
unlockFunc: func(t *testing.T) error { return fmt.Errorf("some unlock error") },
|
|
key: testKey{},
|
|
wantErrors: []string{
|
|
"failed to read cache, resetting: invalid cache file: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type execcredcache.credCache",
|
|
"could not unlock cache file: some unlock error",
|
|
},
|
|
},
|
|
{
|
|
name: "unreadable file",
|
|
makeTestFile: func(t *testing.T, tmp string) {
|
|
require.NoError(t, os.Mkdir(tmp, 0700))
|
|
},
|
|
key: testKey{},
|
|
wantErrors: []string{
|
|
"failed to read cache, resetting: could not read cache file: read TEMPFILE: is a directory",
|
|
"could not write cache: open TEMPFILE: is a directory",
|
|
},
|
|
},
|
|
{
|
|
name: "valid file but cache miss",
|
|
makeTestFile: func(t *testing.T, tmp string) {
|
|
validCache := emptyCache()
|
|
validCache.Entries = []entry{{
|
|
Key: jsonSHA256Hex(testKey{K1: "v3", K2: "v4"}),
|
|
CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)),
|
|
LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)),
|
|
Credential: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
Token: "test-token",
|
|
ExpirationTimestamp: &oneHourFromNow,
|
|
},
|
|
}}
|
|
require.NoError(t, validCache.writeTo(tmp))
|
|
},
|
|
key: testKey{K1: "v1", K2: "v2"},
|
|
wantErrors: []string{},
|
|
},
|
|
{
|
|
name: "valid file but expired cache hit",
|
|
makeTestFile: func(t *testing.T, tmp string) {
|
|
validCache := emptyCache()
|
|
oneMinuteAgo := metav1.NewTime(now.Add(-1 * time.Minute))
|
|
validCache.Entries = []entry{{
|
|
Key: jsonSHA256Hex(testKey{K1: "v1", K2: "v2"}),
|
|
CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)),
|
|
LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)),
|
|
Credential: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
Token: "test-token",
|
|
ExpirationTimestamp: &oneMinuteAgo,
|
|
},
|
|
}}
|
|
require.NoError(t, validCache.writeTo(tmp))
|
|
},
|
|
key: testKey{K1: "v1", K2: "v2"},
|
|
wantErrors: []string{},
|
|
},
|
|
{
|
|
name: "valid file with cache hit",
|
|
makeTestFile: func(t *testing.T, tmp string) {
|
|
validCache := emptyCache()
|
|
|
|
validCache.Entries = []entry{{
|
|
Key: jsonSHA256Hex(testKey{K1: "v1", K2: "v2"}),
|
|
CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)),
|
|
LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)),
|
|
Credential: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
Token: "test-token",
|
|
ExpirationTimestamp: &oneHourFromNow,
|
|
},
|
|
}}
|
|
require.NoError(t, validCache.writeTo(tmp))
|
|
},
|
|
key: testKey{K1: "v1", K2: "v2"},
|
|
wantErrors: []string{},
|
|
want: &clientauthenticationv1beta1.ExecCredential{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "ExecCredential",
|
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
},
|
|
Spec: clientauthenticationv1beta1.ExecCredentialSpec{},
|
|
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
Token: "test-token",
|
|
ExpirationTimestamp: &oneHourFromNow,
|
|
},
|
|
},
|
|
wantTestFile: func(t *testing.T, tmp string) {
|
|
cache, err := readCache(tmp)
|
|
require.NoError(t, err)
|
|
require.Len(t, cache.Entries, 1)
|
|
require.Less(t, time.Since(cache.Entries[0].LastUsedTimestamp.Time).Nanoseconds(), (5 * time.Second).Nanoseconds())
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
tmp := testutil.TempDir(t) + "/sessions.yaml"
|
|
if tt.makeTestFile != nil {
|
|
tt.makeTestFile(t, tmp)
|
|
}
|
|
|
|
// Initialize a cache with a reporter that collects errors
|
|
errors := errorCollector{t: t}
|
|
c := New(tmp)
|
|
c.errReporter = errors.report
|
|
if tt.trylockFunc != nil {
|
|
c.trylockFunc = func() error { return tt.trylockFunc(t) }
|
|
}
|
|
if tt.unlockFunc != nil {
|
|
c.unlockFunc = func() error { return tt.unlockFunc(t) }
|
|
}
|
|
|
|
got := c.Get(tt.key)
|
|
require.Equal(t, tt.want, got)
|
|
errors.require(tt.wantErrors, "TEMPFILE", tmp)
|
|
if tt.wantTestFile != nil {
|
|
tt.wantTestFile(t, tmp)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPutToken(t *testing.T) {
|
|
t.Parallel()
|
|
now := time.Now().Round(1 * time.Second)
|
|
|
|
type testKey struct{ K1, K2 string }
|
|
|
|
tests := []struct {
|
|
name string
|
|
makeTestFile func(t *testing.T, tmp string)
|
|
key testKey
|
|
cred *clientauthenticationv1beta1.ExecCredential
|
|
wantErrors []string
|
|
wantTestFile func(t *testing.T, tmp string)
|
|
}{
|
|
{
|
|
name: "fail to create directory",
|
|
makeTestFile: func(t *testing.T, tmp string) {
|
|
require.NoError(t, os.WriteFile(filepath.Dir(tmp), []byte{}, 0600))
|
|
},
|
|
wantErrors: []string{
|
|
"could not create credential cache directory: mkdir TEMPDIR: not a directory",
|
|
},
|
|
},
|
|
{
|
|
name: "update to existing entry",
|
|
makeTestFile: func(t *testing.T, tmp string) {
|
|
validCache := emptyCache()
|
|
validCache.Entries = []entry{
|
|
{
|
|
Key: jsonSHA256Hex(testKey{K1: "v1", K2: "v2"}),
|
|
CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)),
|
|
LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)),
|
|
Credential: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)),
|
|
Token: "token-one",
|
|
},
|
|
},
|
|
|
|
// A second entry that was created over a day ago.
|
|
{
|
|
Key: jsonSHA256Hex(testKey{K1: "v3", K2: "v4"}),
|
|
CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Hour)),
|
|
LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Hour)),
|
|
Credential: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)),
|
|
Token: "token-two",
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(tmp), 0700))
|
|
require.NoError(t, validCache.writeTo(tmp))
|
|
},
|
|
key: testKey{K1: "v1", K2: "v2"},
|
|
cred: &clientauthenticationv1beta1.ExecCredential{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "ExecCredential",
|
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
},
|
|
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)),
|
|
Token: "token-one",
|
|
},
|
|
},
|
|
wantTestFile: func(t *testing.T, tmp string) {
|
|
cache, err := readCache(tmp)
|
|
require.NoError(t, err)
|
|
require.Len(t, cache.Entries, 1)
|
|
require.Less(t, time.Since(cache.Entries[0].LastUsedTimestamp.Time).Nanoseconds(), (5 * time.Second).Nanoseconds())
|
|
require.Equal(t, &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
ExpirationTimestamp: timePtr(now.Add(1 * time.Hour).Local()),
|
|
Token: "token-one",
|
|
}, cache.Entries[0].Credential)
|
|
},
|
|
},
|
|
{
|
|
name: "new entry",
|
|
makeTestFile: func(t *testing.T, tmp string) {
|
|
validCache := emptyCache()
|
|
validCache.Entries = []entry{
|
|
{
|
|
Key: jsonSHA256Hex(testKey{K1: "v3", K2: "v4"}),
|
|
CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)),
|
|
LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)),
|
|
Credential: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)),
|
|
Token: "other-token",
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(tmp), 0700))
|
|
require.NoError(t, validCache.writeTo(tmp))
|
|
},
|
|
key: testKey{K1: "v1", K2: "v2"},
|
|
cred: &clientauthenticationv1beta1.ExecCredential{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "ExecCredential",
|
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
},
|
|
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)),
|
|
Token: "token-one",
|
|
},
|
|
},
|
|
wantTestFile: func(t *testing.T, tmp string) {
|
|
cache, err := readCache(tmp)
|
|
require.NoError(t, err)
|
|
require.Len(t, cache.Entries, 2)
|
|
require.Less(t, time.Since(cache.Entries[1].LastUsedTimestamp.Time).Nanoseconds(), (5 * time.Second).Nanoseconds())
|
|
require.Equal(t, &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
ExpirationTimestamp: timePtr(now.Add(1 * time.Hour).Local()),
|
|
Token: "token-one",
|
|
}, cache.Entries[1].Credential)
|
|
},
|
|
},
|
|
{
|
|
name: "error writing cache",
|
|
makeTestFile: func(t *testing.T, tmp string) {
|
|
require.NoError(t, os.MkdirAll(tmp, 0700))
|
|
},
|
|
key: testKey{K1: "v1", K2: "v2"},
|
|
cred: &clientauthenticationv1beta1.ExecCredential{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "ExecCredential",
|
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
},
|
|
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)),
|
|
Token: "token-one",
|
|
},
|
|
},
|
|
wantErrors: []string{
|
|
"failed to read cache, resetting: could not read cache file: read TEMPFILE: is a directory",
|
|
"could not write cache: open TEMPFILE: is a directory",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
tmp := testutil.TempDir(t) + "/cachedir/credentials.yaml"
|
|
if tt.makeTestFile != nil {
|
|
tt.makeTestFile(t, tmp)
|
|
}
|
|
// Initialize a cache with a reporter that collects errors
|
|
errors := errorCollector{t: t}
|
|
c := New(tmp)
|
|
c.errReporter = errors.report
|
|
c.Put(tt.key, tt.cred)
|
|
errors.require(tt.wantErrors, "TEMPFILE", tmp, "TEMPDIR", filepath.Dir(tmp))
|
|
if tt.wantTestFile != nil {
|
|
tt.wantTestFile(t, tmp)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHashing(t *testing.T) {
|
|
type testKey struct{ K1, K2 string }
|
|
require.Equal(t, "38e0b9de817f645c4bec37c0d4a3e58baecccb040f5718dc069a72c7385a0bed", jsonSHA256Hex(nil))
|
|
require.Equal(t, "625bb1f93dc90a1bda400fdaceb8c96328e567a0c6aaf81e7fccc68958b4565d", jsonSHA256Hex([]string{"k1", "k2"}))
|
|
require.Equal(t, "8fb659f5dd266ffd8d0c96116db1d96fe10e3879f9cb6f7e9ace016696ff69f6", jsonSHA256Hex(testKey{K1: "v1", K2: "v2"}))
|
|
require.Equal(t, "42c783a2c29f91127b064df368bda61788181d2dd1709b417f9506102ea8da67", jsonSHA256Hex(testKey{K1: "v3", K2: "v4"}))
|
|
require.Panics(t, func() { jsonSHA256Hex(&unmarshalable{}) })
|
|
}
|
|
|
|
type errorCollector struct {
|
|
t *testing.T
|
|
saw []error
|
|
}
|
|
|
|
func (e *errorCollector) report(err error) {
|
|
e.saw = append(e.saw, err)
|
|
}
|
|
|
|
func (e *errorCollector) require(want []string, subs ...string) {
|
|
require.Len(e.t, e.saw, len(want))
|
|
for i, w := range want {
|
|
for i := 0; i < len(subs); i += 2 {
|
|
w = strings.ReplaceAll(w, subs[i], subs[i+1])
|
|
}
|
|
require.EqualError(e.t, e.saw[i], w)
|
|
}
|
|
}
|
|
|
|
func timePtr(from time.Time) *metav1.Time {
|
|
t := metav1.NewTime(from)
|
|
return &t
|
|
}
|
|
|
|
type unmarshalable struct{}
|
|
|
|
func (*unmarshalable) MarshalJSON() ([]byte, error) { return nil, fmt.Errorf("some MarshalJSON error") }
|