From 97ba8b3bad3b201f9b91f047466914576d45d254 Mon Sep 17 00:00:00 2001 From: Johan Brandhorst-Satzkorn Date: Wed, 8 Nov 2023 17:00:31 -0800 Subject: [PATCH] internal/pagination: add generic ListPlugin functions These new methods can be used in places where the caller wants to automatically paginate over a resource that is returned together with a plugin, such as plugin hosts, host sets and storage buckets. --- internal/pagination/pagination.go | 213 +- internal/pagination/pagination_plugin.go | 307 ++ internal/pagination/pagination_plugin_test.go | 3461 +++++++++++++++++ 3 files changed, 3869 insertions(+), 112 deletions(-) create mode 100644 internal/pagination/pagination_plugin.go create mode 100644 internal/pagination/pagination_plugin_test.go diff --git a/internal/pagination/pagination.go b/internal/pagination/pagination.go index 5d73856104..7df93cb47d 100644 --- a/internal/pagination/pagination.go +++ b/internal/pagination/pagination.go @@ -101,55 +101,7 @@ func List[T boundary.Resource]( return nil, errors.Wrap(ctx, err, op) } - resp := &ListResponse[T]{ - Items: items, - CompleteListing: completeListing, - EstimatedItemCount: len(items), - } - - if len(items) > 0 { - lastItem := items[len(items)-1] - - if completeListing { - // If this is the only page in the pagination, create a - // start refresh token so subsequent requests are informed - // that they need to start a new refresh phase. - resp.ListToken, err = listtoken.NewStartRefresh( - ctx, - listTime, // Use list time as the create time of the token - lastItem.GetResourceType(), - grantsHash, - listTime, // Use list time as the starting point for listing deleted ids - listTime, // Use list time as the lower bound for subsequent refresh - ) - if err != nil { - return nil, err - } - } else { - resp.ListToken, err = listtoken.NewPagination( - ctx, - listTime, // Use list time as the create time of the token - lastItem.GetResourceType(), - grantsHash, - lastItem.GetPublicId(), - lastItem.GetCreateTime().AsTime(), - ) - if err != nil { - return nil, err - } - } - } - if !completeListing { - // If this was not a complete listing, get an estimate - // of the total items from the DB. - var err error - resp.EstimatedItemCount, err = estimatedCountFn(ctx) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - } - - return resp, nil + return buildListResp(ctx, grantsHash, items, completeListing, listTime, estimatedCountFn) } // ListPage returns a ListResponse. The response will contain at most pageSize @@ -195,30 +147,7 @@ func ListPage[T boundary.Resource]( return nil, errors.Wrap(ctx, err, op) } - resp := &ListResponse[T]{ - Items: items, - CompleteListing: completeListing, - ListToken: tok, - } - - resp.EstimatedItemCount, err = estimatedCountFn(ctx) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - var lastItem boundary.Resource - if len(items) > 0 { - lastItem = items[len(items)-1] - } - if err := resp.ListToken.Transition( - ctx, - completeListing, - lastItem, - time.Time{}, // We have no deleted ids time - listTime, - ); err != nil { - return nil, errors.Wrap(ctx, err, op) - } - return resp, nil + return buildListPageResp(ctx, completeListing, nil, time.Time{} /* no deleted ids time */, items, listTime, tok, estimatedCountFn) } // ListRefresh returns a ListResponse. The response will contain at most pageSize @@ -274,25 +203,7 @@ func ListRefresh[T boundary.Resource]( return nil, errors.Wrap(ctx, err, op) } - resp := &ListResponse[T]{ - Items: items, - CompleteListing: completeListing, - DeletedIds: deletedIds, - ListToken: tok, - } - - resp.EstimatedItemCount, err = estimatedCountFn(ctx) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - var lastItem boundary.Resource - if len(items) > 0 { - lastItem = items[len(items)-1] - } - if err := resp.ListToken.Transition(ctx, completeListing, lastItem, deletedIdsTime, listTime); err != nil { - return nil, errors.Wrap(ctx, err, op) - } - return resp, nil + return buildListPageResp(ctx, completeListing, deletedIds, deletedIdsTime, items, listTime, tok, estimatedCountFn) } // ListRefreshPage returns a ListResponse. The response will contain at most pageSize @@ -348,26 +259,7 @@ func ListRefreshPage[T boundary.Resource]( return nil, errors.Wrap(ctx, err, op) } - resp := &ListResponse[T]{ - Items: items, - CompleteListing: completeListing, - DeletedIds: deletedIds, - ListToken: tok, - } - - resp.EstimatedItemCount, err = estimatedCountFn(ctx) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - - var lastItem boundary.Resource - if len(items) > 0 { - lastItem = items[len(items)-1] - } - if err := resp.ListToken.Transition(ctx, completeListing, lastItem, deletedIdsTime, listTime); err != nil { - return nil, errors.Wrap(ctx, err, op) - } - return resp, nil + return buildListPageResp(ctx, completeListing, deletedIds, deletedIdsTime, items, listTime, tok, estimatedCountFn) } func list[T boundary.Resource]( @@ -425,3 +317,100 @@ dbLoop: return items, completeListing, firstListTime, nil } + +func buildListResp[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + items []T, + completeListing bool, + listTime time.Time, + estimatedCountFn EstimatedCountFunc, +) (*ListResponse[T], error) { + resp := &ListResponse[T]{ + Items: items, + CompleteListing: completeListing, + EstimatedItemCount: len(items), + } + + var err error + if len(items) > 0 { + lastItem := items[len(items)-1] + + if completeListing { + // If this is the only page in the pagination, create a + // start refresh token so subsequent requests are informed + // that they need to start a new refresh phase. + resp.ListToken, err = listtoken.NewStartRefresh( + ctx, + listTime, // Use list time as the create time of the token + lastItem.GetResourceType(), + grantsHash, + listTime, // Use list time as the starting point for listing deleted ids + listTime, // Use list time as the lower bound for subsequent refresh + ) + if err != nil { + return nil, err + } + } else { + resp.ListToken, err = listtoken.NewPagination( + ctx, + listTime, // Use list time as the create time of the token + lastItem.GetResourceType(), + grantsHash, + lastItem.GetPublicId(), + lastItem.GetCreateTime().AsTime(), + ) + if err != nil { + return nil, err + } + } + } + if !completeListing { + // If this was not a complete listing, get an estimate + // of the total items from the DB. + var err error + resp.EstimatedItemCount, err = estimatedCountFn(ctx) + if err != nil { + return nil, err + } + } + return resp, err +} + +func buildListPageResp[T boundary.Resource]( + ctx context.Context, + completeListing bool, + deletedIds []string, + deletedIdsTime time.Time, + items []T, + listTime time.Time, + tok *listtoken.Token, + estimatedCountFn EstimatedCountFunc, +) (*ListResponse[T], error) { + resp := &ListResponse[T]{ + Items: items, + CompleteListing: completeListing, + ListToken: tok, + DeletedIds: deletedIds, + } + + var err error + resp.EstimatedItemCount, err = estimatedCountFn(ctx) + if err != nil { + return nil, err + } + var lastItem boundary.Resource + if len(items) > 0 { + lastItem = items[len(items)-1] + } + if err := resp.ListToken.Transition( + ctx, + completeListing, + lastItem, + deletedIdsTime, + listTime, + ); err != nil { + return nil, err + } + return resp, err +} diff --git a/internal/pagination/pagination_plugin.go b/internal/pagination/pagination_plugin.go new file mode 100644 index 0000000000..25933316e9 --- /dev/null +++ b/internal/pagination/pagination_plugin.go @@ -0,0 +1,307 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package pagination + +import ( + "context" + "time" + + "github.com/hashicorp/boundary/internal/boundary" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/listtoken" + "github.com/hashicorp/boundary/internal/plugin" +) + +// ListPluginFilterFunc is a callback used to filter out resources that don't match +// some criteria. The function must return true for items that should be included in the final +// result. Returning an error results in an error being returned from the pagination. +type ListPluginFilterFunc[T boundary.Resource] func(ctx context.Context, item T, plugin *plugin.Plugin) (bool, error) + +// ListPluginItemsFunc returns a slice of T that are ordered after prevPageLastItem according to +// the implementation of the function. If prevPageLastItem is empty, it should return +// a slice of T from the start, as defined by the function. It also returns the timestamp +// of the DB transaction used to list the items. +type ListPluginItemsFunc[T boundary.Resource] func(ctx context.Context, prevPageLastItem T, limit int) ([]T, *plugin.Plugin, time.Time, error) + +// ListPlugin returns a ListResponse and a plugin associated with the resources. +// The response will contain at most pageSize number of items. +// Items are fetched using the listItemsFn and checked using +// the filterItemFn to determine if they should be included in the response. +// The response includes a new list token used to continue pagination or refresh. +// The estimatedCountFn is used to provide an estimated total number of +// items that can be returned by making additional requests using the returned +// list token. +func ListPlugin[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn ListPluginFilterFunc[T], + listItemsFn ListPluginItemsFunc[T], + estimatedCountFn EstimatedCountFunc, +) (*ListResponse[T], *plugin.Plugin, error) { + const op = "pagination.ListPlugin" + + switch { + case len(grantsHash) == 0: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash") + case pageSize < 1: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1") + case filterItemFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback") + case listItemsFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list items callback") + case estimatedCountFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing estimated count callback") + } + + items, plg, completeListing, listTime, err := listPlugin(ctx, pageSize, filterItemFn, listItemsFn) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + resp, err := buildListResp(ctx, grantsHash, items, completeListing, listTime, estimatedCountFn) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + return resp, plg, nil +} + +// ListPluginPage returns a ListResponse and a plugin associated with the resources. +// The response will contain at most pageSize number of items. +// Items are fetched using the listItemsFn and checked using +// the filterItemFn to determine if they should be included in the response. +// Items will be fetched based on the contents of the list token. The list +// token must contain a PaginationToken component. +// The response includes a new list token used to continue pagination or refresh. +// The estimatedCountFn is used to provide an estimated total number of +// items that can be returned by making additional requests using the returned +// list token. +func ListPluginPage[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn ListPluginFilterFunc[T], + listItemsFn ListPluginItemsFunc[T], + estimatedCountFn EstimatedCountFunc, + tok *listtoken.Token, +) (*ListResponse[T], *plugin.Plugin, error) { + const op = "pagination.ListPluginPage" + + switch { + case len(grantsHash) == 0: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash") + case pageSize < 1: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1") + case filterItemFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback") + case listItemsFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list items callback") + case estimatedCountFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing estimated count callback") + case tok == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list token") + } + if _, ok := tok.Subtype.(*listtoken.PaginationToken); !ok { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a pagination token component") + } + + items, plg, completeListing, listTime, err := listPlugin(ctx, pageSize, filterItemFn, listItemsFn) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + resp, err := buildListPageResp(ctx, completeListing, nil, time.Time{} /* no deleted ids time */, items, listTime, tok, estimatedCountFn) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + return resp, plg, nil +} + +// ListPluginRefresh returns a ListResponse and a plugin associated with the resources. +// The response will contain at most pageSize number of items. +// Items are fetched using the listItemsFn and checked using +// the filterItemFn to determine if they should be included in the response. +// Items will be fetched based on the contents of the list token. The list +// token must contain a StartRefreshToken component. +// The response includes a new list token used to continue pagination or refresh. +// The estimatedCountFn is used to provide an estimated total number of +// items that can be returned by making additional requests using the returned +// list token. The listDeletedIDsFn is used to list the IDs of any +// resources that have been deleted since the list token was last used. +func ListPluginRefresh[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn ListPluginFilterFunc[T], + listItemsFn ListPluginItemsFunc[T], + estimatedCountFn EstimatedCountFunc, + listDeletedIDsFn ListDeletedIDsFunc, + tok *listtoken.Token, +) (*ListResponse[T], *plugin.Plugin, error) { + const op = "pagination.ListPluginRefresh" + + switch { + case len(grantsHash) == 0: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash") + case pageSize < 1: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1") + case filterItemFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback") + case listItemsFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list items callback") + case estimatedCountFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing estimated count callback") + case listDeletedIDsFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list deleted IDs callback") + case tok == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list token") + } + srt, ok := tok.Subtype.(*listtoken.StartRefreshToken) + if !ok { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a start-refresh token component") + } + + deletedIds, deletedIdsTime, err := listDeletedIDsFn(ctx, srt.PreviousDeletedIdsTime) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + items, plg, completeListing, listTime, err := listPlugin(ctx, pageSize, filterItemFn, listItemsFn) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + resp, err := buildListPageResp(ctx, completeListing, deletedIds, deletedIdsTime, items, listTime, tok, estimatedCountFn) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + return resp, plg, nil +} + +// ListPluginRefreshPage returns a ListResponse and a plugin associated with the resources. +// The response will contain at most pageSize number of items. +// Items are fetched using the listItemsFn and checked using +// the filterItemFn to determine if they should be included in the response. +// Items will be fetched based on the contents of the list token. The list +// token must contain a RefreshToken component. +// The response includes a new list token used to continue pagination or refresh. +// The estimatedCountFn is used to provide an estimated total number of +// items that can be returned by making additional requests using the returned +// list token. The listDeletedIDsFn is used to list the IDs of any +// resources that have been deleted since the list token was last used. +func ListPluginRefreshPage[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn ListPluginFilterFunc[T], + listItemsFn ListPluginItemsFunc[T], + estimatedCountFn EstimatedCountFunc, + listDeletedIDsFn ListDeletedIDsFunc, + tok *listtoken.Token, +) (*ListResponse[T], *plugin.Plugin, error) { + const op = "pagination.ListPluginRefreshPage" + + switch { + case len(grantsHash) == 0: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash") + case pageSize < 1: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1") + case filterItemFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter item callback") + case listItemsFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list items callback") + case estimatedCountFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing estimated count callback") + case listDeletedIDsFn == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list deleted IDs callback") + case tok == nil: + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list token") + } + rt, ok := tok.Subtype.(*listtoken.RefreshToken) + if !ok { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "token did not have a refresh token component") + } + + deletedIds, deletedIdsTime, err := listDeletedIDsFn(ctx, rt.PreviousDeletedIdsTime) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + items, plg, completeListing, listTime, err := listPlugin(ctx, pageSize, filterItemFn, listItemsFn) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + resp, err := buildListPageResp(ctx, completeListing, deletedIds, deletedIdsTime, items, listTime, tok, estimatedCountFn) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + return resp, plg, nil +} + +func listPlugin[T boundary.Resource]( + ctx context.Context, + pageSize int, + filterItemFn ListPluginFilterFunc[T], + listItemsFn ListPluginItemsFunc[T], +) ([]T, *plugin.Plugin, bool, time.Time, error) { + const op = "pagination.list" + + var lastItem T + var plg *plugin.Plugin + var firstListTime time.Time + limit := pageSize + 1 + items := make([]T, 0, limit) +dbLoop: + for { + // Request another page from the DB until we fill the final items + page, newPlg, listTime, err := listItemsFn(ctx, lastItem, limit) + if err != nil { + return nil, nil, false, time.Time{}, errors.Wrap(ctx, err, op) + } + // Assign the firstListTime once, to ensure we always store the start of listing, + // rather the timestamp of the last listing. + if firstListTime.IsZero() { + firstListTime = listTime + } + if plg == nil { + plg = newPlg + } else if newPlg != nil && plg.PublicId != newPlg.PublicId { + return nil, nil, false, time.Time{}, errors.New(ctx, errors.Internal, op, "plugin changed between list invocations") + } + for _, item := range page { + ok, err := filterItemFn(ctx, item, plg) + if err != nil { + return nil, nil, false, time.Time{}, errors.Wrap(ctx, err, op) + } + if ok { + items = append(items, item) + // If we filled the items after filtering, + // we're done. + if len(items) == cap(items) { + break dbLoop + } + } + } + // If the current page was shorter than the limit, stop iterating + if len(page) < limit { + break dbLoop + } + + lastItem = page[len(page)-1] + } + // If we couldn't fill the items, it was a complete listing. + completeListing := len(items) < cap(items) + if !completeListing { + // Items is of size pageSize+1, so + // truncate if it was filled. + items = items[:pageSize] + } + + return items, plg, completeListing, firstListTime, nil +} diff --git a/internal/pagination/pagination_plugin_test.go b/internal/pagination/pagination_plugin_test.go new file mode 100644 index 0000000000..0e33b5c301 --- /dev/null +++ b/internal/pagination/pagination_plugin_test.go @@ -0,0 +1,3461 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package pagination + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/listtoken" + "github.com/hashicorp/boundary/internal/plugin" + "github.com/hashicorp/boundary/internal/types/resource" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_ListPlugin(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("validation", func(t *testing.T) { + t.Parallel() + t.Run("empty grants hash", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte(nil) + _, _, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "missing grants hash") + }) + t.Run("zero page size", func(t *testing.T) { + t.Parallel() + pageSize := 0 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "page size must be at least 1") + }) + t.Run("negative page size", func(t *testing.T) { + t.Parallel() + pageSize := -1 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "page size must be at least 1") + }) + t.Run("nil filter item callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := ListPluginFilterFunc[*testType](nil) + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "missing filter item callback") + }) + t.Run("nil list items callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listItemsFn := ListPluginItemsFunc[*testType](nil) + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "missing list items callback") + }) + t.Run("nil estimated count callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := EstimatedCountFunc(nil) + grantsHash := []byte("some hash") + _, _, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "missing estimated count callback") + }) + }) + t.Run("error-cases", func(t *testing.T) { + t.Parallel() + t.Run("errors-when-list-errors-immediately", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + return nil, nil, time.Time{}, errors.New("failed to list") + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-list-errors-subsequently", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + return nil, nil, time.Time{}, errors.New("failed to list") + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-filter-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return false, errors.New("failed to filter") + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "failed to filter") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-estimated-count-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 0, errors.New("failed to estimate count") + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "failed to estimate count") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-plugin-changes-between-invocations", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + origPlg.PublicId = "id1" + otherPlg := plugin.NewPlugin() + otherPlg.PublicId = "id2" + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast == nil { + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "5", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-4 * time.Second), lastItemUpdateTime.Add(-4 * time.Second)}, + }, otherPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" || item.ID == "6" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "plugin changed between list invocations") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + }) + t.Run("no-rows", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.NoError(t, err) + assert.Empty(t, resp.Items) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 0) + // No response token expected when there were no results + assert.Nil(t, resp.ListToken) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-first-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(listReturnTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemId, "2") + assert.True(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemCreateTime.Equal(lastItemCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-first-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 2) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(listReturnTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(listReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" || item.ID == "6" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(listReturnTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemId, "3") + assert.True(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemCreateTime.Equal(lastItemCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(listReturnTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemId, "3") + assert.True(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemCreateTime.Equal(lastItemCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 2) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(listReturnTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(listReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("dont-fill-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + })) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 1) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(listReturnTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(listReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("dont-fill-with-full-last-page", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + switch { + case prevPageLast == nil: + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + case prevPageLast.ID == "3": + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlg, listReturnTime.Add(2 * time.Second), nil + default: + t.Fatalf("unexpected call to listItemsFn with %#v", prevPageLast) + return nil, nil, time.Time{}, nil + } + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + })) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 1) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(listReturnTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(listReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("filter-everything", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + switch { + case prevPageLast == nil: + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + case prevPageLast.ID == "3": + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlg, listReturnTime.Add(2 * time.Second), nil + default: + t.Fatalf("unexpected call to listItemsFn with %#v", prevPageLast) + return nil, nil, time.Time{}, nil + } + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + // Filter every item + return false, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPlugin(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.NoError(t, err) + assert.Empty(t, resp.Items) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 0) + assert.Nil(t, resp.ListToken) + assert.Equal(t, origPlg, plg) + }) +} + +func Test_ListPluginPage(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("validation", func(t *testing.T) { + t.Parallel() + t.Run("empty grants hash", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte(nil) + _, _, err = ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "missing grants hash") + }) + t.Run("zero page size", func(t *testing.T) { + t.Parallel() + pageSize := 0 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "page size must be at least 1") + }) + t.Run("negative page size", func(t *testing.T) { + t.Parallel() + pageSize := -1 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "page size must be at least 1") + }) + t.Run("nil filter item callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := ListPluginFilterFunc[*testType](nil) + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "missing filter item callback") + }) + t.Run("nil list items callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + listItemsFn := ListPluginItemsFunc[*testType](nil) + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "missing list items callback") + }) + t.Run("nil token", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, nil) + require.ErrorContains(t, err, "missing list token") + }) + t.Run("wrong token type", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "token did not have a pagination token component") + }) + t.Run("nil estimated count callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := EstimatedCountFunc(nil) + grantsHash := []byte("some hash") + _, _, err = ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "missing estimated count callback") + }) + }) + t.Run("error-cases", func(t *testing.T) { + t.Run("errors-when-list-errors-immediately", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + return nil, nil, time.Time{}, errors.New("failed to list") + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-list-errors-subsequently", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + return nil, nil, time.Time{}, errors.New("failed to list") + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-filter-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return false, errors.New("failed to filter") + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "failed to filter") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-estimated-count-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 0, errors.New("failed to estimate count") + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "failed to estimate count") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-plugin-changes-between-invocations", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + origPlg.PublicId = "id1" + otherPlg := plugin.NewPlugin() + otherPlg.PublicId = "id2" + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast == nil { + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "5", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-4 * time.Second), lastItemUpdateTime.Add(-4 * time.Second)}, + }, otherPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" || item.ID == "6" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "plugin changed between list invocations") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + }) + t.Run("no-rows", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.NoError(t, err) + assert.Empty(t, resp.Items) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(tokenCreateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(tokenCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-first-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemId, "2") + assert.True(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemCreateTime.Equal(lastItemCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-first-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(tokenCreateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(tokenCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" || item.ID == "6" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemId, "3") + assert.True(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemCreateTime.Equal(lastItemCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemId, "3") + assert.True(t, resp.ListToken.Subtype.(*listtoken.PaginationToken).LastItemCreateTime.Equal(lastItemCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(tokenCreateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(tokenCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("dont-fill-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + })) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(tokenCreateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(tokenCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("dont-fill-with-full-last-page", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + switch { + case prevPageLast == nil: + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + case prevPageLast.ID == "3": + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlg, listReturnTime.Add(2 * time.Second), nil + default: + t.Fatalf("unexpected call to listItemsFn with %#v", prevPageLast) + return nil, nil, time.Time{}, nil + } + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + })) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(tokenCreateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(tokenCreateTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("filter-everything", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + switch { + case prevPageLast == nil: + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + case prevPageLast.ID == "3": + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlg, listReturnTime.Add(2 * time.Second), nil + default: + t.Fatalf("unexpected call to listItemsFn with %#v", prevPageLast) + return nil, nil, time.Time{}, nil + } + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + // Filter every item + return false, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.NoError(t, err) + assert.Empty(t, resp.Items) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(tokenCreateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(tokenCreateTime)) + assert.Equal(t, origPlg, plg) + }) +} + +func Test_ListPluginRefresh(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("validation", func(t *testing.T) { + t.Parallel() + t.Run("empty grants hash", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte(nil) + _, _, err = ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "missing grants hash") + }) + t.Run("zero page size", func(t *testing.T) { + t.Parallel() + pageSize := 0 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "page size must be at least 1") + }) + t.Run("negative page size", func(t *testing.T) { + t.Parallel() + pageSize := -1 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "page size must be at least 1") + }) + t.Run("nil filter item callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := ListPluginFilterFunc[*testType](nil) + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "missing filter item callback") + }) + t.Run("nil list items callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + listItemsFn := ListPluginItemsFunc[*testType](nil) + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "missing list items callback") + }) + t.Run("nil list deleted ids callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := ListDeletedIDsFunc(nil) + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "missing list deleted IDs callback") + }) + t.Run("nil token", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, nil) + require.ErrorContains(t, err, "missing list token") + }) + t.Run("wrong token type", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "token did not have a start-refresh token component") + }) + t.Run("nil estimated count callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := EstimatedCountFunc(nil) + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "missing estimated count callback") + }) + }) + t.Run("error-cases", func(t *testing.T) { + t.Run("errors-when-list-errors-immediately", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + return nil, nil, time.Time{}, errors.New("failed to list") + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-list-errors-subsequently", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + return nil, nil, time.Time{}, errors.New("failed to list") + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-filter-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return false, errors.New("failed to filter") + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to filter") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-estimated-count-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 0, errors.New("failed to estimate count") + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to estimate count") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-list-deleted-ids-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, time.Time{}, errors.New("failed to list deleted IDs") + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list deleted IDs") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-plugin-changes-between-invocations", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + origPlg.PublicId = "id1" + otherPlg := plugin.NewPlugin() + otherPlg.PublicId = "id2" + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast == nil { + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "5", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-4 * time.Second), lastItemUpdateTime.Add(-4 * time.Second)}, + }, otherPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" || item.ID == "6" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "plugin changed between list invocations") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + }) + t.Run("no-rows", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, resp.Items) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-first-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemId, "2") + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemUpdateTime.Equal(lastItemUpdateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseLowerBound.Equal(prevPhaseUpperBound)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-first-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.True(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" || item.ID == "6" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemId, "3") + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemUpdateTime.Equal(lastItemUpdateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseLowerBound.Equal(prevPhaseUpperBound)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemId, "3") + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemUpdateTime.Equal(lastItemUpdateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseLowerBound.Equal(prevPhaseUpperBound)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.True(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("dont-fill-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + })) + assert.True(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("dont-fill-with-full-last-page", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + switch { + case prevPageLast == nil: + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + case prevPageLast.ID == "3": + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlg, listReturnTime.Add(2 * time.Second), nil + default: + t.Fatalf("unexpected call to listItemsFn with %#v", prevPageLast) + return nil, nil, time.Time{}, nil + } + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + })) + assert.True(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) + t.Run("filter-everything", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewStartRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + prevPhaseUpperBound, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + switch { + case prevPageLast == nil: + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + case prevPageLast.ID == "3": + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlg, listReturnTime.Add(2 * time.Second), nil + default: + t.Fatalf("unexpected call to listItemsFn with %#v", prevPageLast) + return nil, nil, time.Time{}, nil + } + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + // Filter every item + return false, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, resp.Items) + assert.True(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(listReturnTime)) + assert.Equal(t, origPlg, plg) + }) +} + +func Test_ListPluginRefreshPage(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("validation", func(t *testing.T) { + t.Parallel() + t.Run("empty grants hash", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte(nil) + _, _, err = ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "missing grants hash") + }) + t.Run("zero page size", func(t *testing.T) { + t.Parallel() + pageSize := 0 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "page size must be at least 1") + }) + t.Run("negative page size", func(t *testing.T) { + t.Parallel() + pageSize := -1 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "page size must be at least 1") + }) + t.Run("nil filter item callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := ListPluginFilterFunc[*testType](nil) + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "missing filter item callback") + }) + t.Run("nil list items callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + listItemsFn := ListPluginItemsFunc[*testType](nil) + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "missing list items callback") + }) + t.Run("nil list deleted ids callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := ListDeletedIDsFunc(nil) + grantsHash := []byte("some hash") + _, _, err = ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "missing list deleted IDs callback") + }) + t.Run("nil token", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, nil) + require.ErrorContains(t, err, "missing list token") + }) + t.Run("wrong token type", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewPagination( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + "some id", + lastItemCreateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "token did not have a refresh token component") + }) + t.Run("nil estimated count callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, nil, time.Time{}, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := EstimatedCountFunc(nil) + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "missing estimated count callback") + }) + }) + t.Run("error-cases", func(t *testing.T) { + t.Run("errors-when-list-errors-immediately", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + return nil, nil, time.Time{}, errors.New("failed to list") + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-list-errors-subsequently", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + return nil, nil, time.Time{}, errors.New("failed to list") + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-filter-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return false, errors.New("failed to filter") + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to filter") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-estimated-count-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 0, errors.New("failed to estimate count") + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to estimate count") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-list-deleted-ids-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, time.Time{}, errors.New("failed to list deleted IDs") + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list deleted IDs") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-plugin-changes-between-invocations", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + origPlg.PublicId = "id1" + otherPlg := plugin.NewPlugin() + otherPlg.PublicId = "id2" + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast == nil { + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "5", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-4 * time.Second), lastItemUpdateTime.Add(-4 * time.Second)}, + }, otherPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" || item.ID == "6" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "plugin changed between list invocations") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + }) + t.Run("no-rows", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return nil, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, resp.Items) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(phaseUpperBound)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-first-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + {nil, "3", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemId, "2") + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemUpdateTime.Equal(lastItemUpdateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseLowerBound.Equal(phaseLowerBound)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseUpperBound.Equal(phaseUpperBound)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-first-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "2", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.True(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(phaseUpperBound)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" || item.ID == "6" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemId, "3") + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemUpdateTime.Equal(lastItemUpdateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseLowerBound.Equal(phaseLowerBound)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseUpperBound.Equal(phaseUpperBound)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.False(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.Equal(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemId, "3") + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).LastItemUpdateTime.Equal(lastItemUpdateTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseLowerBound.Equal(phaseLowerBound)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.RefreshToken).PhaseUpperBound.Equal(phaseUpperBound)) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID == "2" || item.ID == "4" { + // Filter every other item + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + })) + assert.True(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(phaseUpperBound)) + assert.Equal(t, origPlg, plg) + }) + t.Run("dont-fill-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{ + {nil, "4", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + } + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + })) + assert.True(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(phaseUpperBound)) + assert.Equal(t, origPlg, plg) + }) + t.Run("dont-fill-with-full-last-page", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + switch { + case prevPageLast == nil: + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + case prevPageLast.ID == "3": + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlg, listReturnTime.Add(2 * time.Second), nil + default: + t.Fatalf("unexpected call to listItemsFn with %#v", prevPageLast) + return nil, nil, time.Time{}, nil + } + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + if item.ID != "1" { + // Filter every item except the first + return false, nil + } + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + })) + assert.True(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(phaseUpperBound)) + assert.Equal(t, origPlg, plg) + }) + t.Run("filter-everything", func(t *testing.T) { + t.Parallel() + pageSize := 2 + tok, err := listtoken.NewRefresh( + ctx, + tokenCreateTime, + resource.Unknown, + []byte("some hash"), + prevDeletedTime, + phaseUpperBound, + phaseLowerBound, + "some id", + lastItemUpdateTime, + ) + require.NoError(t, err) + origPlg := plugin.NewPlugin() + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, time.Time, error) { + switch { + case prevPageLast == nil: + return []*testType{ + {nil, "1", lastItemCreateTime.Add(2 * time.Second), lastItemUpdateTime.Add(2 * time.Second)}, + {nil, "2", lastItemCreateTime.Add(time.Second), lastItemUpdateTime.Add(time.Second)}, + {nil, "3", lastItemCreateTime, lastItemUpdateTime}, + }, origPlg, listReturnTime, nil + case prevPageLast.ID == "3": + return []*testType{ + {nil, "4", lastItemCreateTime.Add(-time.Second), lastItemUpdateTime.Add(-time.Second)}, + {nil, "5", lastItemCreateTime.Add(-2 * time.Second), lastItemUpdateTime.Add(-2 * time.Second)}, + {nil, "6", lastItemCreateTime.Add(-3 * time.Second), lastItemUpdateTime.Add(-3 * time.Second)}, + }, origPlg, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlg, listReturnTime.Add(2 * time.Second), nil + default: + t.Fatalf("unexpected call to listItemsFn with %#v", prevPageLast) + return nil, nil, time.Time{}, nil + } + } + filterItemFn := func(ctx context.Context, item *testType, plg *plugin.Plugin) (bool, error) { + // Filter every item + return false, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + deletedIDsFn := func(ctx context.Context, since time.Time) ([]string, time.Time, error) { + return []string{"deleted-id"}, deletedIDsReturnTime, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.NoError(t, err) + assert.Empty(t, resp.Items) + assert.True(t, resp.CompleteListing) + assert.Equal(t, []string{"deleted-id"}, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.ListToken) + assert.True(t, resp.ListToken.CreateTime.Equal(tokenCreateTime)) + assert.Equal(t, resp.ListToken.GrantsHash, grantsHash) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousDeletedIdsTime.Equal(deletedIDsReturnTime)) + assert.True(t, resp.ListToken.Subtype.(*listtoken.StartRefreshToken).PreviousPhaseUpperBound.Equal(phaseUpperBound)) + assert.Equal(t, origPlg, plg) + }) +}