github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/datastore/pagination/iterator_test.go (about)

     1  package pagination
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strconv"
     8  	"testing"
     9  
    10  	"github.com/stretchr/testify/mock"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/authzed/spicedb/internal/datastore/common"
    14  	"github.com/authzed/spicedb/pkg/datastore"
    15  	"github.com/authzed/spicedb/pkg/datastore/options"
    16  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    17  )
    18  
    19  func TestDownstreamErrors(t *testing.T) {
    20  	defaultPageSize := uint64(10)
    21  	defaultSortOrder := options.ByResource
    22  	defaultError := errors.New("something went wrong")
    23  	ctx := context.Background()
    24  	var nilIter *mockedIterator
    25  	var nilRel *core.RelationTuple
    26  
    27  	t.Run("OnReader", func(t *testing.T) {
    28  		require := require.New(t)
    29  		ds := &mockedReader{}
    30  		ds.
    31  			On("QueryRelationships", options.Cursor(nil), defaultSortOrder, defaultPageSize).
    32  			Return(nilIter, defaultError)
    33  
    34  		_, err := NewPaginatedIterator(ctx, ds, datastore.RelationshipsFilter{}, defaultPageSize, defaultSortOrder, nil)
    35  		require.ErrorIs(err, defaultError)
    36  		require.True(ds.AssertExpectations(t))
    37  	})
    38  
    39  	t.Run("OnIterator", func(t *testing.T) {
    40  		require := require.New(t)
    41  
    42  		iterMock := &mockedIterator{}
    43  		iterMock.On("Next").Return(nilRel)
    44  		iterMock.On("Err").Return(defaultError)
    45  		iterMock.On("Close")
    46  
    47  		ds := &mockedReader{}
    48  		ds.
    49  			On("QueryRelationships", options.Cursor(nil), defaultSortOrder, defaultPageSize).
    50  			Return(iterMock, nil)
    51  
    52  		iter, err := NewPaginatedIterator(ctx, ds, datastore.RelationshipsFilter{}, defaultPageSize, defaultSortOrder, nil)
    53  		require.NoError(err)
    54  		require.NotNil(iter)
    55  
    56  		require.Nil(iter.Next())
    57  		require.ErrorIs(iter.Err(), defaultError)
    58  
    59  		iter.Close()
    60  		require.Nil(iter.Next())
    61  		require.ErrorIs(iter.Err(), datastore.ErrClosedIterator)
    62  
    63  		require.True(iterMock.AssertExpectations(t))
    64  		require.True(ds.AssertExpectations(t))
    65  	})
    66  
    67  	t.Run("OnIterator", func(t *testing.T) {
    68  		require := require.New(t)
    69  
    70  		iterMock := &mockedIterator{}
    71  		iterMock.On("Next").Return(&core.RelationTuple{})
    72  		iterMock.On("Cursor").Return(options.Cursor(nil), defaultError)
    73  		iterMock.On("Close")
    74  
    75  		ds := &mockedReader{}
    76  		ds.
    77  			On("QueryRelationships", options.Cursor(nil), defaultSortOrder, defaultPageSize).
    78  			Return(iterMock, nil)
    79  
    80  		iter, err := NewPaginatedIterator(ctx, ds, datastore.RelationshipsFilter{}, defaultPageSize, defaultSortOrder, nil)
    81  		require.NoError(err)
    82  		require.NotNil(iter)
    83  
    84  		require.NotNil(iter.Next())
    85  		require.NoError(iter.Err())
    86  
    87  		cursor, err := iter.Cursor()
    88  		require.Nil(cursor)
    89  		require.ErrorIs(err, defaultError)
    90  
    91  		iter.Close()
    92  		require.Nil(iter.Next())
    93  		require.ErrorIs(iter.Err(), datastore.ErrClosedIterator)
    94  
    95  		require.True(iterMock.AssertExpectations(t))
    96  		require.True(ds.AssertExpectations(t))
    97  	})
    98  
    99  	t.Run("OnReaderAfterSuccess", func(t *testing.T) {
   100  		require := require.New(t)
   101  
   102  		iterMock := &mockedIterator{}
   103  		iterMock.On("Next").Return(&core.RelationTuple{}).Once()
   104  		iterMock.On("Next").Return(nil).Once()
   105  		iterMock.On("Err").Return(nil).Once()
   106  		iterMock.On("Cursor").Return(options.Cursor(nil), nil).Once()
   107  		iterMock.On("Close")
   108  
   109  		ds := &mockedReader{}
   110  		ds.
   111  			On("QueryRelationships", options.Cursor(nil), defaultSortOrder, uint64(1)).
   112  			Return(iterMock, nil).Once().
   113  			On("QueryRelationships", options.Cursor(nil), defaultSortOrder, uint64(1)).
   114  			Return(nil, defaultError).Once()
   115  
   116  		iter, err := NewPaginatedIterator(ctx, ds, datastore.RelationshipsFilter{}, 1, defaultSortOrder, nil)
   117  		require.NoError(err)
   118  		require.NotNil(iter)
   119  
   120  		require.NotNil(iter.Next())
   121  		require.NoError(iter.Err())
   122  
   123  		require.Nil(iter.Next())
   124  		require.Error(iter.Err())
   125  		iter.Close()
   126  	})
   127  }
   128  
   129  func TestPaginatedIterator(t *testing.T) {
   130  	testCases := []struct {
   131  		order              options.SortOrder
   132  		pageSize           uint64
   133  		totalRelationships uint64
   134  	}{
   135  		{options.ByResource, 1, 0},
   136  		{options.ByResource, 1, 1},
   137  		{options.ByResource, 1, 10},
   138  		{options.ByResource, 10, 10},
   139  		{options.ByResource, 100, 10},
   140  		{options.ByResource, 10, 1000},
   141  		{options.ByResource, 9, 20},
   142  	}
   143  
   144  	for _, tc := range testCases {
   145  		t.Run(fmt.Sprintf("%d/%d-%d", tc.pageSize, tc.totalRelationships, tc.order), func(t *testing.T) {
   146  			require := require.New(t)
   147  
   148  			tpls := make([]*core.RelationTuple, 0, tc.totalRelationships)
   149  			for i := 0; i < int(tc.totalRelationships); i++ {
   150  				tpls = append(tpls, &core.RelationTuple{
   151  					ResourceAndRelation: &core.ObjectAndRelation{
   152  						Namespace: "document",
   153  						ObjectId:  strconv.Itoa(i),
   154  						Relation:  "owner",
   155  					},
   156  					Subject: &core.ObjectAndRelation{
   157  						Namespace: "user",
   158  						ObjectId:  strconv.Itoa(i),
   159  						Relation:  datastore.Ellipsis,
   160  					},
   161  				})
   162  			}
   163  
   164  			ds := generateMock(tpls, tc.pageSize, options.ByResource)
   165  
   166  			ctx := context.Background()
   167  			iter, err := NewPaginatedIterator(ctx, ds, datastore.RelationshipsFilter{
   168  				OptionalResourceType: "unused",
   169  			}, tc.pageSize, options.ByResource, nil)
   170  			require.NoError(err)
   171  			defer iter.Close()
   172  
   173  			cursor, err := iter.Cursor()
   174  			require.ErrorIs(err, datastore.ErrCursorEmpty)
   175  			require.Nil(cursor)
   176  
   177  			var count uint64
   178  			for tpl := iter.Next(); tpl != nil; tpl = iter.Next() {
   179  				count++
   180  				require.NoError(iter.Err())
   181  
   182  				cursor, err := iter.Cursor()
   183  				require.NoError(err)
   184  				require.NotNil(cursor)
   185  			}
   186  
   187  			require.Equal(tc.totalRelationships, count)
   188  
   189  			require.NoError(iter.Err())
   190  
   191  			iter.Close()
   192  			require.Nil(iter.Next())
   193  			require.Error(iter.Err(), datastore.ErrClosedIterator)
   194  
   195  			// Make sure everything got called
   196  			require.True(ds.AssertExpectations(t))
   197  		})
   198  	}
   199  }
   200  
   201  func generateMock(tpls []*core.RelationTuple, pageSize uint64, order options.SortOrder) *mockedReader {
   202  	mock := &mockedReader{}
   203  	tplsLen := uint64(len(tpls))
   204  
   205  	var last options.Cursor
   206  	for i := uint64(0); i <= tplsLen; i += pageSize {
   207  		pastLastIndex := i + pageSize
   208  		if pastLastIndex > tplsLen {
   209  			pastLastIndex = tplsLen
   210  		}
   211  
   212  		iter := common.NewSliceRelationshipIterator(tpls[i:pastLastIndex], order)
   213  		mock.On("QueryRelationships", last, order, pageSize).Return(iter, nil)
   214  		if tplsLen > 0 {
   215  			last = tpls[pastLastIndex-1]
   216  		}
   217  	}
   218  
   219  	return mock
   220  }
   221  
   222  type mockedReader struct {
   223  	mock.Mock
   224  }
   225  
   226  var _ datastore.Reader = &mockedReader{}
   227  
   228  func (m *mockedReader) QueryRelationships(
   229  	_ context.Context,
   230  	_ datastore.RelationshipsFilter,
   231  	opts ...options.QueryOptionsOption,
   232  ) (datastore.RelationshipIterator, error) {
   233  	queryOpts := options.NewQueryOptionsWithOptions(opts...)
   234  	args := m.Called(queryOpts.After, queryOpts.Sort, *queryOpts.Limit)
   235  	potentialRelIter := args.Get(0)
   236  	if potentialRelIter == nil {
   237  		return nil, args.Error(1)
   238  	}
   239  	return potentialRelIter.(datastore.RelationshipIterator), args.Error(1)
   240  }
   241  
   242  func (m *mockedReader) ReverseQueryRelationships(
   243  	_ context.Context,
   244  	_ datastore.SubjectsFilter,
   245  	_ ...options.ReverseQueryOptionsOption,
   246  ) (datastore.RelationshipIterator, error) {
   247  	panic("not implemented")
   248  }
   249  
   250  func (m *mockedReader) ReadCaveatByName(_ context.Context, _ string) (caveat *core.CaveatDefinition, lastWritten datastore.Revision, err error) {
   251  	panic("not implemented")
   252  }
   253  
   254  func (m *mockedReader) ListAllCaveats(_ context.Context) ([]datastore.RevisionedCaveat, error) {
   255  	panic("not implemented")
   256  }
   257  
   258  func (m *mockedReader) LookupCaveatsWithNames(_ context.Context, _ []string) ([]datastore.RevisionedCaveat, error) {
   259  	panic("not implemented")
   260  }
   261  
   262  func (m *mockedReader) ReadNamespaceByName(_ context.Context, _ string) (ns *core.NamespaceDefinition, lastWritten datastore.Revision, err error) {
   263  	panic("not implemented")
   264  }
   265  
   266  func (m *mockedReader) ListAllNamespaces(_ context.Context) ([]datastore.RevisionedNamespace, error) {
   267  	panic("not implemented")
   268  }
   269  
   270  func (m *mockedReader) LookupNamespacesWithNames(_ context.Context, _ []string) ([]datastore.RevisionedNamespace, error) {
   271  	panic("not implemented")
   272  }
   273  
   274  type mockedIterator struct {
   275  	mock.Mock
   276  }
   277  
   278  var _ datastore.RelationshipIterator = &mockedIterator{}
   279  
   280  func (m *mockedIterator) Next() *core.RelationTuple {
   281  	args := m.Called()
   282  	potentialTuple := args.Get(0)
   283  	if potentialTuple == nil {
   284  		return nil
   285  	}
   286  	return potentialTuple.(*core.RelationTuple)
   287  }
   288  
   289  func (m *mockedIterator) Cursor() (options.Cursor, error) {
   290  	args := m.Called()
   291  	potentialCursor := args.Get(0)
   292  	if potentialCursor == nil {
   293  		return nil, args.Error(1)
   294  	}
   295  	return potentialCursor.(options.Cursor), args.Error(1)
   296  }
   297  
   298  func (m *mockedIterator) Err() error {
   299  	args := m.Called()
   300  	return args.Error(0)
   301  }
   302  
   303  func (m *mockedIterator) Close() {
   304  	m.Called()
   305  }