diff --git a/internal/pagination/pagination_plugins.go b/internal/pagination/pagination_plugins.go new file mode 100644 index 0000000000..a53c22b44f --- /dev/null +++ b/internal/pagination/pagination_plugins.go @@ -0,0 +1,306 @@ +// 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" +) + +// ListPluginsFilterFunc 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 ListPluginsFilterFunc[T boundary.Resource] func(ctx context.Context, item T, plugin map[string]*plugin.Plugin) (bool, error) + +// ListPluginsItemsFunc 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 ListPluginsItemsFunc[T boundary.Resource] func(ctx context.Context, prevPageLastItem T, limit int) ([]T, []*plugin.Plugin, time.Time, error) + +// ListPlugins returns a ListResponse and a map of plugin id to the plugins associated +// with the returned 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 ListPlugins[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn ListPluginsFilterFunc[T], + listItemsFn ListPluginsItemsFunc[T], + estimatedCountFn EstimatedCountFunc, +) (*ListResponse[T], map[string]*plugin.Plugin, error) { + const op = "pagination.ListsPlugin" + + 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, plgs, completeListing, listTime, err := listPlugins(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, plgs, nil +} + +// ListPluginsPage returns a ListResponse and a map of plugin id to the plugins associated +// with the returned 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 ListPluginsPage[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn ListPluginsFilterFunc[T], + listItemsFn ListPluginsItemsFunc[T], + estimatedCountFn EstimatedCountFunc, + tok *listtoken.Token, +) (*ListResponse[T], map[string]*plugin.Plugin, error) { + const op = "pagination.ListPluginsPage" + + 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, plgs, completeListing, listTime, err := listPlugins(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, plgs, nil +} + +// ListPluginsRefresh returns a ListResponse and a map of plugin id to the plugins associated +// with the returned 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 ListPluginsRefresh[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn ListPluginsFilterFunc[T], + listItemsFn ListPluginsItemsFunc[T], + estimatedCountFn EstimatedCountFunc, + listDeletedIDsFn ListDeletedIDsFunc, + tok *listtoken.Token, +) (*ListResponse[T], map[string]*plugin.Plugin, error) { + const op = "pagination.ListPluginsRefresh" + + 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, plgs, completeListing, listTime, err := listPlugins(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, plgs, nil +} + +// ListPluginsRefreshPage returns a ListResponse and a plugin. 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 ListPluginsRefreshPage[T boundary.Resource]( + ctx context.Context, + grantsHash []byte, + pageSize int, + filterItemFn ListPluginsFilterFunc[T], + listItemsFn ListPluginsItemsFunc[T], + estimatedCountFn EstimatedCountFunc, + listDeletedIDsFn ListDeletedIDsFunc, + tok *listtoken.Token, +) (*ListResponse[T], map[string]*plugin.Plugin, error) { + const op = "pagination.ListPluginsRefreshPage" + + 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, plgs, completeListing, listTime, err := listPlugins(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, plgs, nil +} + +func listPlugins[T boundary.Resource]( + ctx context.Context, + pageSize int, + filterItemFn ListPluginsFilterFunc[T], + listItemsFn ListPluginsItemsFunc[T], +) ([]T, map[string]*plugin.Plugin, bool, time.Time, error) { + const op = "pagination.list" + + var lastItem T + plgs := map[string]*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, newPlgs, 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 + } + for _, plg := range newPlgs { + if _, ok := plgs[plg.PublicId]; !ok { + plgs[plg.PublicId] = plg + } + } + for _, item := range page { + ok, err := filterItemFn(ctx, item, plgs) + 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, plgs, completeListing, firstListTime, nil +} diff --git a/internal/pagination/pagination_plugins_test.go b/internal/pagination/pagination_plugins_test.go new file mode 100644 index 0000000000..7a6a10d10e --- /dev/null +++ b/internal/pagination/pagination_plugins_test.go @@ -0,0 +1,3879 @@ +// 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_ListPlugins(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte(nil) + _, _, err := ListPlugins(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPlugins(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPlugins(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 := ListPluginsFilterFunc[*testType](nil) + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPlugins(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 := ListPluginsItemsFunc[*testType](nil) + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPlugins(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := EstimatedCountFunc(nil) + grantsHash := []byte("some hash") + _, _, err := ListPlugins(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plgs, err := ListPlugins(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + t.Run("errors-when-list-errors-subsequently", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPlugins(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + t.Run("errors-when-filter-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPlugins(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "failed to filter") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + t.Run("errors-when-estimated-count-errors", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPlugins(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn) + require.ErrorContains(t, err, "failed to estimate count") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + }) + t.Run("no-rows", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, []*plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plgs, err := ListPlugins(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, plgsMap, plgs) + }) + t.Run("fill-on-first-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plgs, err := ListPlugins(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, plgsMap, plgs) + }) + t.Run("fill-on-first-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plgs, err := ListPlugins(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, plgsMap, plgs) + }) + t.Run("fill-on-subsequent-with-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPlugins(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, plgsMap, plgs) + }) + t.Run("fill-on-subsequent-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPlugins(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, plgsMap, plgs) + }) + t.Run("fill-on-subsequent", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPlugins(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, plgsMap, plgs) + }) + t.Run("dont-fill-without-remaining", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPlugins(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, plgsMap, plgs) + }) + t.Run("dont-fill-with-full-last-page", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, 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)}, + }, origPlgs, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlgs, 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, plgs map[string]*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, plgs, err := ListPlugins(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, plgsMap, plgs) + }) + t.Run("filter-everything", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, 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)}, + }, origPlgs, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlgs, 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, plgs map[string]*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, plgs, err := ListPlugins(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, plgsMap, plgs) + }) + t.Run("appends-and-deduplicates-plugins-between-invocation", func(t *testing.T) { + t.Parallel() + pageSize := 2 + plg1 := plugin.NewPlugin() + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin() + plg2.PublicId = "id2" + plg3 := plugin.NewPlugin() + plg3.PublicId = "id3" + origPlgs := []*plugin.Plugin{plg1, plg2} + otherPlgs := []*plugin.Plugin{plg2, plg3} + 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)}, + }, otherPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPlugins(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, + map[string]*plugin.Plugin{plg1.PublicId: plg1, plg2.PublicId: plg2, plg3.PublicId: plg3}, + plgs, + ) + }) +} + +func Test_ListPluginsPage(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte(nil) + _, _, err = ListPluginsPage(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginsPage(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginsPage(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 := ListPluginsFilterFunc[*testType](nil) + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginsPage(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 := ListPluginsItemsFunc[*testType](nil) + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginsPage(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err := ListPluginsPage(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + _, _, err = ListPluginsPage(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := EstimatedCountFunc(nil) + grantsHash := []byte("some hash") + _, _, err = ListPluginsPage(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, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plgs, err := ListPluginsPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "failed to filter") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, tok) + require.ErrorContains(t, err, "failed to estimate count") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, []*plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plgs, err := ListPluginsPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plgs, err := ListPluginsPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*plugin.Plugin) (bool, error) { + return true, nil + } + estimatedItemCountFn := func(ctx context.Context) (int, error) { + return 10, nil + } + grantsHash := []byte("some hash") + resp, plgs, err := ListPluginsPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, 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)}, + }, origPlgs, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlgs, 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, plgs map[string]*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, plgs, err := ListPluginsPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, 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)}, + }, origPlgs, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlgs, 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, plgs map[string]*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, plgs, err := ListPluginsPage(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, plgsMap, plgs) + }) + t.Run("appends-and-deduplicates-plugins-between-invocation", 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) + plg1 := plugin.NewPlugin() + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin() + plg2.PublicId = "id2" + plg3 := plugin.NewPlugin() + plg3.PublicId = "id3" + origPlgs := []*plugin.Plugin{plg1, plg2} + otherPlgs := []*plugin.Plugin{plg2, plg3} + 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)}, + }, otherPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsPage(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, + map[string]*plugin.Plugin{plg1.PublicId: plg1, plg2.PublicId: plg2, plg3.PublicId: plg3}, + plgs, + ) + }) +} + +func Test_ListPluginsRefresh(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, plgs map[string]*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 = ListPluginsRefresh(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, plgs map[string]*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 = ListPluginsRefresh(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, plgs map[string]*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 = ListPluginsRefresh(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 := ListPluginsFilterFunc[*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 = ListPluginsRefresh(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 := ListPluginsItemsFunc[*testType](nil) + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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 = ListPluginsRefresh(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, plgs map[string]*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 = ListPluginsRefresh(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, plgs map[string]*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 := ListPluginsRefresh(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, plgs map[string]*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 = ListPluginsRefresh(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, plgs map[string]*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 = ListPluginsRefresh(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, plgs map[string]*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, plgs, err := ListPluginsRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to filter") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to estimate count") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list deleted IDs") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, []*plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, 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)}, + }, origPlgs, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlgs, 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, plgs map[string]*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, plgs, err := ListPluginsRefresh(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, 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)}, + }, origPlgs, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlgs, 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, plgs map[string]*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, plgs, err := ListPluginsRefresh(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, plgsMap, plgs) + }) + t.Run("appends-and-deduplicates-plugins-between-invocation", 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) + plg1 := plugin.NewPlugin() + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin() + plg2.PublicId = "id2" + plg3 := plugin.NewPlugin() + plg3.PublicId = "id3" + origPlgs := []*plugin.Plugin{plg1, plg2} + otherPlgs := []*plugin.Plugin{plg2, plg3} + 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)}, + }, otherPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefresh(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, + map[string]*plugin.Plugin{plg1.PublicId: plg1, plg2.PublicId: plg2, plg3.PublicId: plg3}, + plgs, + ) + }) +} + +func Test_ListPluginsRefreshPage(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, plgs map[string]*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 = ListPluginsRefreshPage(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, plgs map[string]*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 = ListPluginsRefreshPage(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, plgs map[string]*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 = ListPluginsRefreshPage(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 := ListPluginsFilterFunc[*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 = ListPluginsRefreshPage(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 := ListPluginsItemsFunc[*testType](nil) + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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 = ListPluginsRefreshPage(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, plgs map[string]*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 = ListPluginsRefreshPage(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, plgs map[string]*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 := ListPluginsRefreshPage(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, plgs map[string]*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 = ListPluginsRefreshPage(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, plgs map[string]*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 = ListPluginsRefreshPage(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, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to filter") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to estimate count") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listItemsFn, estimatedItemCountFn, deletedIDsFn, tok) + require.ErrorContains(t, err, "failed to list deleted IDs") + assert.Empty(t, resp) + assert.Empty(t, plgs) + }) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + listItemsFn := func(ctx context.Context, prevPageLast *testType, limit int) ([]*testType, []*plugin.Plugin, time.Time, error) { + assert.Nil(t, prevPageLast) + return nil, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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)}, + }, origPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, 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)}, + }, origPlgs, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlgs, 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, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(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, plgsMap, plgs) + }) + 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) + plg1 := plugin.NewPlugin(plugin.WithName("plugin-1")) + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin(plugin.WithName("plugin-2")) + plg2.PublicId = "id2" + plgsMap := map[string]*plugin.Plugin{ + plg1.PublicId: plg1, + plg2.PublicId: plg2, + } + origPlgs := []*plugin.Plugin{plg1, plg2} + 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}, + }, origPlgs, 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)}, + }, origPlgs, listReturnTime.Add(time.Second), nil + case prevPageLast.ID == "6": + return nil, origPlgs, 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, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(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, plgsMap, plgs) + }) + t.Run("appends-and-deduplicates-plugins-between-invocation", 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) + plg1 := plugin.NewPlugin() + plg1.PublicId = "id1" + plg2 := plugin.NewPlugin() + plg2.PublicId = "id2" + plg3 := plugin.NewPlugin() + plg3.PublicId = "id3" + origPlgs := []*plugin.Plugin{plg1, plg2} + otherPlgs := []*plugin.Plugin{plg2, plg3} + 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)}, + }, otherPlgs, 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}, + }, origPlgs, listReturnTime, nil + } + filterItemFn := func(ctx context.Context, item *testType, plgs map[string]*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, plgs, err := ListPluginsRefreshPage(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, + map[string]*plugin.Plugin{plg1.PublicId: plg1, plg2.PublicId: plg2, plg3.PublicId: plg3}, + plgs, + ) + }) +}