1233 lines
35 KiB
Go
1233 lines
35 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cache
|
|
|
|
import (
|
|
"context"
|
|
"database/sql/driver"
|
|
stderrors "errors"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/api/aliases"
|
|
"github.com/hashicorp/boundary/api/authtokens"
|
|
"github.com/hashicorp/boundary/api/sessions"
|
|
"github.com/hashicorp/boundary/api/targets"
|
|
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/go-dbw"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
func TestRepository_AddKeyringToken(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
addr := "address"
|
|
u := user{
|
|
Id: "u1",
|
|
Address: addr,
|
|
}
|
|
at := &authtokens.AuthToken{
|
|
Id: "at_1",
|
|
Token: "at_1_token",
|
|
UserId: u.Id,
|
|
}
|
|
// used to test mismatched user ids between keyring and cache
|
|
mismatchingAt := &authtokens.AuthToken{
|
|
Id: "at_mismatch",
|
|
Token: "at_mismatch_token",
|
|
UserId: u.Id,
|
|
}
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at, mismatchingAt}
|
|
keyring := "k"
|
|
tokenName := "t"
|
|
atMap := make(map[ringToken]*authtokens.AuthToken)
|
|
atMap[ringToken{keyring, tokenName}] = at
|
|
atMap[ringToken{"mismatch", "mismatch"}] = mismatchingAt
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
|
|
t.Run("userid mismatch between db and keyring", func(t *testing.T) {
|
|
require.NoError(t, r.AddKeyringToken(ctx, "address", KeyringToken{
|
|
KeyringType: "mismatch",
|
|
TokenName: "mismatch",
|
|
AuthTokenId: mismatchingAt.Id,
|
|
}))
|
|
|
|
mismatchingAt.UserId = "changedToMismatch"
|
|
assert.ErrorContains(t, r.AddKeyringToken(ctx, "address", KeyringToken{
|
|
KeyringType: "mismatch",
|
|
TokenName: "mismatch",
|
|
AuthTokenId: mismatchingAt.Id,
|
|
}), "user id doesn't match what is specified in the stored auth token")
|
|
})
|
|
|
|
errCases := []struct {
|
|
name string
|
|
addr string
|
|
kt KeyringToken
|
|
errorContains string
|
|
}{
|
|
{
|
|
name: "success",
|
|
addr: "address",
|
|
kt: KeyringToken{
|
|
KeyringType: keyring,
|
|
TokenName: tokenName,
|
|
AuthTokenId: at.Id,
|
|
},
|
|
errorContains: "",
|
|
},
|
|
{
|
|
name: "not in keyring",
|
|
addr: "address",
|
|
kt: KeyringToken{
|
|
KeyringType: keyring,
|
|
TokenName: "unknowntokenname",
|
|
AuthTokenId: at.Id,
|
|
},
|
|
errorContains: "unable to find token in the keyring specified",
|
|
},
|
|
{
|
|
name: "missing address",
|
|
kt: KeyringToken{
|
|
KeyringType: keyring,
|
|
TokenName: tokenName,
|
|
AuthTokenId: at.Id,
|
|
},
|
|
errorContains: "boundary address is empty",
|
|
},
|
|
{
|
|
name: "missing token",
|
|
addr: "address",
|
|
kt: KeyringToken{
|
|
KeyringType: keyring,
|
|
AuthTokenId: at.Id,
|
|
},
|
|
errorContains: "token name is empty",
|
|
},
|
|
{
|
|
name: "missing keyring type",
|
|
addr: "address",
|
|
kt: KeyringToken{
|
|
TokenName: tokenName,
|
|
AuthTokenId: at.Id,
|
|
},
|
|
errorContains: "keyring type is empty",
|
|
},
|
|
{
|
|
name: "missing auth token id",
|
|
addr: "address",
|
|
kt: KeyringToken{
|
|
KeyringType: keyring,
|
|
TokenName: tokenName,
|
|
},
|
|
errorContains: "boundary auth token id is empty",
|
|
},
|
|
{
|
|
name: "unmatching auth token id",
|
|
addr: "address",
|
|
kt: KeyringToken{
|
|
KeyringType: keyring,
|
|
TokenName: tokenName,
|
|
AuthTokenId: "wrong auth token id",
|
|
},
|
|
errorContains: "provided auth token id doesn't match",
|
|
},
|
|
}
|
|
|
|
for _, tc := range errCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := r.AddKeyringToken(ctx, tc.addr, tc.kt)
|
|
if tc.errorContains == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
assert.ErrorContains(t, err, tc.errorContains)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("When the keyring function errors", func(t *testing.T) {
|
|
keyringFn := func(keyring, tokenName string) (*authtokens.AuthToken, error) {
|
|
return nil, stderrors.New("keyring lookup function failed")
|
|
}
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, keyringFn, sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
|
|
err = r.AddKeyringToken(ctx, "address", KeyringToken{
|
|
KeyringType: "k",
|
|
TokenName: "t",
|
|
AuthTokenId: "at_1",
|
|
})
|
|
assert.ErrorContains(t, err, "keyring lookup function failed")
|
|
})
|
|
}
|
|
|
|
func TestRepository_AddRawToken(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
at := &authtokens.AuthToken{
|
|
Id: "at_1",
|
|
Token: "at_1_token",
|
|
UserId: "u1",
|
|
}
|
|
existingAt := &authtokens.AuthToken{
|
|
Id: "at_existing",
|
|
Token: "at_existing_token",
|
|
UserId: "u2",
|
|
}
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at, existingAt}
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(map[ringToken]*authtokens.AuthToken{}), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
|
|
t.Run("mismatched userid between memory and db", func(t *testing.T) {
|
|
require.NoError(t, r.AddRawToken(ctx, "address", existingAt.Token))
|
|
loadedExistingV, loaded := r.idToKeyringlessAuthToken.Load(existingAt.Id)
|
|
require.True(t, loaded)
|
|
loadedExistingAt := loadedExistingV.(*authtokens.AuthToken)
|
|
loadedExistingAt.UserId = "mismatchingUserId"
|
|
r.idToKeyringlessAuthToken.Store(existingAt.Id, loadedExistingAt)
|
|
|
|
err := r.AddRawToken(ctx, "address", loadedExistingAt.Token)
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "user id doesn't match what is specified in the stored auth token")
|
|
})
|
|
|
|
errCases := []struct {
|
|
name string
|
|
addr string
|
|
rawAt string
|
|
errorContains string
|
|
}{
|
|
{
|
|
name: "success",
|
|
addr: "address",
|
|
rawAt: at.Token,
|
|
errorContains: "",
|
|
},
|
|
{
|
|
name: "missing address",
|
|
rawAt: at.Token,
|
|
errorContains: "boundary address is empty",
|
|
},
|
|
{
|
|
name: "missing token",
|
|
addr: "address",
|
|
errorContains: "auth token is empty",
|
|
},
|
|
{
|
|
name: "malformed auth token",
|
|
addr: "address",
|
|
rawAt: fmt.Sprintf("%s_extraunderscore", at.Token),
|
|
errorContains: "boundary auth token is is malformed",
|
|
},
|
|
{
|
|
name: "not found in boundary",
|
|
addr: "address",
|
|
rawAt: "at_123_notfoundinboundary",
|
|
errorContains: "not found",
|
|
},
|
|
}
|
|
|
|
for _, tc := range errCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := r.AddRawToken(ctx, tc.addr, tc.rawAt)
|
|
if tc.errorContains == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
assert.ErrorContains(t, err, tc.errorContains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRepository_AddToken_EvictsOverLimitUsers(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{
|
|
{
|
|
UserId: "user_base",
|
|
Id: "at_base",
|
|
Token: "at_base_token",
|
|
},
|
|
}
|
|
for i := 0; i < usersLimit; i++ {
|
|
iAt := &authtokens.AuthToken{
|
|
UserId: fmt.Sprintf("user%d", i),
|
|
Id: fmt.Sprintf("at_%d", i),
|
|
}
|
|
iAt.Token = fmt.Sprintf("%s_token", iAt.Id)
|
|
boundaryAuthTokens = append(boundaryAuthTokens, iAt)
|
|
}
|
|
|
|
atMap := make(map[ringToken]*authtokens.AuthToken)
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
|
|
addr := "address"
|
|
kt := KeyringToken{
|
|
KeyringType: "keyring",
|
|
TokenName: "token",
|
|
AuthTokenId: boundaryAuthTokens[0].Id,
|
|
}
|
|
atMap[ringToken{kt.KeyringType, kt.TokenName}] = boundaryAuthTokens[0]
|
|
|
|
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
|
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
|
|
|
lastKtAdded := kt
|
|
for i, at := range boundaryAuthTokens[1:] {
|
|
kr := fmt.Sprintf("%s%d", kt.KeyringType, i)
|
|
tn := fmt.Sprintf("%s%d", kt.TokenName, i)
|
|
ikt := KeyringToken{KeyringType: kr, TokenName: tn, AuthTokenId: at.Id}
|
|
|
|
atMap[ringToken{ikt.KeyringType, ikt.TokenName}] = at
|
|
assert.NoError(t, r.AddKeyringToken(ctx, addr, ikt))
|
|
lastKtAdded = ikt
|
|
}
|
|
|
|
// Lookup the first token added. It should have been deleted when the
|
|
// associated user was evicted for being used the least recently.
|
|
gotP, err := r.LookupToken(ctx, kt.AuthTokenId)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, gotP)
|
|
|
|
gotP, err = r.LookupToken(ctx, lastKtAdded.AuthTokenId)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, gotP)
|
|
}
|
|
|
|
func TestRepository_AddToken_EvictsOverLimit_Keyringless(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{
|
|
{
|
|
UserId: "user_base",
|
|
Id: "at_base",
|
|
Token: "at_base_token",
|
|
},
|
|
}
|
|
for i := 0; i < usersLimit; i++ {
|
|
iAt := &authtokens.AuthToken{
|
|
UserId: fmt.Sprintf("user%d", i),
|
|
Id: fmt.Sprintf("at_%d", i),
|
|
}
|
|
iAt.Token = fmt.Sprintf("%s_token", iAt.Id)
|
|
boundaryAuthTokens = append(boundaryAuthTokens, iAt)
|
|
}
|
|
|
|
atMap := make(map[ringToken]*authtokens.AuthToken)
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
|
|
addr := "address"
|
|
|
|
assert.NoError(t, r.AddRawToken(ctx, addr, boundaryAuthTokens[0].Token))
|
|
assert.NoError(t, r.AddRawToken(ctx, addr, boundaryAuthTokens[0].Token))
|
|
for _, at := range boundaryAuthTokens[1:] {
|
|
assert.NoError(t, r.AddRawToken(ctx, addr, at.Token))
|
|
}
|
|
// Lookup the first persona added. It should have been evicted from the db
|
|
// for being used the least recently. It is only removed from the db once
|
|
// cleanAuthTokens is called.
|
|
gotP, err := r.LookupToken(ctx, boundaryAuthTokens[0].Id)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, gotP)
|
|
_, ok := r.idToKeyringlessAuthToken.Load(boundaryAuthTokens[0].Id)
|
|
assert.True(t, ok)
|
|
|
|
gotP, err = r.LookupToken(ctx, boundaryAuthTokens[len(boundaryAuthTokens)-1].Id)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, gotP)
|
|
_, ok = r.idToKeyringlessAuthToken.Load(boundaryAuthTokens[len(boundaryAuthTokens)-1].Id)
|
|
assert.True(t, ok)
|
|
|
|
assert.NoError(t, r.syncKeyringlessTokensWithDb(ctx))
|
|
_, ok = r.idToKeyringlessAuthToken.Load(boundaryAuthTokens[0].Id)
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestRepository_CleanAuthTokens(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
at := &authtokens.AuthToken{
|
|
UserId: "user_base",
|
|
Id: "at_base",
|
|
Token: "at_base_token",
|
|
}
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at}
|
|
|
|
atMap := make(map[ringToken]*authtokens.AuthToken)
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
assert.NoError(t, r.AddRawToken(ctx, "baddr", at.Token))
|
|
_, present := r.idToKeyringlessAuthToken.Load(at.Id)
|
|
assert.True(t, present)
|
|
|
|
_, err = r.rw.Delete(ctx, &user{Id: at.UserId})
|
|
require.NoError(t, err)
|
|
|
|
_, present = r.idToKeyringlessAuthToken.Load(at.Id)
|
|
assert.True(t, present)
|
|
|
|
assert.NoError(t, r.syncKeyringlessTokensWithDb(ctx))
|
|
|
|
_, present = r.idToKeyringlessAuthToken.Load(at.Id)
|
|
assert.False(t, present)
|
|
}
|
|
|
|
func TestRepository_AddKeyringToken_AddingExistingUpdatesLastAccessedTime(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
addr := "address"
|
|
at1 := &authtokens.AuthToken{
|
|
Id: "at_1",
|
|
Token: "at_1_token",
|
|
UserId: "u_1",
|
|
}
|
|
kt1 := KeyringToken{
|
|
TokenName: "t1",
|
|
KeyringType: "k1",
|
|
AuthTokenId: at1.Id,
|
|
}
|
|
at2 := &authtokens.AuthToken{
|
|
Id: "at_2",
|
|
Token: "at_2_token",
|
|
UserId: "u_2",
|
|
}
|
|
kt2 := KeyringToken{
|
|
TokenName: "t2",
|
|
KeyringType: "k2",
|
|
AuthTokenId: "at_2",
|
|
}
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at1, at2}
|
|
atMap := map[ringToken]*authtokens.AuthToken{
|
|
{kt1.KeyringType, kt1.TokenName}: at1,
|
|
{kt2.KeyringType, kt2.TokenName}: at2,
|
|
}
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
|
|
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt1))
|
|
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt2))
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt1))
|
|
|
|
gotP1, err := r.LookupToken(ctx, kt1.AuthTokenId)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, gotP1)
|
|
gotP2, err := r.LookupToken(ctx, kt2.AuthTokenId)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, gotP2)
|
|
|
|
assert.Greater(t, gotP1.LastAccessedTime, gotP2.LastAccessedTime)
|
|
}
|
|
|
|
func TestRepository_AddRawToken_AddingExistingUpdatesLastAccessedTime(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
addr := "address"
|
|
at1 := &authtokens.AuthToken{
|
|
Id: "at_1",
|
|
Token: "at_1_token",
|
|
UserId: "u_1",
|
|
}
|
|
at2 := &authtokens.AuthToken{
|
|
Id: "at_2",
|
|
Token: "at_2_token",
|
|
UserId: "u_2",
|
|
}
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at1, at2}
|
|
atMap := map[ringToken]*authtokens.AuthToken{}
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
|
|
assert.NoError(t, r.AddRawToken(ctx, addr, at1.Token))
|
|
assert.NoError(t, r.AddRawToken(ctx, addr, at2.Token))
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
assert.NoError(t, r.AddRawToken(ctx, addr, at1.Token))
|
|
|
|
gotP1, err := r.LookupToken(ctx, at1.Id)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, gotP1)
|
|
gotP2, err := r.LookupToken(ctx, at2.Id)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, gotP2)
|
|
|
|
assert.Greater(t, gotP1.LastAccessedTime, gotP2.LastAccessedTime)
|
|
}
|
|
|
|
func TestRepository_ListTokens(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
addr := "address"
|
|
u := &user{
|
|
Id: "u1",
|
|
Address: addr,
|
|
}
|
|
at := &authtokens.AuthToken{
|
|
Id: "at_1",
|
|
Token: "at_1_token",
|
|
UserId: u.Id,
|
|
}
|
|
|
|
atMap := make(map[ringToken]*authtokens.AuthToken)
|
|
|
|
ktTokenCount := 15
|
|
for i := 0; i < ktTokenCount; i++ {
|
|
k := fmt.Sprintf("k%d", i)
|
|
t := fmt.Sprintf("t%d", i)
|
|
atMap[ringToken{k, t}] = at
|
|
}
|
|
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(maps.Values(atMap)))
|
|
require.NoError(t, err)
|
|
|
|
for k, v := range atMap {
|
|
require.NoError(t, r.AddKeyringToken(ctx, addr, KeyringToken{
|
|
KeyringType: k.k,
|
|
TokenName: k.t,
|
|
AuthTokenId: v.Id,
|
|
}))
|
|
}
|
|
|
|
t.Run("no token", func(t *testing.T) {
|
|
gotP, err := r.listTokens(ctx, &user{Id: "tokenless"})
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, gotP)
|
|
})
|
|
|
|
t.Run("many tokens", func(t *testing.T) {
|
|
gotAt, err := r.listTokens(ctx, u)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, gotAt, 1)
|
|
|
|
gotKrt, err := r.listKeyringTokens(ctx, gotAt[0])
|
|
assert.NoError(t, err)
|
|
assert.Len(t, gotKrt, ktTokenCount)
|
|
})
|
|
}
|
|
|
|
func TestRepository_DeleteKeyringToken(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
addr := "address"
|
|
at1 := &authtokens.AuthToken{
|
|
Id: "at_1",
|
|
Token: "at_1_token",
|
|
UserId: "u_1",
|
|
}
|
|
kt1 := KeyringToken{
|
|
TokenName: "t1",
|
|
KeyringType: "k1",
|
|
AuthTokenId: at1.Id,
|
|
}
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at1}
|
|
atMap := map[ringToken]*authtokens.AuthToken{
|
|
{kt1.KeyringType, kt1.TokenName}: at1,
|
|
}
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
|
|
t.Run("delete non existing", func(t *testing.T) {
|
|
assert.ErrorContains(t, r.deleteKeyringToken(ctx, KeyringToken{KeyringType: "Unknown", TokenName: "Unknown"}), "not found")
|
|
})
|
|
|
|
t.Run("delete existing", func(t *testing.T) {
|
|
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt1))
|
|
p, err := r.LookupToken(ctx, kt1.AuthTokenId)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, p)
|
|
|
|
assert.NoError(t, r.deleteKeyringToken(ctx, kt1))
|
|
|
|
got, err := r.LookupToken(ctx, kt1.AuthTokenId)
|
|
require.NoError(t, err)
|
|
require.Nil(t, got)
|
|
})
|
|
}
|
|
|
|
func TestRepository_LookupToken(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
addr := "address"
|
|
at := &authtokens.AuthToken{
|
|
Id: "at_1",
|
|
Token: "at_1_token",
|
|
UserId: "u_1",
|
|
}
|
|
kt := KeyringToken{
|
|
TokenName: "t1",
|
|
KeyringType: "k1",
|
|
AuthTokenId: at.Id,
|
|
}
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at}
|
|
atMap := map[ringToken]*authtokens.AuthToken{
|
|
{kt.KeyringType, kt.TokenName}: at,
|
|
}
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
|
|
t.Run("empty token id", func(t *testing.T) {
|
|
p, err := r.LookupToken(ctx, "")
|
|
assert.ErrorContains(t, err, "auth token id is empty")
|
|
assert.Nil(t, p)
|
|
})
|
|
t.Run("not found", func(t *testing.T) {
|
|
p, err := r.LookupToken(ctx, "token")
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, p)
|
|
})
|
|
t.Run("found", func(t *testing.T) {
|
|
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
|
p, err := r.LookupToken(ctx, kt.AuthTokenId)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, p)
|
|
})
|
|
|
|
t.Run("withUpdateLastAccessedTime", func(t *testing.T) {
|
|
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
|
time.Sleep(1 * time.Millisecond)
|
|
|
|
beforeP, err := r.LookupToken(ctx, kt.AuthTokenId, WithUpdateLastAccessedTime(true))
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, beforeP)
|
|
|
|
afterP, err := r.LookupToken(ctx, kt.AuthTokenId)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, afterP)
|
|
assert.Greater(t, afterP.LastAccessedTime, beforeP.LastAccessedTime)
|
|
})
|
|
}
|
|
|
|
func TestRepository_lookupUpUser(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
addr := "address"
|
|
at := &authtokens.AuthToken{
|
|
Id: "at_1",
|
|
Token: "at_1_token",
|
|
UserId: "u_1",
|
|
}
|
|
kt := KeyringToken{
|
|
TokenName: "t1",
|
|
KeyringType: "k1",
|
|
AuthTokenId: at.Id,
|
|
}
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at}
|
|
atMap := map[ringToken]*authtokens.AuthToken{
|
|
{kt.KeyringType, kt.TokenName}: at,
|
|
}
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
assert.NoError(t, r.AddKeyringToken(ctx, addr, kt))
|
|
|
|
t.Run("empty user id", func(t *testing.T) {
|
|
u, err := r.lookupUser(ctx, "")
|
|
assert.ErrorContains(t, err, "empty id")
|
|
assert.Nil(t, u)
|
|
})
|
|
t.Run("not found user id", func(t *testing.T) {
|
|
u, err := r.lookupUser(ctx, "notfound")
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, u)
|
|
})
|
|
t.Run("found", func(t *testing.T) {
|
|
u, err := r.lookupUser(ctx, at.UserId)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, &user{Id: at.UserId, Address: addr}, u)
|
|
})
|
|
t.Run("soft-deleted", func(t *testing.T) {
|
|
at2 := &authtokens.AuthToken{
|
|
Id: "at_2",
|
|
Token: "at_2_token",
|
|
UserId: "u_2",
|
|
ExpirationTime: time.Now().Add(1 * time.Minute), // not expired is required for this test
|
|
}
|
|
kt2 := KeyringToken{
|
|
TokenName: "t2",
|
|
KeyringType: "k2",
|
|
AuthTokenId: at2.Id,
|
|
}
|
|
addr2 := "address2"
|
|
boundaryAuthTokens2 := []*authtokens.AuthToken{at2}
|
|
atMap2 := map[ringToken]*authtokens.AuthToken{
|
|
{kt2.KeyringType, kt2.TokenName}: at2,
|
|
}
|
|
m := &sync.Map{}
|
|
r2, err := NewRepository(ctx, s, m, mapBasedAuthTokenKeyringLookup(atMap2), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens2))
|
|
require.NoError(t, err)
|
|
assert.NoError(t, r2.AddKeyringToken(ctx, addr2, kt2))
|
|
|
|
rs, err := NewRefreshService(ctx, r2, hclog.NewNullLogger(), 0, 0)
|
|
require.NoError(t, err)
|
|
|
|
retTargets := []*targets.Target{
|
|
target("1"),
|
|
target("2"),
|
|
target("3"),
|
|
target("4"),
|
|
}
|
|
opts := []Option{
|
|
WithAliasRetrievalFunc(testResolvableAliasStaticResourceRetrievalFunc(testStaticResourceRetrievalFuncForId[*aliases.Alias](t, nil, nil))),
|
|
WithSessionRetrievalFunc(testSessionStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc[*sessions.Session](t, nil, nil))),
|
|
WithTargetRetrievalFunc(testTargetStaticResourceRetrievalFunc(testStaticResourceRetrievalFunc[*targets.Target](t,
|
|
[][]*targets.Target{
|
|
retTargets[:3],
|
|
retTargets[3:],
|
|
},
|
|
[][]string{
|
|
nil,
|
|
{retTargets[0].Id, retTargets[1].Id},
|
|
},
|
|
))),
|
|
}
|
|
assert.NoError(t, rs.RefreshForSearch(ctx, at2.Id, Targets, opts...))
|
|
// Now load up a few resources and a token, and trying again should
|
|
// see the RefreshForSearch update more fields.
|
|
assert.NoError(t, rs.Refresh(ctx, opts...))
|
|
cachedTargets, err := r.ListTargets(ctx, at2.Id)
|
|
assert.NoError(t, err)
|
|
assert.ElementsMatch(t, retTargets[:3], cachedTargets.Targets)
|
|
|
|
// should be found in cache (user_active)
|
|
u2, err := r2.lookupUser(ctx, at2.UserId)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, &user{Id: at2.UserId, Address: addr2}, u2)
|
|
u2, err = r2.lookupUser(ctx, at2.UserId)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, &user{Id: at2.UserId, Address: addr2}, u2)
|
|
|
|
// should be found in underlying user table as well
|
|
tu, err := testLookupUser(t, s, at2.UserId)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, &testUser{Id: at2.UserId, Address: addr2, DeletedAt: infinityValue}, tu)
|
|
|
|
// there better be some refresh tokens
|
|
tks, err := r2.listRefreshTokens(ctx, u2)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, tks)
|
|
|
|
// now delete the user's auth_token and be sure the user is still found
|
|
// in the cache (table == "user" and not in "user_active")
|
|
err = r2.deleteKeyringToken(ctx, kt2)
|
|
require.NoError(t, err)
|
|
|
|
currentTks, err := r2.listTokens(ctx, u2)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, currentTks)
|
|
|
|
// should no longer be an active user
|
|
u2, err = r2.lookupUser(ctx, tu.Id)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, u2)
|
|
|
|
// should still be found in underlying user table
|
|
tu, err = testLookupUser(t, s, tu.Id)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, &testUser{Id: tu.Id, Address: tu.Address, DeletedAt: tu.DeletedAt}, tu)
|
|
})
|
|
t.Run("hard-deleted", func(t *testing.T) {
|
|
at3 := &authtokens.AuthToken{
|
|
Id: "at_3",
|
|
Token: "at_3_token",
|
|
UserId: "u_3",
|
|
ExpirationTime: time.Now().Add(1 * time.Minute), // not expired is required for this test
|
|
}
|
|
kt3 := KeyringToken{
|
|
TokenName: "t3",
|
|
KeyringType: "k3",
|
|
AuthTokenId: at3.Id,
|
|
}
|
|
addr3 := "address3"
|
|
boundaryAuthTokens3 := []*authtokens.AuthToken{at3}
|
|
atMap3 := map[ringToken]*authtokens.AuthToken{
|
|
{kt3.KeyringType, kt3.TokenName}: at3,
|
|
}
|
|
m := &sync.Map{}
|
|
r3, err := NewRepository(ctx, s, m, mapBasedAuthTokenKeyringLookup(atMap3), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens3))
|
|
require.NoError(t, err)
|
|
assert.NoError(t, r3.AddKeyringToken(ctx, addr3, kt3))
|
|
|
|
// should be found in cache (user_active)
|
|
u3, err := r3.lookupUser(ctx, at3.UserId)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, &user{Id: at3.UserId, Address: addr3}, u3)
|
|
u3, err = r3.lookupUser(ctx, at3.UserId)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, &user{Id: at3.UserId, Address: addr3}, u3)
|
|
|
|
// should be found in underlying user table as well
|
|
tu, err := testLookupUser(t, s, at3.UserId)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, &testUser{Id: at3.UserId, Address: addr3, DeletedAt: infinityValue}, tu)
|
|
|
|
// there better be some refresh tokens
|
|
tks, err := r3.listRefreshTokens(ctx, u3)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, tks)
|
|
|
|
// now delete the user's auth_token and be sure the user is not found
|
|
// in the cache (not in either the "user" or "user_active" tables)
|
|
err = r3.deleteKeyringToken(ctx, kt3)
|
|
require.NoError(t, err)
|
|
|
|
currentTks, err := r3.listTokens(ctx, u3)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, currentTks)
|
|
|
|
// should no longer be an active user
|
|
u3, err = r3.lookupUser(ctx, tu.Id)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, u3)
|
|
|
|
// should not be found in underlying user table
|
|
_, err = testLookupUser(t, s, tu.Id)
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, dbw.ErrRecordNotFound)
|
|
})
|
|
}
|
|
|
|
// infinityValue represents a time.Time that is infinity
|
|
var infinityValue = infinityDate{
|
|
Time: time.Time{},
|
|
IsInfinity: true,
|
|
}
|
|
|
|
// infinityDate is used to represent a time.Time that can be infinity, neg
|
|
// infinity or a regular time.Time
|
|
type infinityDate struct {
|
|
Time time.Time
|
|
IsInfinity bool
|
|
IsNegInfinity bool
|
|
}
|
|
|
|
// sqliteDatetimeLayout defines the format for sqlite datetime ('YYYY-MM-DD HH:MM:SS.SSS')
|
|
const sqliteDatetimeLayout = "2006-01-02 15:04:05.999"
|
|
|
|
// Scan implements the sql.Scanner interface for infinityDate
|
|
func (d *infinityDate) Scan(value any) error {
|
|
switch v := value.(type) {
|
|
case string:
|
|
if v == "infinity" {
|
|
d.IsInfinity = true
|
|
d.IsNegInfinity = false
|
|
return nil
|
|
} else if v == "-infinity" {
|
|
d.IsNegInfinity = true
|
|
d.IsInfinity = false
|
|
return nil
|
|
} else {
|
|
parsedTime, err := time.Parse(sqliteDatetimeLayout, v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.Time = parsedTime
|
|
d.IsInfinity = false
|
|
d.IsNegInfinity = false
|
|
return nil
|
|
}
|
|
case time.Time:
|
|
d.Time = v
|
|
d.IsInfinity = false
|
|
d.IsNegInfinity = false
|
|
return nil
|
|
}
|
|
return stderrors.New("unsupported data type for Date")
|
|
}
|
|
|
|
// Value implements the driver.Valuer interface for infinityDate
|
|
func (d infinityDate) Value() (driver.Value, error) {
|
|
if d.IsInfinity {
|
|
return "infinity", nil
|
|
} else if d.IsNegInfinity {
|
|
return "-infinity", nil
|
|
}
|
|
return d.Time.Format(sqliteDatetimeLayout), nil
|
|
}
|
|
|
|
// testUser is used by testLookupUser to lookup a user from the database and
|
|
// supports returning the user's DeletedAt time (soft delete).
|
|
type testUser struct {
|
|
Id string
|
|
Address string
|
|
DeletedAt infinityDate
|
|
}
|
|
|
|
// testLookupUser is a helper function to lookup a user from the database in the
|
|
// underlying user table.
|
|
func testLookupUser(t *testing.T, conn any, id string) (*testUser, error) {
|
|
t.Helper()
|
|
var rw db.Reader
|
|
switch v := conn.(type) {
|
|
case *db.DB:
|
|
rw = db.New(v)
|
|
case db.Reader:
|
|
rw = v
|
|
}
|
|
u := &testUser{
|
|
Id: id,
|
|
}
|
|
err := rw.LookupById(context.Background(), u, db.WithTable("user"))
|
|
switch {
|
|
case err == nil:
|
|
return u, nil
|
|
default:
|
|
return &testUser{}, err
|
|
}
|
|
}
|
|
|
|
func TestRepository_RemoveStaleTokens(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
|
|
u := &user{
|
|
Id: "user",
|
|
Address: "address",
|
|
}
|
|
at1 := &authtokens.AuthToken{
|
|
Id: "at_1",
|
|
Token: "at_1_token",
|
|
UserId: u.Id,
|
|
ExpirationTime: time.Now().Add(1 * time.Minute),
|
|
}
|
|
kt1 := KeyringToken{
|
|
TokenName: "t1",
|
|
KeyringType: "k1",
|
|
AuthTokenId: at1.Id,
|
|
}
|
|
at2 := &authtokens.AuthToken{
|
|
Id: "at_2",
|
|
Token: "at_2_token",
|
|
UserId: u.Id,
|
|
ExpirationTime: time.Now().Add(1 * time.Minute),
|
|
}
|
|
kt2 := KeyringToken{
|
|
TokenName: "t2",
|
|
KeyringType: "k2",
|
|
AuthTokenId: "at_2",
|
|
}
|
|
// this auth token expired a minute ago
|
|
at3 := &authtokens.AuthToken{
|
|
Id: "at_3",
|
|
Token: "at_3_token",
|
|
UserId: u.Id,
|
|
ExpirationTime: time.Now().Add(-(1 * time.Minute)),
|
|
}
|
|
kt3 := KeyringToken{
|
|
TokenName: "t3",
|
|
KeyringType: "k3",
|
|
AuthTokenId: "at_3",
|
|
}
|
|
|
|
boundaryAuthTokens := []*authtokens.AuthToken{at1, at2, at3}
|
|
atMap := map[ringToken]*authtokens.AuthToken{
|
|
{kt1.KeyringType, kt1.TokenName}: at1,
|
|
{kt2.KeyringType, kt2.TokenName}: at2,
|
|
{kt3.KeyringType, kt3.TokenName}: at3,
|
|
}
|
|
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
assert.NoError(t, r.AddKeyringToken(ctx, u.Address, kt1))
|
|
assert.NoError(t, r.AddKeyringToken(ctx, u.Address, kt2))
|
|
assert.NoError(t, r.AddKeyringToken(ctx, u.Address, kt3))
|
|
|
|
staleTime := time.Now().Add(-(tokenStalenessLimit + 1*time.Hour))
|
|
freshTime := time.Now().Add(-(tokenStalenessLimit - 1*time.Hour))
|
|
|
|
freshAt := &AuthToken{
|
|
Id: at1.Id,
|
|
LastAccessedTime: freshTime,
|
|
}
|
|
_, err = r.rw.Update(ctx, freshAt, []string{"LastAccessedTime"}, nil)
|
|
require.NoError(t, err)
|
|
anotherFreshAt := &AuthToken{
|
|
Id: at3.Id,
|
|
LastAccessedTime: freshTime,
|
|
}
|
|
_, err = r.rw.Update(ctx, anotherFreshAt, []string{"LastAccessedTime"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
staleAt := &AuthToken{
|
|
Id: at2.Id,
|
|
LastAccessedTime: staleTime,
|
|
}
|
|
_, err = r.rw.Update(ctx, staleAt, []string{"LastAccessedTime"}, nil)
|
|
require.NoError(t, err)
|
|
|
|
lAt, err := r.listTokens(ctx, u)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, lAt, 3)
|
|
|
|
assert.NoError(t, r.cleanExpiredOrOrphanedAuthTokens(ctx))
|
|
|
|
lAt, err = r.listTokens(ctx, u)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, lAt, 1)
|
|
assert.Equal(t, lAt[0].Id, at1.Id)
|
|
}
|
|
|
|
func TestCleanExpiredOrOrphanedAuthTokens_Errors(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
err := cleanExpiredOrOrphanedAuthTokens(ctx, nil, &sync.Map{})
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "writer is nil")
|
|
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
rw := db.New(s)
|
|
|
|
err = cleanExpiredOrOrphanedAuthTokens(ctx, rw, &sync.Map{})
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "writer isn't part of an inflight transaction")
|
|
|
|
_, err = rw.DoTx(ctx, 1, db.ExpBackoff{}, func(_ db.Reader, writer db.Writer) error {
|
|
err := cleanExpiredOrOrphanedAuthTokens(ctx, writer, nil)
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "keyringless auth token map is nil")
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestUpsertUserAndAuthToken(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
s, err := cachedb.Open(ctx)
|
|
require.NoError(t, err)
|
|
rw := db.New(s)
|
|
|
|
defaultAt := &authtokens.AuthToken{
|
|
Id: "at_123",
|
|
Token: "at_123_token",
|
|
UserId: "u_123",
|
|
}
|
|
|
|
_, err = rw.DoTx(ctx, 1, db.ExpBackoff{}, func(txReader db.Reader, txWriter db.Writer) error {
|
|
errorCases := []struct {
|
|
name string
|
|
reader db.Reader
|
|
writer db.Writer
|
|
addr string
|
|
at *authtokens.AuthToken
|
|
errorContains string
|
|
}{
|
|
{
|
|
name: "nil reader",
|
|
reader: nil,
|
|
writer: txWriter,
|
|
addr: "address",
|
|
at: defaultAt,
|
|
errorContains: "reader is nil",
|
|
},
|
|
{
|
|
name: "nil writer",
|
|
reader: txReader,
|
|
writer: nil,
|
|
addr: "address",
|
|
at: defaultAt,
|
|
errorContains: "writer is nil",
|
|
},
|
|
{
|
|
name: "writer not in tx",
|
|
reader: txReader,
|
|
writer: rw,
|
|
addr: "address",
|
|
at: defaultAt,
|
|
errorContains: "writer isn't part of an inflight transaction",
|
|
},
|
|
{
|
|
name: "empty address",
|
|
reader: txReader,
|
|
writer: txWriter,
|
|
addr: "",
|
|
at: defaultAt,
|
|
errorContains: "boundary address is empty",
|
|
},
|
|
{
|
|
name: "auth token is nil",
|
|
reader: txReader,
|
|
writer: txWriter,
|
|
addr: "address",
|
|
at: nil,
|
|
errorContains: "auth token is nil",
|
|
},
|
|
{
|
|
name: "auth token missing id",
|
|
reader: txReader,
|
|
writer: txWriter,
|
|
addr: "address",
|
|
at: &authtokens.AuthToken{
|
|
Token: "at_123_token",
|
|
UserId: "u_123",
|
|
},
|
|
errorContains: "auth token id is empty",
|
|
},
|
|
{
|
|
name: "auth token missing user id",
|
|
reader: txReader,
|
|
writer: txWriter,
|
|
addr: "address",
|
|
at: &authtokens.AuthToken{
|
|
Id: "at_123",
|
|
Token: "at_123_token",
|
|
},
|
|
errorContains: "auth token user id is empty",
|
|
},
|
|
}
|
|
|
|
for _, tc := range errorCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := upsertUserAndAuthToken(ctx, tc.reader, tc.writer, tc.addr, tc.at)
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, tc.errorContains)
|
|
})
|
|
}
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
t.Run("hard-and-soft-delete-oldest-user", func(t *testing.T) {
|
|
boundaryAuthTokens := make([]*authtokens.AuthToken, 0, usersLimit)
|
|
atMap := map[ringToken]*authtokens.AuthToken{}
|
|
m := &sync.Map{}
|
|
|
|
// create usersLimit users to simulate the case where the user limit is
|
|
// reached. The Tx is required because upsertUserAndAuthToken requires
|
|
// an inflight transaction.
|
|
_, err = rw.DoTx(ctx, 1, db.ExpBackoff{}, func(txReader db.Reader, txWriter db.Writer) error {
|
|
for i := 1; i <= usersLimit; i++ {
|
|
u := &user{
|
|
Id: fmt.Sprintf("u_%d", i),
|
|
Address: fmt.Sprintf("address_%d", i),
|
|
}
|
|
at := &authtokens.AuthToken{
|
|
Id: fmt.Sprintf("at_%d", i),
|
|
Token: fmt.Sprintf("at_%d_token", i),
|
|
UserId: u.Id,
|
|
}
|
|
boundaryAuthTokens = append(boundaryAuthTokens, at)
|
|
atMap[ringToken{fmt.Sprintf("k_%d", i), fmt.Sprintf("t_%d", i)}] = at
|
|
err := upsertUserAndAuthToken(ctx, txReader, txWriter, u.Address, at)
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
return nil
|
|
})
|
|
// verify that all the initial users were added
|
|
repo, err := NewRepository(ctx, s, m, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
|
require.NoError(t, err)
|
|
for i := 1; i <= usersLimit; i++ {
|
|
userId := fmt.Sprintf("u_%d", i)
|
|
foundUser, err := repo.lookupUser(ctx, userId)
|
|
require.NoError(t, err)
|
|
_, err = testLookupUser(t, s, foundUser.Id)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
{
|
|
// setup is done. Let's add a new user and verify that the oldest
|
|
// user is hard deleted
|
|
_, err = rw.DoTx(ctx, 1, db.ExpBackoff{}, func(txReader db.Reader, txWriter db.Writer) error {
|
|
// add a new user, which should trigger the hard deletion of the oldest user
|
|
newUser := &user{
|
|
Id: "u_new",
|
|
Address: "address_new",
|
|
}
|
|
newUserAt := &authtokens.AuthToken{
|
|
Id: "at_new",
|
|
Token: "at_new_token",
|
|
UserId: newUser.Id,
|
|
}
|
|
err := upsertUserAndAuthToken(ctx, txReader, txWriter, newUser.Address, newUserAt)
|
|
require.NoError(t, err)
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// verify that the oldest user was hard deleted
|
|
foundUser, err := repo.lookupUser(ctx, "u_1")
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, foundUser)
|
|
foundTestUser, err := testLookupUser(t, s, "u_1")
|
|
assert.Error(t, err)
|
|
assert.Equal(t, &testUser{}, foundTestUser)
|
|
}
|
|
{
|
|
// Let's add a refresh token for the oldest user and then new user
|
|
// and verify that the oldest user is soft deleted
|
|
rt := &refreshToken{
|
|
UserId: "u_2",
|
|
ResourceType: "target",
|
|
RefreshToken: "rt_2",
|
|
CreateTime: time.Now().Add(-24 * time.Hour),
|
|
UpdateTime: time.Now().Add(-24 * time.Hour),
|
|
}
|
|
err = repo.rw.Create(ctx, rt)
|
|
require.NoError(t, err)
|
|
|
|
_, err = rw.DoTx(ctx, 1, db.ExpBackoff{}, func(txReader db.Reader, txWriter db.Writer) error {
|
|
// add a new user, which should trigger the soft deletion of the oldest user
|
|
newUser := &user{
|
|
Id: "u_new_2",
|
|
Address: "address_new_2",
|
|
}
|
|
newUserAt := &authtokens.AuthToken{
|
|
Id: "at_new_2",
|
|
Token: "at_new_token_2",
|
|
UserId: newUser.Id,
|
|
}
|
|
err := upsertUserAndAuthToken(ctx, txReader, txWriter, newUser.Address, newUserAt)
|
|
require.NoError(t, err)
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// verify that the oldest user was soft deleted
|
|
foundUser, err := repo.lookupUser(ctx, "u_2")
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, foundUser)
|
|
// should not find the user in the underlying user table
|
|
foundTestUser, err := testLookupUser(t, s, "u_2")
|
|
assert.NoError(t, err)
|
|
assert.NotEqual(t, &testUser{}, foundTestUser)
|
|
}
|
|
})
|
|
}
|