From 0d9ad3dd9d8dc5c061f04a5a938badcbb80c5982 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 ListPlugin and ListRefreshPlugin 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_plugin.go | 221 +++ internal/pagination/pagination_plugin_test.go | 1689 +++++++++++++++++ 2 files changed, 1910 insertions(+) create mode 100644 internal/pagination/pagination_plugin.go create mode 100644 internal/pagination/pagination_plugin_test.go diff --git a/internal/pagination/pagination_plugin.go b/internal/pagination/pagination_plugin.go new file mode 100644 index 00000000000..c691a3571b2 --- /dev/null +++ b/internal/pagination/pagination_plugin.go @@ -0,0 +1,221 @@ +package pagination + +import ( + "context" + + "github.com/hashicorp/boundary/internal/boundary" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/plugin" + "github.com/hashicorp/boundary/internal/refreshtoken" +) + +// 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 have been updated since prevPageLastItem. +// If prevPageLastItem is empty, it returns a slice of T starting with the least recently updated. +type ListPluginItemsFunc[T boundary.Resource] func(ctx context.Context, prevPageLastItem T, limit int) ([]T, *plugin.Plugin, error) + +// ListPluginRefreshItemsFunc returns a slice of T that have been updated since prevPageLastItem. +// If prevPageLastItem is empty, it returns a slice of T that have been updated since the +// item in the refresh token. +type ListPluginRefreshItemsFunc[T boundary.Resource] func(ctx context.Context, tok *refreshtoken.Token, prevPageLastItem T, limit int) ([]T, *plugin.Plugin, error) + +// ListPlugin returns a ListResponse and a plugin. The response will contain at most a +// number of items equal to the pageSize. Items are fetched using the +// listItemsFn and then items are checked using the filterItemFn +// to determine if they should be included in the response. +// The response includes a new refresh token based on the grants and items. +// The estimatedCountFn is used to provide an estimated total number of +// items that can be returned by making additional requests using the provided +// refresh token. The plugin is expected to be the same across invocations +// of listPluginItemsFn. +func ListPlugin[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterPluginItemFn ListPluginFilterFunc[T], + listPluginItemsFn ListPluginItemsFunc[T], + estimatedCountFn EstimatedCountFunc, +) (*ListResponse[T], *plugin.Plugin, error) { + const op = "pagination.ListPlugin" + + if len(grantsHash) == 0 { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash") + } + if pageSize < 1 { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1") + } + if filterPluginItemFn == nil { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter plugin item callback") + } + if listPluginItemsFn == nil { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list plugin items callback") + } + if estimatedCountFn == nil { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing estimated count callback") + } + + items, plg, completeListing, err := listPlugin(ctx, pageSize, filterPluginItemFn, listPluginItemsFn) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + resp := &ListResponse[T]{ + Items: items, + CompleteListing: completeListing, + EstimatedItemCount: len(items), + } + + 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, nil, errors.Wrap(ctx, err, op) + } + } + + if len(items) > 0 { + resp.RefreshToken = refreshtoken.FromResource(items[len(items)-1], grantsHash) + } + + return resp, plg, nil +} + +// ListRefresh returns a ListResponse and a plugin. The response will contain at most a +// number of items equal to the pageSize. Items are fetched using the +// listRefreshItemsFn and then items are checked using the filterItemFn +// to determine if they should be included in the response. +// The response includes a new refresh token based on the grants and items. +// The estimatedCountFn is used to provide an estimated total number of +// items that can be returned by making additional requests using the provided +// refresh token. The listDeletedIDsFn is used to list the IDs of any +// resources that have been deleted since the refresh token was last used. +// The plugin is expected to be the same across invocations of listPluginItemsFn. +func ListPluginRefresh[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterPluginItemFn ListPluginFilterFunc[T], + listPluginRefreshItemsFn ListPluginRefreshItemsFunc[T], + estimatedCountFn EstimatedCountFunc, + listDeletedIDsFn ListDeletedIDsFunc, + tok *refreshtoken.Token, +) (*ListResponse[T], *plugin.Plugin, error) { + const op = "pagination.ListPluginRefresh" + + if len(grantsHash) == 0 { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants hash") + } + if pageSize < 1 { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "page size must be at least 1") + } + if filterPluginItemFn == nil { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing filter plugin item callback") + } + if listPluginRefreshItemsFn == nil { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list plugin refresh items callback") + } + if estimatedCountFn == nil { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing estimated count callback") + } + if listDeletedIDsFn == nil { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing list deleted IDs callback") + } + if tok == nil { + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing refresh token") + } + + deletedIds, transactionTimestamp, err := listDeletedIDsFn(ctx, tok.UpdatedTime) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + listItemsFn := func(ctx context.Context, prevPageLast T, limit int) ([]T, *plugin.Plugin, error) { + return listPluginRefreshItemsFn(ctx, tok, prevPageLast, limit) + } + + items, plg, completeListing, err := listPlugin(ctx, pageSize, filterPluginItemFn, listItemsFn) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + resp := &ListResponse[T]{ + Items: items, + CompleteListing: completeListing, + DeletedIds: deletedIds, + } + + resp.EstimatedItemCount, err = estimatedCountFn(ctx) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + + if len(items) > 0 { + resp.RefreshToken = tok.RefreshLastItem(items[len(items)-1], transactionTimestamp) + } else { + resp.RefreshToken = tok.Refresh(transactionTimestamp) + } + + return resp, plg, nil +} + +func listPlugin[T boundary.Resource]( + ctx context.Context, + pageSize int, + filterItemFn ListPluginFilterFunc[T], + listItemsFn ListPluginItemsFunc[T], +) ([]T, *plugin.Plugin, bool, error) { + const op = "pagination.listPlugin" + + var lastItem T + var plg *plugin.Plugin + limit := pageSize + 1 + items := make([]T, 0, limit) +dbLoop: + for { + // Request another page from the DB until we fill the final items + page, newPlg, err := listItemsFn(ctx, lastItem, limit) + if err != nil { + return nil, nil, false, errors.Wrap(ctx, err, op) + } + if plg == nil { + plg = newPlg + } else if newPlg != nil && plg.PublicId != newPlg.PublicId { + return nil, nil, false, 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, 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, nil +} diff --git a/internal/pagination/pagination_plugin_test.go b/internal/pagination/pagination_plugin_test.go new file mode 100644 index 00000000000..e632f704ef5 --- /dev/null +++ b/internal/pagination/pagination_plugin_test.go @@ -0,0 +1,1689 @@ +// 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/plugin" + "github.com/hashicorp/boundary/internal/refreshtoken" + "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("empty grants hash", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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 plugin 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 plugin 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, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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("no-rows", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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.RefreshToken) + assert.Empty(t, 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, error) { + assert.Nil(t, prevPageLast) + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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"}, {nil, "2"}})) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "2") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + 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, error) { + assert.Nil(t, prevPageLast) + return []*testType{{nil, "1"}, {nil, "2"}}, origPlg, 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"}, {nil, "2"}})) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 2) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "2") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + 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, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{{nil, "4"}, {nil, "5"}, {nil, "6"}}, origPlg, nil + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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"}, {nil, "3"}})) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "3") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + 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, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{{nil, "4"}, {nil, "5"}}, origPlg, nil + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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"}, {nil, "3"}})) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "3") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + 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, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{{nil, "4"}}, origPlg, nil + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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"}, {nil, "3"}})) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 2) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "3") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + 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, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{{nil, "4"}}, origPlg, nil + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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"}})) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 1) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "1") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + 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, error) { + switch { + case prevPageLast == nil: + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, nil + case prevPageLast.ID == "3": + return []*testType{{nil, "4"}, {nil, "5"}, {nil, "6"}}, origPlg, nil + case prevPageLast.ID == "6": + return nil, nil, nil + default: + t.Fatalf("unexpected call to listItemsFn with %#v", prevPageLast) + return nil, nil, 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"}})) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 1) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "1") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + 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, error) { + switch { + case prevPageLast == nil: + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, nil + case prevPageLast.ID == "3": + return []*testType{{nil, "4"}, {nil, "5"}, {nil, "6"}}, origPlg, nil + case prevPageLast.ID == "6": + return nil, nil, nil + default: + t.Fatalf("unexpected call to listItemsFn with %#v", prevPageLast) + return nil, nil, 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.RefreshToken) + assert.Equal(t, origPlg, plg) + }) + 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, error) { + return nil, nil, 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, error) { + if prevPageLast != nil { + return nil, nil, errors.New("failed to list") + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, error) { + assert.Nil(t, prevPageLast) + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, error) { + assert.Nil(t, prevPageLast) + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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-different-plugin-returned", 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, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{{nil, "4"}}, otherPlg, nil + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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) + }) +} + +func Test_ListPluginRefresh(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("empty grants hash", func(t *testing.T) { + t.Parallel() + pageSize := 2 + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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{}, nil + } + grantsHash := []byte(nil) + _, _, err = ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.ErrorContains(t, err, "missing grants hash") + }) + t.Run("zero page size", func(t *testing.T) { + t.Parallel() + pageSize := 0 + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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{}, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.ErrorContains(t, err, "page size must be at least 1") + }) + t.Run("negative page size", func(t *testing.T) { + t.Parallel() + pageSize := -1 + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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{}, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + 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 + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.ErrorContains(t, err, "missing filter plugin item callback") + }) + t.Run("nil list items callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := ListPluginRefreshItemsFunc[*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, time.Time{}, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.ErrorContains(t, err, "missing list plugin refresh items callback") + }) + t.Run("nil estimated count callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.ErrorContains(t, err, "missing estimated count callback") + }) + t.Run("nil deleted ids callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.ErrorContains(t, err, "missing list deleted IDs callback") + }) + t.Run("nil deleted ids callback", func(t *testing.T) { + t.Parallel() + pageSize := 2 + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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{}, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + nil, + ) + require.ErrorContains(t, err, "missing refresh token") + }) + t.Run("no-rows", func(t *testing.T) { + t.Parallel() + pageSize := 2 + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return nil, nil, 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{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + 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) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "1") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.Empty(t, plg) + }) + t.Run("fill-on-first-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{{nil, "1"}, {nil, "2"}})) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "2") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-first-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return []*testType{{nil, "1"}, {nil, "2"}}, origPlg, 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{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{{nil, "1"}, {nil, "2"}})) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "2") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{{nil, "4"}, {nil, "5"}, {nil, "6"}}, origPlg, nil + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{{nil, "1"}, {nil, "3"}})) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "3") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{{nil, "4"}, {nil, "5"}}, origPlg, nil + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{{nil, "1"}, {nil, "3"}})) + assert.False(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "3") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.Equal(t, origPlg, plg) + }) + t.Run("fill-on-subsequent", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{{nil, "4"}}, origPlg, nil + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{{nil, "1"}, {nil, "3"}})) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "3") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.Equal(t, origPlg, plg) + }) + t.Run("dont-fill-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{{nil, "4"}}, origPlg, nil + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{{nil, "1"}})) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "1") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.Equal(t, origPlg, plg) + }) + t.Run("dont-fill-with-full-last-page", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + switch { + case prevPageLast == nil: + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, nil + case prevPageLast.ID == "3": + return []*testType{{nil, "4"}, {nil, "5"}, {nil, "6"}}, origPlg, nil + case prevPageLast.ID == "6": + return nil, nil, nil + default: + t.Fatalf("unexpected call to listRefreshItemsFn with %#v", prevPageLast) + return nil, nil, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(resp.Items, []*testType{{nil, "1"}})) + assert.True(t, resp.CompleteListing) + assert.Empty(t, resp.DeletedIds) + assert.Equal(t, resp.EstimatedItemCount, 10) + require.NotNil(t, resp.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "1") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.Equal(t, origPlg, plg) + }) + t.Run("filter-everything", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + switch { + case prevPageLast == nil: + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, nil + case prevPageLast.ID == "3": + return []*testType{{nil, "4"}, {nil, "5"}, {nil, "6"}}, origPlg, nil + case prevPageLast.ID == "6": + return nil, nil, nil + default: + t.Fatalf("unexpected call to listRefreshItemsFn with %#v", prevPageLast) + return nil, nil, 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 nil, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + 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.RefreshToken) + // Times should be within ~10 seconds of now + assert.True(t, resp.RefreshToken.CreatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.CreatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.True(t, resp.RefreshToken.UpdatedTime.Equal(resp.RefreshToken.CreatedTime)) + assert.Equal(t, resp.RefreshToken.GrantsHash, grantsHash) + assert.Equal(t, resp.RefreshToken.LastItemId, "1") + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.After(time.Now().Add(-10*time.Second))) + assert.True(t, resp.RefreshToken.LastItemUpdatedTime.Before(time.Now().Add(10*time.Second))) + assert.Equal(t, origPlg, plg) + }) + t.Run("errors-when-list-errors-immediately", func(t *testing.T) { + t.Parallel() + pageSize := 2 + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + return nil, nil, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plg) + assert.Empty(t, plg) + }) + t.Run("errors-when-list-errors-subsequently", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + if prevPageLast != nil { + return nil, nil, errors.New("failed to list") + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + 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() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + 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() + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.ErrorContains(t, err, "failed to estimate count") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-listing-deleted-ids-errors", func(t *testing.T) { + t.Parallel() + origPlg := plugin.NewPlugin() + pageSize := 2 + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + assert.Nil(t, prevPageLast) + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.ErrorContains(t, err, "failed to list deleted ids") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) + t.Run("errors-when-different-plugin-returned", func(t *testing.T) { + t.Parallel() + pageSize := 2 + origPlg := plugin.NewPlugin() + origPlg.PublicId = "id1" + otherPlg := plugin.NewPlugin() + otherPlg.PublicId = "id2" + refreshToken, err := refreshtoken.New( + ctx, + time.Now(), + time.Now(), + resource.Unknown, + []byte("some hash"), + "1", + time.Now(), + ) + require.NoError(t, err) + listRefreshItemsFn := func(ctx context.Context, tok *refreshtoken.Token, prevPageLast *testType, limit int) ([]*testType, *plugin.Plugin, error) { + if prevPageLast != nil { + assert.Equal(t, "3", prevPageLast.ID) + return []*testType{{nil, "4"}}, otherPlg, nil + } + return []*testType{{nil, "1"}, {nil, "2"}, {nil, "3"}}, origPlg, 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, time.Time{}, nil + } + grantsHash := []byte("some hash") + resp, plg, err := ListPluginRefresh( + ctx, + grantsHash, + pageSize, + filterItemFn, + listRefreshItemsFn, + estimatedItemCountFn, + deletedIDsFn, + refreshToken, + ) + require.ErrorContains(t, err, "plugin changed between list invocations") + assert.Empty(t, resp) + assert.Empty(t, plg) + }) +}