ContainerImage.Pinniped/internal/execcredcache/execcredcache_test.go
Ryan Richard c6c2c525a6 Upgrade the linter and fix all new linter warnings
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).
2022-08-24 14:45:55 -07:00

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") }