270 lines
7.2 KiB
Go
270 lines
7.2 KiB
Go
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package filesession
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"go.pinniped.dev/pkg/oidcclient"
|
|
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
|
)
|
|
|
|
// validSession should be the same data as `testdata/valid.yaml`.
|
|
var validSession = sessionCache{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "config.supervisor.pinniped.dev/v1alpha1", Kind: "SessionCache"},
|
|
Sessions: []sessionEntry{
|
|
{
|
|
Key: oidcclient.SessionCacheKey{
|
|
Issuer: "test-issuer",
|
|
ClientID: "test-client-id",
|
|
Scopes: []string{"email", "offline_access", "openid", "profile"},
|
|
RedirectURI: "http://localhost:0/callback",
|
|
},
|
|
CreationTimestamp: metav1.NewTime(time.Date(2020, 10, 20, 18, 42, 7, 0, time.UTC).Local()),
|
|
LastUsedTimestamp: metav1.NewTime(time.Date(2020, 10, 20, 18, 45, 31, 0, time.UTC).Local()),
|
|
Tokens: oidctypes.Token{
|
|
AccessToken: &oidctypes.AccessToken{
|
|
Token: "test-access-token",
|
|
Type: "Bearer",
|
|
Expiry: metav1.NewTime(time.Date(2020, 10, 20, 19, 46, 30, 0, time.UTC).Local()),
|
|
},
|
|
IDToken: &oidctypes.IDToken{
|
|
Token: "test-id-token",
|
|
Expiry: metav1.NewTime(time.Date(2020, 10, 20, 19, 42, 07, 0, time.UTC).Local()),
|
|
Claims: map[string]interface{}{
|
|
"foo": "bar",
|
|
"nested": map[string]interface{}{
|
|
"key1": "value1",
|
|
"key2": "value2",
|
|
},
|
|
},
|
|
},
|
|
RefreshToken: &oidctypes.RefreshToken{
|
|
Token: "test-refresh-token",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestReadSessionCache(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
want *sessionCache
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "does not exist",
|
|
path: "./testdata/does-not-exist.yaml",
|
|
want: &sessionCache{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "config.supervisor.pinniped.dev/v1alpha1", Kind: "SessionCache"},
|
|
Sessions: []sessionEntry{},
|
|
},
|
|
},
|
|
{
|
|
name: "other file error",
|
|
path: "./testdata/",
|
|
wantErr: "could not read session file: read ./testdata/: is a directory",
|
|
},
|
|
{
|
|
name: "invalid YAML",
|
|
path: "./testdata/invalid.yaml",
|
|
wantErr: "invalid session file: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type filesession.sessionCache",
|
|
},
|
|
{
|
|
name: "wrong version",
|
|
path: "./testdata/wrong-version.yaml",
|
|
wantErr: `unsupported session version: v1.TypeMeta{Kind:"NotASessionCache", APIVersion:"config.supervisor.pinniped.dev/v2alpha6"}`,
|
|
},
|
|
{
|
|
name: "valid",
|
|
path: "./testdata/valid.yaml",
|
|
want: &validSession,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, err := readSessionCache(tt.path)
|
|
if tt.wantErr != "" {
|
|
require.EqualError(t, err, tt.wantErr)
|
|
require.Nil(t, got)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEmptySessionCache(t *testing.T) {
|
|
t.Parallel()
|
|
got := emptySessionCache()
|
|
require.Equal(t, metav1.TypeMeta{APIVersion: "config.supervisor.pinniped.dev/v1alpha1", Kind: "SessionCache"}, got.TypeMeta)
|
|
require.Equal(t, 0, len(got.Sessions))
|
|
require.Equal(t, 1, cap(got.Sessions))
|
|
}
|
|
|
|
func TestWriteTo(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("io error", func(t *testing.T) {
|
|
t.Parallel()
|
|
tmp := t.TempDir() + "/sessions.yaml"
|
|
require.NoError(t, os.Mkdir(tmp, 0700))
|
|
err := validSession.writeTo(tmp)
|
|
require.EqualError(t, err, "open "+tmp+": is a directory")
|
|
})
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
t.Parallel()
|
|
require.NoError(t, validSession.writeTo(t.TempDir()+"/sessions.yaml"))
|
|
})
|
|
}
|
|
|
|
func TestNormalized(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("empty", func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, emptySessionCache(), emptySessionCache().normalized())
|
|
})
|
|
|
|
t.Run("nonempty", func(t *testing.T) {
|
|
t.Parallel()
|
|
input := emptySessionCache()
|
|
now := time.Now()
|
|
input.Sessions = []sessionEntry{
|
|
// ID token is empty, but not nil.
|
|
{
|
|
LastUsedTimestamp: metav1.NewTime(now),
|
|
Tokens: oidctypes.Token{
|
|
IDToken: &oidctypes.IDToken{
|
|
Token: "",
|
|
Expiry: metav1.NewTime(now.Add(1 * time.Minute)),
|
|
},
|
|
},
|
|
},
|
|
// ID token is expired.
|
|
{
|
|
LastUsedTimestamp: metav1.NewTime(now),
|
|
Tokens: oidctypes.Token{
|
|
IDToken: &oidctypes.IDToken{
|
|
Token: "test-id-token",
|
|
Expiry: metav1.NewTime(now.Add(-1 * time.Minute)),
|
|
},
|
|
},
|
|
},
|
|
// Access token is empty, but not nil.
|
|
{
|
|
LastUsedTimestamp: metav1.NewTime(now),
|
|
Tokens: oidctypes.Token{
|
|
AccessToken: &oidctypes.AccessToken{
|
|
Token: "",
|
|
Expiry: metav1.NewTime(now.Add(1 * time.Minute)),
|
|
},
|
|
},
|
|
},
|
|
// Access token is expired.
|
|
{
|
|
LastUsedTimestamp: metav1.NewTime(now),
|
|
Tokens: oidctypes.Token{
|
|
AccessToken: &oidctypes.AccessToken{
|
|
Token: "test-access-token",
|
|
Expiry: metav1.NewTime(now.Add(-1 * time.Minute)),
|
|
},
|
|
},
|
|
},
|
|
// Refresh token is empty, but not nil.
|
|
{
|
|
LastUsedTimestamp: metav1.NewTime(now),
|
|
Tokens: oidctypes.Token{
|
|
RefreshToken: &oidctypes.RefreshToken{
|
|
Token: "",
|
|
},
|
|
},
|
|
},
|
|
// Session has a refresh token but it hasn't been used in >90 days.
|
|
{
|
|
LastUsedTimestamp: metav1.NewTime(now.AddDate(-1, 0, 0)),
|
|
Tokens: oidctypes.Token{
|
|
RefreshToken: &oidctypes.RefreshToken{
|
|
Token: "test-refresh-token",
|
|
},
|
|
},
|
|
},
|
|
// Two entries that are still valid.
|
|
{
|
|
CreationTimestamp: metav1.NewTime(now.Add(-1 * time.Hour)),
|
|
LastUsedTimestamp: metav1.NewTime(now),
|
|
Tokens: oidctypes.Token{
|
|
RefreshToken: &oidctypes.RefreshToken{
|
|
Token: "test-refresh-token2",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Hour)),
|
|
LastUsedTimestamp: metav1.NewTime(now),
|
|
Tokens: oidctypes.Token{
|
|
RefreshToken: &oidctypes.RefreshToken{
|
|
Token: "test-refresh-token1",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Expect that all but the last two valid session are pruned, and that they're sorted.
|
|
require.Equal(t, &sessionCache{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "config.supervisor.pinniped.dev/v1alpha1", Kind: "SessionCache"},
|
|
Sessions: []sessionEntry{
|
|
{
|
|
CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Hour)),
|
|
LastUsedTimestamp: metav1.NewTime(now),
|
|
Tokens: oidctypes.Token{
|
|
RefreshToken: &oidctypes.RefreshToken{
|
|
Token: "test-refresh-token1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
CreationTimestamp: metav1.NewTime(now.Add(-1 * time.Hour)),
|
|
LastUsedTimestamp: metav1.NewTime(now),
|
|
Tokens: oidctypes.Token{
|
|
RefreshToken: &oidctypes.RefreshToken{
|
|
Token: "test-refresh-token2",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, input.normalized())
|
|
})
|
|
}
|
|
|
|
func TestLookup(t *testing.T) {
|
|
t.Parallel()
|
|
require.Nil(t, validSession.lookup(oidcclient.SessionCacheKey{}))
|
|
require.NotNil(t, validSession.lookup(oidcclient.SessionCacheKey{
|
|
Issuer: "test-issuer",
|
|
ClientID: "test-client-id",
|
|
Scopes: []string{"email", "offline_access", "openid", "profile"},
|
|
RedirectURI: "http://localhost:0/callback",
|
|
}))
|
|
}
|
|
|
|
func TestInsert(t *testing.T) {
|
|
t.Parallel()
|
|
c := emptySessionCache()
|
|
c.insert(sessionEntry{})
|
|
require.Len(t, c.Sessions, 1)
|
|
}
|