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 }