github.com/openfga/openfga@v1.5.4-rc1/pkg/server/test/read_changes.go (about) 1 package test 2 3 import ( 4 "context" 5 "sync" 6 "testing" 7 8 "github.com/google/go-cmp/cmp" 9 openfgav1 "github.com/openfga/api/proto/openfga/v1" 10 "github.com/stretchr/testify/require" 11 "google.golang.org/protobuf/protoadapt" 12 "google.golang.org/protobuf/testing/protocmp" 13 "google.golang.org/protobuf/types/known/wrapperspb" 14 15 "github.com/openfga/openfga/pkg/server/commands" 16 serverErrors "github.com/openfga/openfga/pkg/server/errors" 17 "github.com/openfga/openfga/pkg/storage" 18 "github.com/openfga/openfga/pkg/testutils" 19 ) 20 21 type testCase struct { 22 _name string 23 request *openfgav1.ReadChangesRequest 24 expectedError error 25 expectedChanges []*openfgav1.TupleChange 26 expectEmptyContinuationToken bool 27 saveContinuationTokenForNextTest bool 28 } 29 30 var tkMaria = &openfgav1.TupleKey{ 31 Object: "repo:openfga/openfgapb", 32 Relation: "admin", 33 User: "maria", 34 } 35 var tkMariaOrg = &openfgav1.TupleKey{ 36 Object: "org:openfga", 37 Relation: "member", 38 User: "maria", 39 } 40 var tkCraig = &openfgav1.TupleKey{ 41 Object: "repo:openfga/openfgapb", 42 Relation: "admin", 43 User: "craig", 44 } 45 var tkYamil = &openfgav1.TupleKey{ 46 Object: "repo:openfga/openfgapb", 47 Relation: "admin", 48 User: "yamil", 49 } 50 51 func newReadChangesRequest(store, objectType, contToken string, pageSize int32) *openfgav1.ReadChangesRequest { 52 return &openfgav1.ReadChangesRequest{ 53 StoreId: store, 54 Type: objectType, 55 ContinuationToken: contToken, 56 PageSize: wrapperspb.Int32(pageSize), 57 } 58 } 59 60 func TestReadChanges(t *testing.T, datastore storage.OpenFGADatastore) { 61 store := testutils.CreateRandomString(10) 62 ctx, backend, err := writeTuples(store, datastore) 63 require.NoError(t, err) 64 65 t.Run("read_changes_without_type", func(t *testing.T) { 66 testCases := []testCase{ 67 { 68 _name: "request_with_pageSize=2_returns_2_tuple_and_a_token", 69 request: newReadChangesRequest(store, "", "", 2), 70 expectedChanges: []*openfgav1.TupleChange{ 71 { 72 TupleKey: tkMaria, 73 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 74 }, 75 { 76 TupleKey: tkCraig, 77 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 78 }, 79 }, 80 expectEmptyContinuationToken: false, 81 expectedError: nil, 82 saveContinuationTokenForNextTest: true, 83 }, 84 { 85 _name: "request_with_previous_token_returns_all_remaining_changes", 86 request: newReadChangesRequest(store, "", "", storage.DefaultPageSize), 87 expectedChanges: []*openfgav1.TupleChange{ 88 { 89 TupleKey: tkYamil, 90 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 91 }, 92 { 93 TupleKey: tkMariaOrg, 94 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 95 }, 96 }, 97 expectEmptyContinuationToken: false, 98 expectedError: nil, 99 saveContinuationTokenForNextTest: true, 100 }, 101 { 102 _name: "request_with_previous_token_returns_no_more_changes", 103 request: newReadChangesRequest(store, "", "", storage.DefaultPageSize), 104 expectedChanges: nil, 105 expectEmptyContinuationToken: false, 106 expectedError: nil, 107 saveContinuationTokenForNextTest: false, 108 }, 109 { 110 _name: "request_with_invalid_token_returns_invalid_token_error", 111 request: newReadChangesRequest(store, "", "foo", storage.DefaultPageSize), 112 expectedChanges: nil, 113 expectEmptyContinuationToken: false, 114 expectedError: serverErrors.InvalidContinuationToken, 115 saveContinuationTokenForNextTest: false, 116 }, 117 } 118 119 readChangesQuery := commands.NewReadChangesQuery(backend) 120 runTests(t, ctx, testCases, readChangesQuery) 121 }) 122 123 t.Run("read_changes_with_type", func(t *testing.T) { 124 testCases := []testCase{ 125 { 126 _name: "if_no_tuples_with_type,_return_empty_changes_and_no_token", 127 request: newReadChangesRequest(store, "type-not-found", "", 1), 128 expectedChanges: nil, 129 expectEmptyContinuationToken: true, 130 expectedError: nil, 131 }, 132 { 133 _name: "if_1_tuple_with_'org type',_read_changes_with_'org'_filter_returns_1_change_and_a_token", 134 request: newReadChangesRequest(store, "org", "", storage.DefaultPageSize), 135 expectedChanges: []*openfgav1.TupleChange{{ 136 TupleKey: tkMariaOrg, 137 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 138 }}, 139 expectEmptyContinuationToken: false, 140 expectedError: nil, 141 }, 142 { 143 _name: "if_2_tuples_with_'repo'_type,_read_changes_with_'repo'_filter and page size of 1 returns 1 change and a token", 144 request: newReadChangesRequest(store, "repo", "", 1), 145 expectedChanges: []*openfgav1.TupleChange{{ 146 TupleKey: tkMaria, 147 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 148 }}, 149 expectEmptyContinuationToken: false, 150 expectedError: nil, 151 saveContinuationTokenForNextTest: true, 152 }, { 153 _name: "using_the_token_from_the_previous_test_yields_1_change_and_a_token", 154 request: newReadChangesRequest(store, "repo", "", storage.DefaultPageSize), 155 expectedChanges: []*openfgav1.TupleChange{{ 156 TupleKey: tkCraig, 157 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 158 }, { 159 TupleKey: tkYamil, 160 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 161 }}, 162 expectEmptyContinuationToken: false, 163 expectedError: nil, 164 saveContinuationTokenForNextTest: true, 165 }, { 166 _name: "using_the_token_from_the_previous_test_yields_0_changes_and_a_token", 167 request: newReadChangesRequest(store, "repo", "", storage.DefaultPageSize), 168 expectedChanges: nil, 169 expectEmptyContinuationToken: false, 170 expectedError: nil, 171 saveContinuationTokenForNextTest: true, 172 }, { 173 _name: "using_the_token_from_the_previous_test_yields_an_error_because_the_types_in_the_token_and_the_request_don't_match", 174 request: newReadChangesRequest(store, "does-not-match", "", storage.DefaultPageSize), 175 expectedError: serverErrors.MismatchObjectType, 176 }, 177 } 178 179 readChangesQuery := commands.NewReadChangesQuery(backend) 180 runTests(t, ctx, testCases, readChangesQuery) 181 }) 182 183 t.Run("read_changes_with_horizon_offset", func(t *testing.T) { 184 testCases := []testCase{ 185 { 186 _name: "when_the_horizon_offset_is_non-zero_no_tuples_should_be_returned", 187 request: &openfgav1.ReadChangesRequest{ 188 StoreId: store, 189 }, 190 expectedChanges: nil, 191 expectEmptyContinuationToken: true, 192 expectedError: nil, 193 }, 194 } 195 196 readChangesQuery := commands.NewReadChangesQuery(backend, 197 commands.WithReadChangeQueryHorizonOffset(2), 198 ) 199 runTests(t, ctx, testCases, readChangesQuery) 200 }) 201 } 202 203 func runTests(t *testing.T, ctx context.Context, testCasesInOrder []testCase, readChangesQuery *commands.ReadChangesQuery) { //nolint:revive 204 ignoreTimestampOpts := protocmp.IgnoreFields(protoadapt.MessageV2Of(&openfgav1.TupleChange{}), "timestamp") 205 var res *openfgav1.ReadChangesResponse 206 var err error 207 for i, test := range testCasesInOrder { 208 t.Run(test._name, func(t *testing.T) { 209 if i >= 1 { 210 previousTest := testCasesInOrder[i-1] 211 if previousTest.saveContinuationTokenForNextTest { 212 previousToken := res.GetContinuationToken() 213 test.request.ContinuationToken = previousToken 214 } 215 } 216 res, err = readChangesQuery.Execute(ctx, test.request) 217 218 if test.expectedError != nil { 219 require.ErrorIs(t, err, test.expectedError) 220 } else { 221 require.NoError(t, err) 222 require.NotNil(t, res) 223 if diff := cmp.Diff(test.expectedChanges, res.GetChanges(), ignoreTimestampOpts, protocmp.Transform()); diff != "" { 224 t.Errorf("tuple change mismatch (-want +got):\n%s", diff) 225 } 226 if test.expectEmptyContinuationToken { 227 require.Empty(t, res.GetContinuationToken()) 228 } else { 229 require.NotEmpty(t, res.GetContinuationToken()) 230 } 231 } 232 }) 233 } 234 } 235 236 func TestReadChangesReturnsSameContTokenWhenNoChanges(t *testing.T, datastore storage.OpenFGADatastore) { 237 store := testutils.CreateRandomString(10) 238 ctx, backend, err := writeTuples(store, datastore) 239 require.NoError(t, err) 240 241 readChangesQuery := commands.NewReadChangesQuery(backend) 242 243 res1, err := readChangesQuery.Execute(ctx, newReadChangesRequest(store, "", "", storage.DefaultPageSize)) 244 require.NoError(t, err) 245 246 res2, err := readChangesQuery.Execute(ctx, newReadChangesRequest(store, "", res1.GetContinuationToken(), storage.DefaultPageSize)) 247 require.NoError(t, err) 248 249 require.Equal(t, res1.GetContinuationToken(), res2.GetContinuationToken()) 250 } 251 252 func TestReadChangesAfterConcurrentWritesReturnsUniqueResults(t *testing.T, datastore storage.OpenFGADatastore) { 253 store := testutils.CreateRandomString(10) 254 255 tuplesToWriteOne := []*openfgav1.TupleKey{tkMaria, tkCraig} 256 tuplesToWriteTwo := []*openfgav1.TupleKey{tkYamil} 257 totalTuplesToWrite := len(tuplesToWriteOne) + len(tuplesToWriteTwo) 258 ctx, backend := writeTuplesConcurrently(t, store, datastore, tuplesToWriteOne, tuplesToWriteTwo) 259 260 readChangesQuery := commands.NewReadChangesQuery(backend) 261 262 // without type 263 res1, err := readChangesQuery.Execute(ctx, newReadChangesRequest(store, "", "", storage.DefaultPageSize)) 264 require.NoError(t, err) 265 require.Len(t, res1.GetChanges(), totalTuplesToWrite) 266 267 // with type 268 res2, err := readChangesQuery.Execute(ctx, newReadChangesRequest(store, "repo", "", storage.DefaultPageSize)) 269 require.NoError(t, err) 270 require.Len(t, res2.GetChanges(), totalTuplesToWrite) 271 } 272 273 func writeTuples(store string, datastore storage.OpenFGADatastore) (context.Context, storage.ChangelogBackend, error) { 274 ctx := context.Background() 275 276 writes := []*openfgav1.TupleKey{tkMaria, tkCraig, tkYamil, tkMariaOrg} 277 err := datastore.Write( 278 ctx, 279 store, 280 []*openfgav1.TupleKeyWithoutCondition{}, 281 writes, 282 ) 283 if err != nil { 284 return nil, nil, err 285 } 286 287 return ctx, datastore, nil 288 } 289 290 // writeTuplesConcurrently writes two groups of tuples concurrently to expose potential race issues when reading changes. 291 func writeTuplesConcurrently(t *testing.T, store string, datastore storage.OpenFGADatastore, tupleGroupOne, tupleGroupTwo []*openfgav1.TupleKey) (context.Context, storage.ChangelogBackend) { 292 t.Helper() 293 ctx := context.Background() 294 295 var wg sync.WaitGroup 296 wg.Add(2) 297 298 go func() { 299 err := datastore.Write( 300 ctx, 301 store, 302 []*openfgav1.TupleKeyWithoutCondition{}, 303 tupleGroupOne, 304 ) 305 if err != nil { 306 t.Logf("failed to write tuples: %s", err) 307 } 308 wg.Done() 309 }() 310 311 go func() { 312 err := datastore.Write( 313 ctx, 314 store, 315 []*openfgav1.TupleKeyWithoutCondition{}, 316 tupleGroupTwo, 317 ) 318 if err != nil { 319 t.Logf("failed to write tuples: %s", err) 320 } 321 wg.Done() 322 }() 323 324 wg.Wait() 325 326 return ctx, datastore 327 }