Files
threshold/internal/listtoken/list_token_test.go
T
Johan Brandhorst-Satzkorn a237157b91 internal/listtoken: remove validation depending on application time
All times in the list tokens are sourced from the database,
and any comparisons to times in the application can
cause false positive validation errors. We've observed
this from multiple users of the pagination logic, and
the value of this validation is very little compared
to the cost of a single false positive error like this
happening to a real world customer. Therefore, remove
any list token validation using the application time,
except the one monitoring for token expiry, which
should not suffer from the issue to a degree
that matters to our users.

Also refactor validation such that the list token subtype
constructors only perform sanity checks on the values
while punting business logic validation to the validate
method on the new token. This means we avoid
duplicating logic.
2024-01-05 07:43:21 -08:00

1065 lines
34 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package listtoken_test
import (
"context"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/internal/boundary"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_NewPaginationToken(t *testing.T) {
t.Parallel()
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
tests := []struct {
name string
createdTime time.Time
typ resource.Type
grantsHash []byte
lastItemId string
lastItemCreateTime time.Time
want *listtoken.Token
wantErrString string
wantErrCode errors.Code
}{
{
name: "valid list+pagination token",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
lastItemId: "some id",
lastItemCreateTime: fiveDaysAgo,
want: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.PaginationToken{
LastItemId: "some id",
LastItemCreateTime: fiveDaysAgo,
},
},
},
{
name: "missing create time",
createdTime: time.Time{},
typ: resource.Target,
grantsHash: []byte("some hash"),
lastItemId: "some id",
lastItemCreateTime: fiveDaysAgo,
wantErrString: "missing create time",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing resource type",
createdTime: fiveDaysAgo,
typ: resource.Unknown,
grantsHash: []byte("some hash"),
lastItemId: "some id",
lastItemCreateTime: fiveDaysAgo,
wantErrString: "missing resource type",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing grants hash",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: nil,
lastItemId: "some id",
lastItemCreateTime: fiveDaysAgo,
wantErrString: "missing grants hash",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing last item id",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
lastItemId: "",
lastItemCreateTime: fiveDaysAgo,
wantErrString: "missing last item ID",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing last item create time",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
lastItemId: "some id",
lastItemCreateTime: time.Time{},
wantErrString: "missing last item create time",
wantErrCode: errors.InvalidParameter,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := listtoken.NewPagination(context.Background(), tt.createdTime, tt.typ, tt.grantsHash, tt.lastItemId, tt.lastItemCreateTime)
if tt.wantErrString != "" {
require.ErrorContains(t, err, tt.wantErrString)
require.Equal(t, errors.Convert(err).Code, tt.wantErrCode)
return
}
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, tt.want))
})
}
}
func Test_NewStartRefreshToken(t *testing.T) {
t.Parallel()
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
tests := []struct {
name string
createdTime time.Time
typ resource.Type
grantsHash []byte
previousDeletedIdsTime time.Time
previousPhaseUpperBound time.Time
want *listtoken.Token
wantErrString string
wantErrCode errors.Code
}{
{
name: "valid list+start-refresh token",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
previousDeletedIdsTime: fiveDaysAgo,
previousPhaseUpperBound: fiveDaysAgo,
want: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.StartRefreshToken{
PreviousPhaseUpperBound: fiveDaysAgo,
PreviousDeletedIdsTime: fiveDaysAgo,
},
},
},
{
name: "missing create time",
createdTime: time.Time{},
typ: resource.Target,
grantsHash: []byte("some hash"),
previousDeletedIdsTime: fiveDaysAgo,
previousPhaseUpperBound: fiveDaysAgo,
wantErrString: "missing create time",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing resource type",
createdTime: fiveDaysAgo,
typ: resource.Unknown,
grantsHash: []byte("some hash"),
previousDeletedIdsTime: fiveDaysAgo,
previousPhaseUpperBound: fiveDaysAgo,
wantErrString: "missing resource type",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing grants hash",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: nil,
previousDeletedIdsTime: fiveDaysAgo,
previousPhaseUpperBound: fiveDaysAgo,
wantErrString: "missing grants hash",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing previous deleted ids time",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
previousDeletedIdsTime: time.Time{},
previousPhaseUpperBound: fiveDaysAgo,
wantErrString: "missing previous deleted ids time",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing previous phase upper bound",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
previousDeletedIdsTime: fiveDaysAgo,
previousPhaseUpperBound: time.Time{},
wantErrString: "missing previous phase upper bound",
wantErrCode: errors.InvalidParameter,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := listtoken.NewStartRefresh(context.Background(), tt.createdTime, tt.typ, tt.grantsHash, tt.previousDeletedIdsTime, tt.previousPhaseUpperBound)
if tt.wantErrString != "" {
require.ErrorContains(t, err, tt.wantErrString)
require.Equal(t, errors.Convert(err).Code, tt.wantErrCode)
return
}
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, tt.want))
})
}
}
func Test_NewRefreshToken(t *testing.T) {
t.Parallel()
timeNow := time.Now()
fiveDaysAgo := timeNow.AddDate(0, 0, -5)
fourDaysAgo := timeNow.AddDate(0, 0, -4)
tests := []struct {
name string
createdTime time.Time
typ resource.Type
grantsHash []byte
phaseUpperBound time.Time
phaseLowerBound time.Time
previousDeleteIdsTime time.Time
lastItemId string
lastItemUpdateTime time.Time
want *listtoken.Token
wantErrString string
wantErrCode errors.Code
}{
{
name: "valid list+refresh token",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
phaseUpperBound: timeNow,
phaseLowerBound: fiveDaysAgo,
previousDeleteIdsTime: fiveDaysAgo,
lastItemId: "some id",
lastItemUpdateTime: fourDaysAgo,
want: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.RefreshToken{
PhaseUpperBound: timeNow,
PhaseLowerBound: fiveDaysAgo,
PreviousDeletedIdsTime: fiveDaysAgo,
LastItemId: "some id",
LastItemUpdateTime: fourDaysAgo,
},
},
},
{
name: "missing create time",
createdTime: time.Time{},
typ: resource.Target,
grantsHash: []byte("some hash"),
phaseUpperBound: timeNow,
phaseLowerBound: fiveDaysAgo,
previousDeleteIdsTime: fiveDaysAgo,
lastItemId: "some id",
lastItemUpdateTime: fourDaysAgo,
wantErrString: "missing create time",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing resource type",
createdTime: fiveDaysAgo,
typ: resource.Unknown,
grantsHash: []byte("some hash"),
phaseUpperBound: timeNow,
phaseLowerBound: fiveDaysAgo,
previousDeleteIdsTime: fiveDaysAgo,
lastItemId: "some id",
lastItemUpdateTime: fourDaysAgo,
wantErrString: "missing resource type",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing grants hash",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: nil,
phaseUpperBound: timeNow,
phaseLowerBound: fiveDaysAgo,
previousDeleteIdsTime: fiveDaysAgo,
lastItemId: "some id",
lastItemUpdateTime: fourDaysAgo,
wantErrString: "missing grants hash",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing previous deleted ids time",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
phaseUpperBound: timeNow,
phaseLowerBound: fiveDaysAgo,
previousDeleteIdsTime: time.Time{},
lastItemId: "some id",
lastItemUpdateTime: fourDaysAgo,
wantErrString: "missing previous deleted ids time",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing phase upper bound",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
phaseUpperBound: time.Time{},
phaseLowerBound: fiveDaysAgo,
previousDeleteIdsTime: fiveDaysAgo,
lastItemId: "some id",
lastItemUpdateTime: fourDaysAgo,
wantErrString: "missing phase upper bound",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing phase lower bound",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
phaseUpperBound: timeNow,
phaseLowerBound: time.Time{},
previousDeleteIdsTime: fiveDaysAgo,
lastItemId: "some id",
lastItemUpdateTime: fourDaysAgo,
wantErrString: "missing phase lower bound",
wantErrCode: errors.InvalidParameter,
},
{
name: "missing last item id",
createdTime: fiveDaysAgo,
typ: resource.Target,
grantsHash: []byte("some hash"),
phaseUpperBound: timeNow,
phaseLowerBound: fiveDaysAgo,
previousDeleteIdsTime: fiveDaysAgo,
lastItemId: "",
lastItemUpdateTime: fourDaysAgo,
wantErrString: "missing last item ID",
wantErrCode: errors.InvalidParameter,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := listtoken.NewRefresh(
context.Background(),
tt.createdTime,
tt.typ,
tt.grantsHash,
tt.previousDeleteIdsTime,
tt.phaseUpperBound,
tt.phaseLowerBound,
tt.lastItemId,
tt.lastItemUpdateTime,
)
if tt.wantErrString != "" {
require.ErrorContains(t, err, tt.wantErrString)
require.Equal(t, errors.Convert(err).Code, tt.wantErrCode)
return
}
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, tt.want))
})
}
}
func Test_ValidateListToken(t *testing.T) {
t.Parallel()
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
tests := []struct {
name string
token *listtoken.Token
grantsHash []byte
resourceType resource.Type
wantErrString string
wantErrCode errors.Code
}{
{
name: "valid token",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
},
{
name: "nil token",
token: nil,
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token was missing",
wantErrCode: errors.InvalidListToken,
},
{
name: "no grants hash",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: nil,
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token was missing its grants hash",
wantErrCode: errors.InvalidListToken,
},
{
name: "changed grants hash",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
},
grantsHash: []byte("some other hash"),
resourceType: resource.Target,
wantErrString: "grants have changed since list token was issued",
wantErrCode: errors.InvalidListToken,
},
{
name: "expired",
token: &listtoken.Token{
CreateTime: time.Now().AddDate(0, 0, -31),
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token was expired",
wantErrCode: errors.InvalidListToken,
},
{
name: "resource type mismatch",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
},
grantsHash: []byte("some hash"),
resourceType: resource.SessionRecording,
wantErrString: "list token resource type does not match expected resource type",
wantErrCode: errors.InvalidListToken,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.token.Validate(context.Background(), tt.resourceType, tt.grantsHash)
if tt.wantErrString != "" {
require.ErrorContains(t, err, tt.wantErrString)
require.Equal(t, errors.Convert(err).Code, tt.wantErrCode)
return
}
require.NoError(t, err)
})
}
}
func Test_ValidatePaginationToken(t *testing.T) {
t.Parallel()
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
tests := []struct {
name string
token *listtoken.Token
grantsHash []byte
resourceType resource.Type
wantErrString string
wantErrCode errors.Code
}{
{
name: "valid token",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.PaginationToken{
LastItemId: "s_1234567890",
LastItemCreateTime: fiveDaysAgo,
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
},
{
name: "last item ID unset",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.PaginationToken{
LastItemId: "",
LastItemCreateTime: fiveDaysAgo,
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list tokens's pagination component missing last item ID",
wantErrCode: errors.InvalidListToken,
},
{
name: "updated in the future",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.PaginationToken{
LastItemId: "s_1234567890",
LastItemCreateTime: time.Now().AddDate(1, 0, 0),
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token's pagination component's last item was created after the token",
wantErrCode: errors.InvalidListToken,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.token.Validate(context.Background(), tt.resourceType, tt.grantsHash)
if tt.wantErrString != "" {
require.ErrorContains(t, err, tt.wantErrString)
require.Equal(t, errors.Convert(err).Code, tt.wantErrCode)
return
}
require.NoError(t, err)
})
}
}
func Test_ValidateStartRefreshToken(t *testing.T) {
t.Parallel()
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
tests := []struct {
name string
token *listtoken.Token
grantsHash []byte
resourceType resource.Type
wantErrString string
wantErrCode errors.Code
}{
{
name: "valid token",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.StartRefreshToken{
PreviousDeletedIdsTime: fiveDaysAgo,
PreviousPhaseUpperBound: fiveDaysAgo,
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
},
{
name: "previous phase upper bound before create time",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.StartRefreshToken{
PreviousDeletedIdsTime: fiveDaysAgo,
PreviousPhaseUpperBound: fiveDaysAgo.AddDate(-1, 0, 0),
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token's start refresh component's previous phase upper bound was before its creation time",
wantErrCode: errors.InvalidListToken,
},
{
name: "previous deleted ids time before create time",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.StartRefreshToken{
PreviousDeletedIdsTime: fiveDaysAgo.AddDate(-1, 0, 0),
PreviousPhaseUpperBound: fiveDaysAgo,
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token's start refresh component previous deleted ids time was before its creation time",
wantErrCode: errors.InvalidListToken,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.token.Validate(context.Background(), tt.resourceType, tt.grantsHash)
if tt.wantErrString != "" {
require.ErrorContains(t, err, tt.wantErrString)
require.Equal(t, errors.Convert(err).Code, tt.wantErrCode)
return
}
require.NoError(t, err)
})
}
}
func Test_ValidateRefreshToken(t *testing.T) {
t.Parallel()
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
tests := []struct {
name string
token *listtoken.Token
grantsHash []byte
resourceType resource.Type
wantErrString string
wantErrCode errors.Code
}{
{
name: "valid token",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.RefreshToken{
PhaseUpperBound: fiveDaysAgo,
PreviousDeletedIdsTime: fiveDaysAgo,
PhaseLowerBound: fiveDaysAgo,
LastItemId: "some id",
LastItemUpdateTime: fiveDaysAgo,
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
},
{
name: "phase upper bound before create time",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.RefreshToken{
PhaseUpperBound: fiveDaysAgo.AddDate(-1, 0, 0),
PreviousDeletedIdsTime: fiveDaysAgo,
PhaseLowerBound: fiveDaysAgo,
LastItemId: "some id",
LastItemUpdateTime: fiveDaysAgo,
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token's refresh component's phase upper bound was before its creation time",
wantErrCode: errors.InvalidListToken,
},
{
name: "phase upper bound before phase lower bound",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.RefreshToken{
PhaseUpperBound: fiveDaysAgo.AddDate(0, 0, 1),
PreviousDeletedIdsTime: fiveDaysAgo,
PhaseLowerBound: fiveDaysAgo.AddDate(0, 0, 2),
LastItemId: "some id",
LastItemUpdateTime: fiveDaysAgo,
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token's refresh component's phase upper bound was before the phase lower bound",
wantErrCode: errors.InvalidListToken,
},
{
name: "phase lower bound before create time",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.RefreshToken{
PhaseUpperBound: fiveDaysAgo,
PreviousDeletedIdsTime: fiveDaysAgo,
PhaseLowerBound: fiveDaysAgo.AddDate(-1, 0, 0),
LastItemId: "some id",
LastItemUpdateTime: fiveDaysAgo,
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token's refresh component's phase lower bound was before its creation time",
wantErrCode: errors.InvalidListToken,
},
{
name: "previous deleted ids time before create time",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.RefreshToken{
PhaseUpperBound: fiveDaysAgo,
PreviousDeletedIdsTime: fiveDaysAgo.AddDate(-1, 0, 0),
PhaseLowerBound: fiveDaysAgo,
LastItemId: "some id",
LastItemUpdateTime: fiveDaysAgo,
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token's refresh component previous deleted ids time was before its creation time",
wantErrCode: errors.InvalidListToken,
},
{
name: "emtpy last item id",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.RefreshToken{
PhaseUpperBound: fiveDaysAgo,
PreviousDeletedIdsTime: fiveDaysAgo,
PhaseLowerBound: fiveDaysAgo,
LastItemId: "",
LastItemUpdateTime: fiveDaysAgo,
},
},
grantsHash: []byte("some hash"),
resourceType: resource.Target,
wantErrString: "list token's refresh component missing last item ID",
wantErrCode: errors.InvalidListToken,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.token.Validate(context.Background(), tt.resourceType, tt.grantsHash)
if tt.wantErrString != "" {
require.ErrorContains(t, err, tt.wantErrString)
require.Equal(t, errors.Convert(err).Code, tt.wantErrCode)
return
}
require.NoError(t, err)
})
}
}
func TestToken_LastItem(t *testing.T) {
t.Parallel()
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
tests := []struct {
name string
token *listtoken.Token
wantItemPublicId string
wantItemCreateTime *timestamp.Timestamp
wantItemUpdateTime *timestamp.Timestamp
wantResourceType resource.Type
wantErrString string
wantErrCode errors.Code
}{
{
name: "refresh token returns item with update time",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.RefreshToken{
PhaseUpperBound: fiveDaysAgo.Add(time.Hour),
PreviousDeletedIdsTime: fiveDaysAgo,
PhaseLowerBound: fiveDaysAgo,
LastItemId: "some id",
LastItemUpdateTime: fiveDaysAgo,
},
},
wantItemPublicId: "some id",
wantItemCreateTime: nil,
wantItemUpdateTime: timestamp.New(fiveDaysAgo),
wantResourceType: resource.Target,
},
{
name: "pagination token returns item with create time",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.PaginationToken{
LastItemId: "some id",
LastItemCreateTime: fiveDaysAgo,
},
},
wantItemPublicId: "some id",
wantItemCreateTime: timestamp.New(fiveDaysAgo),
wantItemUpdateTime: nil,
wantResourceType: resource.Target,
},
{
name: "start refresh token returns no item",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: &listtoken.StartRefreshToken{
PreviousPhaseUpperBound: fiveDaysAgo,
PreviousDeletedIdsTime: fiveDaysAgo,
},
},
wantErrString: "start refresh tokens have no last item",
wantErrCode: errors.Internal,
},
{
name: "nil subtype returns no item",
token: &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: nil,
},
wantErrString: "unexpected token subtype",
wantErrCode: errors.Internal,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
item, err := tt.token.LastItem(context.Background())
if tt.wantErrString != "" {
require.ErrorContains(t, err, tt.wantErrString)
require.Equal(t, errors.Convert(err).Code, tt.wantErrCode)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantResourceType, item.GetResourceType())
assert.Equal(t, tt.wantItemCreateTime, item.GetCreateTime())
assert.Equal(t, tt.wantItemPublicId, item.GetPublicId())
assert.Equal(t, tt.wantItemUpdateTime, item.GetUpdateTime())
})
}
}
func TestToken_Transition(t *testing.T) {
t.Parallel()
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
t.Run("pagination token without complete listing transitions to pagination token", func(t *testing.T) {
t.Parallel()
createTime := fiveDaysAgo.Add(-time.Hour)
tk, err := listtoken.NewPagination(
context.Background(),
fiveDaysAgo,
resource.Target,
[]byte("some hash"),
"some id",
createTime,
)
require.NoError(t, err)
lastItem := &fakeTarget{
publicId: "some other id",
createTime: timestamp.New(fiveDaysAgo.Add(time.Hour)),
}
deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour)
phaseUpperBound := fiveDaysAgo.Add(3 * time.Hour)
err = tk.Transition(context.Background(), false, lastItem, deletedIdsTime, phaseUpperBound)
require.NoError(t, err)
st, ok := tk.Subtype.(*listtoken.PaginationToken)
require.True(t, ok, "Subtype was %T, not PaginationToken", tk.Subtype)
assert.Equal(t, lastItem.createTime.AsTime(), st.LastItemCreateTime)
assert.Equal(t, lastItem.publicId, st.LastItemId)
})
t.Run("pagination token with complete listing transitions to start refresh", func(t *testing.T) {
t.Parallel()
createTime := fiveDaysAgo.Add(-time.Hour)
tk, err := listtoken.NewPagination(
context.Background(),
fiveDaysAgo,
resource.Target,
[]byte("some hash"),
"some id",
createTime,
)
require.NoError(t, err)
lastItem := &fakeTarget{
publicId: "some other id",
createTime: timestamp.New(fiveDaysAgo.Add(time.Hour)),
}
deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour)
phaseUpperBound := fiveDaysAgo.Add(3 * time.Hour)
err = tk.Transition(context.Background(), true, lastItem, deletedIdsTime, phaseUpperBound)
require.NoError(t, err)
st, ok := tk.Subtype.(*listtoken.StartRefreshToken)
require.True(t, ok, "Subtype was %T, not StartRefreshToken", tk.Subtype)
assert.Equal(t, fiveDaysAgo, st.PreviousDeletedIdsTime)
assert.Equal(t, fiveDaysAgo, st.PreviousPhaseUpperBound)
})
t.Run("start refresh token without complete listing transitions to refresh token", func(t *testing.T) {
t.Parallel()
deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour)
phaseUpperBound := fiveDaysAgo.Add(3 * time.Hour)
tk, err := listtoken.NewStartRefresh(
context.Background(),
fiveDaysAgo,
resource.Target,
[]byte("some hash"),
deletedIdsTime,
phaseUpperBound,
)
require.NoError(t, err)
lastItem := &fakeTarget{
publicId: "some other id",
updateTime: timestamp.New(fiveDaysAgo.Add(time.Hour)),
}
newDeletedIdsTime := fiveDaysAgo.Add(4 * time.Hour)
newPhaseUpperBound := fiveDaysAgo.Add(5 * time.Hour)
err = tk.Transition(context.Background(), false, lastItem, newDeletedIdsTime, newPhaseUpperBound)
require.NoError(t, err)
st, ok := tk.Subtype.(*listtoken.RefreshToken)
require.True(t, ok, "Subtype was %T, not RefreshToken", tk.Subtype)
assert.Equal(t, lastItem.updateTime.AsTime(), st.LastItemUpdateTime)
assert.Equal(t, lastItem.publicId, st.LastItemId)
assert.Equal(t, phaseUpperBound, st.PhaseLowerBound)
assert.Equal(t, newPhaseUpperBound, st.PhaseUpperBound)
assert.Equal(t, newDeletedIdsTime, st.PreviousDeletedIdsTime)
})
t.Run("start refresh token with complete listing transitions to start refresh", func(t *testing.T) {
t.Parallel()
deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour)
phaseUpperBound := fiveDaysAgo.Add(3 * time.Hour)
tk, err := listtoken.NewStartRefresh(
context.Background(),
fiveDaysAgo,
resource.Target,
[]byte("some hash"),
deletedIdsTime,
phaseUpperBound,
)
require.NoError(t, err)
lastItem := &fakeTarget{
publicId: "some other id",
createTime: timestamp.New(fiveDaysAgo.Add(time.Hour)),
}
newDeletedIdsTime := fiveDaysAgo.Add(4 * time.Hour)
newPhaseUpperBound := fiveDaysAgo.Add(5 * time.Hour)
err = tk.Transition(context.Background(), true, lastItem, newDeletedIdsTime, newPhaseUpperBound)
require.NoError(t, err)
st, ok := tk.Subtype.(*listtoken.StartRefreshToken)
require.True(t, ok, "Subtype was %T, not StartRefreshToken", tk.Subtype)
assert.Equal(t, newDeletedIdsTime, st.PreviousDeletedIdsTime)
assert.Equal(t, newPhaseUpperBound, st.PreviousPhaseUpperBound)
})
t.Run("refresh token without complete listing transitions to refresh token", func(t *testing.T) {
t.Parallel()
deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour)
phaseLowerBound := fiveDaysAgo.Add(3 * time.Hour)
phaseUpperBound := fiveDaysAgo.Add(4 * time.Hour)
updateTime := fiveDaysAgo.Add(5 * time.Hour)
tk, err := listtoken.NewRefresh(
context.Background(),
fiveDaysAgo,
resource.Target,
[]byte("some hash"),
deletedIdsTime,
phaseUpperBound,
phaseLowerBound,
"some id",
updateTime,
)
require.NoError(t, err)
lastItem := &fakeTarget{
publicId: "some other id",
updateTime: timestamp.New(fiveDaysAgo.Add(time.Hour)),
}
newDeletedIdsTime := fiveDaysAgo.Add(6 * time.Hour)
newPhaseUpperBound := fiveDaysAgo.Add(7 * time.Hour)
err = tk.Transition(context.Background(), false, lastItem, newDeletedIdsTime, newPhaseUpperBound)
require.NoError(t, err)
st, ok := tk.Subtype.(*listtoken.RefreshToken)
require.True(t, ok, "Subtype was %T, not RefreshToken", tk.Subtype)
assert.Equal(t, lastItem.updateTime.AsTime(), st.LastItemUpdateTime)
assert.Equal(t, lastItem.publicId, st.LastItemId)
assert.Equal(t, phaseLowerBound, st.PhaseLowerBound)
assert.Equal(t, phaseUpperBound, st.PhaseUpperBound)
assert.Equal(t, newDeletedIdsTime, st.PreviousDeletedIdsTime)
})
t.Run("refresh token with complete listing transitions to start refresh", func(t *testing.T) {
t.Parallel()
deletedIdsTime := fiveDaysAgo.Add(2 * time.Hour)
phaseLowerBound := fiveDaysAgo.Add(3 * time.Hour)
phaseUpperBound := fiveDaysAgo.Add(4 * time.Hour)
updateTime := fiveDaysAgo.Add(5 * time.Hour)
tk, err := listtoken.NewRefresh(
context.Background(),
fiveDaysAgo,
resource.Target,
[]byte("some hash"),
deletedIdsTime,
phaseUpperBound,
phaseLowerBound,
"some id",
updateTime,
)
require.NoError(t, err)
lastItem := &fakeTarget{
publicId: "some other id",
createTime: timestamp.New(fiveDaysAgo.Add(time.Hour)),
}
newDeletedIdsTime := fiveDaysAgo.Add(6 * time.Hour)
newPhaseUpperBound := fiveDaysAgo.Add(7 * time.Hour)
err = tk.Transition(context.Background(), true, lastItem, newDeletedIdsTime, newPhaseUpperBound)
require.NoError(t, err)
st, ok := tk.Subtype.(*listtoken.StartRefreshToken)
require.True(t, ok, "Subtype was %T, not StartRefreshToken", tk.Subtype)
assert.Equal(t, newDeletedIdsTime, st.PreviousDeletedIdsTime)
assert.Equal(t, phaseUpperBound, st.PreviousPhaseUpperBound)
})
t.Run("token without subtype errors", func(t *testing.T) {
t.Parallel()
tk := &listtoken.Token{
CreateTime: fiveDaysAgo,
ResourceType: resource.Target,
GrantsHash: []byte("some hash"),
Subtype: nil,
}
lastItem := &fakeTarget{
publicId: "some other id",
createTime: timestamp.New(fiveDaysAgo.Add(time.Hour)),
}
newDeletedIdsTime := fiveDaysAgo.Add(6 * time.Hour)
newPhaseUpperBound := fiveDaysAgo.Add(7 * time.Hour)
err := tk.Transition(context.Background(), true, lastItem, newDeletedIdsTime, newPhaseUpperBound)
require.Error(t, err)
assert.ErrorContains(t, err, "unexpected token subtype")
})
}
type fakeTarget struct {
boundary.Resource
publicId string
updateTime *timestamp.Timestamp
createTime *timestamp.Timestamp
}
func (m *fakeTarget) GetResourceType() resource.Type {
return resource.Target
}
func (m *fakeTarget) GetPublicId() string {
return m.publicId
}
func (m *fakeTarget) GetUpdateTime() *timestamp.Timestamp {
return m.updateTime
}
func (m *fakeTarget) GetCreateTime() *timestamp.Timestamp {
return m.createTime
}