github.com/openfga/openfga@v1.5.4-rc1/pkg/storage/test/tuples.go (about) 1 package test 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "testing" 8 "time" 9 10 "github.com/google/go-cmp/cmp" 11 "github.com/oklog/ulid/v2" 12 openfgav1 "github.com/openfga/api/proto/openfga/v1" 13 "github.com/stretchr/testify/require" 14 "google.golang.org/protobuf/protoadapt" 15 "google.golang.org/protobuf/testing/protocmp" 16 "google.golang.org/protobuf/types/known/structpb" 17 18 "github.com/openfga/openfga/pkg/storage" 19 "github.com/openfga/openfga/pkg/testutils" 20 "github.com/openfga/openfga/pkg/tuple" 21 "github.com/openfga/openfga/pkg/typesystem" 22 ) 23 24 func ReadChangesTest(t *testing.T, datastore storage.OpenFGADatastore) { 25 ctx := context.Background() 26 27 t.Run("read_changes_with_continuation_token", func(t *testing.T) { 28 storeID := ulid.Make().String() 29 30 tk1 := &openfgav1.TupleKey{ 31 Object: tuple.BuildObject("folder", "folder1"), 32 Relation: "viewer", 33 User: "bob", 34 } 35 tk2 := &openfgav1.TupleKey{ 36 Object: tuple.BuildObject("folder", "folder2"), 37 Relation: "viewer", 38 User: "bill", 39 } 40 41 err := datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tk1, tk2}) 42 require.NoError(t, err) 43 44 changes, continuationToken, err := datastore.ReadChanges(ctx, storeID, "", storage.PaginationOptions{PageSize: 1}, 0) 45 require.NoError(t, err) 46 require.NotEmpty(t, continuationToken) 47 48 expectedChanges := []*openfgav1.TupleChange{ 49 { 50 TupleKey: tk1, 51 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 52 }, 53 } 54 55 if diff := cmp.Diff(expectedChanges, changes, cmpOpts...); diff != "" { 56 t.Fatalf("mismatch (-want +got):\n%s", diff) 57 } 58 59 changes, continuationToken, err = datastore.ReadChanges(ctx, storeID, "", storage.PaginationOptions{ 60 PageSize: 2, 61 From: string(continuationToken), 62 }, 63 0, 64 ) 65 require.NoError(t, err) 66 require.NotEmpty(t, continuationToken) 67 68 expectedChanges = []*openfgav1.TupleChange{ 69 { 70 TupleKey: tk2, 71 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 72 }, 73 } 74 if diff := cmp.Diff(expectedChanges, changes, cmpOpts...); diff != "" { 75 t.Errorf("mismatch (-want +got):\n%s", diff) 76 } 77 }) 78 79 t.Run("read_changes_with_no_changes_should_return_not_found", func(t *testing.T) { 80 storeID := ulid.Make().String() 81 82 _, _, err := datastore.ReadChanges(ctx, storeID, "", storage.PaginationOptions{PageSize: storage.DefaultPageSize}, 0) 83 require.ErrorIs(t, err, storage.ErrNotFound) 84 }) 85 86 t.Run("read_changes_with_horizon_offset_should_return_not_found_(no_changes)", func(t *testing.T) { 87 storeID := ulid.Make().String() 88 89 tk1 := &openfgav1.TupleKey{ 90 Object: tuple.BuildObject("folder", "folder1"), 91 Relation: "viewer", 92 User: "bob", 93 } 94 tk2 := &openfgav1.TupleKey{ 95 Object: tuple.BuildObject("folder", "folder2"), 96 Relation: "viewer", 97 User: "bill", 98 } 99 100 err := datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tk1, tk2}) 101 require.NoError(t, err) 102 103 _, _, err = datastore.ReadChanges(ctx, storeID, "", storage.PaginationOptions{PageSize: storage.DefaultPageSize}, 1*time.Minute) 104 require.ErrorIs(t, err, storage.ErrNotFound) 105 }) 106 107 t.Run("read_changes_with_non-empty_object_type_should_only_read_that_object_type", func(t *testing.T) { 108 storeID := ulid.Make().String() 109 110 tk1 := &openfgav1.TupleKey{ 111 Object: tuple.BuildObject("folder", "1"), 112 Relation: "viewer", 113 User: "bob", 114 } 115 tk2 := &openfgav1.TupleKey{ 116 Object: tuple.BuildObject("document", "1"), 117 Relation: "viewer", 118 User: "bill", 119 } 120 121 err := datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tk1, tk2}) 122 require.NoError(t, err) 123 124 changes, continuationToken, err := datastore.ReadChanges(ctx, storeID, "folder", storage.PaginationOptions{PageSize: storage.DefaultPageSize}, 0) 125 require.NoError(t, err) 126 require.NotEmpty(t, continuationToken) 127 128 expectedChanges := []*openfgav1.TupleChange{ 129 { 130 TupleKey: tk1, 131 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 132 }, 133 } 134 if diff := cmp.Diff(expectedChanges, changes, cmpOpts...); diff != "" { 135 t.Errorf("mismatch (-want +got):\n%s", diff) 136 } 137 }) 138 139 t.Run("read_changes_returns_deterministic_ordering_and_no_duplicates", func(t *testing.T) { 140 storeID := ulid.Make().String() 141 142 for i := 0; i < 100; i++ { 143 object := fmt.Sprintf("document:%d", i) 144 tuple := []*openfgav1.TupleKey{tuple.NewTupleKey(object, "viewer", "user:jon")} 145 err := datastore.Write(context.Background(), storeID, nil, tuple) 146 require.NoError(t, err) 147 } 148 149 seenObjects := map[string]struct{}{} 150 151 var changes []*openfgav1.TupleChange 152 var continuationToken []byte 153 var err error 154 for { 155 changes, continuationToken, err = datastore.ReadChanges(context.Background(), storeID, "", storage.PaginationOptions{ 156 PageSize: 10, 157 From: string(continuationToken), 158 }, 1*time.Millisecond) 159 if err != nil { 160 if errors.Is(err, storage.ErrNotFound) { 161 break 162 } 163 } 164 require.NoError(t, err) 165 require.NotNil(t, continuationToken) 166 167 for _, change := range changes { 168 if _, ok := seenObjects[change.GetTupleKey().GetObject()]; ok { 169 require.FailNowf(t, "duplicate changelog entry encountered", change.GetTupleKey().GetObject()) 170 } 171 172 seenObjects[change.GetTupleKey().GetObject()] = struct{}{} 173 } 174 175 if string(continuationToken) == "" { 176 break 177 } 178 } 179 }) 180 181 t.Run("read_changes_with_conditions", func(t *testing.T) { 182 storeID := ulid.Make().String() 183 184 tk1 := &openfgav1.TupleKey{ 185 Object: tuple.BuildObject("folder", "folder1"), 186 Relation: "viewer", 187 User: "bob", 188 Condition: &openfgav1.RelationshipCondition{ 189 Name: "condition", 190 }, 191 } 192 tk2 := &openfgav1.TupleKey{ 193 Object: tuple.BuildObject("folder", "folder2"), 194 Relation: "viewer", 195 User: "bill", 196 Condition: &openfgav1.RelationshipCondition{ 197 Name: "condition", 198 Context: testutils.MustNewStruct(t, map[string]interface{}{"param1": "ok"}), 199 }, 200 } 201 202 err := datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tk1, tk2}) 203 require.NoError(t, err) 204 205 changes, continuationToken, err := datastore.ReadChanges(ctx, storeID, "", storage.PaginationOptions{PageSize: storage.DefaultPageSize}, 0) 206 require.NoError(t, err) 207 require.NotEmpty(t, continuationToken) 208 209 expectedChanges := []*openfgav1.TupleChange{ 210 { 211 TupleKey: &openfgav1.TupleKey{ 212 Object: tuple.BuildObject("folder", "folder1"), 213 Relation: "viewer", 214 User: "bob", 215 Condition: &openfgav1.RelationshipCondition{ 216 Name: "condition", 217 Context: &structpb.Struct{}, 218 }, 219 }, 220 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 221 }, 222 { 223 TupleKey: tk2, 224 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 225 }, 226 } 227 228 if diff := cmp.Diff(expectedChanges, changes, cmpOpts...); diff != "" { 229 t.Fatalf("mismatch (-want +got):\n%s", diff) 230 } 231 }) 232 233 t.Run("tuple_with_condition_deleted", func(t *testing.T) { 234 storeID := ulid.Make().String() 235 236 tk1 := &openfgav1.TupleKey{ 237 Object: tuple.BuildObject("document", "1"), 238 Relation: "viewer", 239 User: "user:jon", 240 Condition: &openfgav1.RelationshipCondition{ 241 Name: "mycond", 242 Context: testutils.MustNewStruct(t, map[string]interface{}{ 243 "x": 10, 244 }), 245 }, 246 } 247 err := datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tk1}) 248 require.NoError(t, err) 249 250 tk2 := &openfgav1.TupleKeyWithoutCondition{ 251 Object: tuple.BuildObject("document", "1"), 252 Relation: "viewer", 253 User: "user:jon", 254 } 255 256 err = datastore.Write(ctx, storeID, []*openfgav1.TupleKeyWithoutCondition{tk2}, nil) 257 require.NoError(t, err) 258 259 changes, continuationToken, err := datastore.ReadChanges(ctx, storeID, "", storage.PaginationOptions{PageSize: storage.DefaultPageSize}, 0) 260 require.NoError(t, err) 261 require.NotEmpty(t, continuationToken) 262 263 expectedChanges := []*openfgav1.TupleChange{ 264 { 265 TupleKey: tk1, 266 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_WRITE, 267 }, 268 { 269 // Tuples with a condition that are deleted don't include the condition info 270 // in the changelog entry. 271 TupleKey: tuple.NewTupleKey("document:1", "viewer", "user:jon"), 272 Operation: openfgav1.TupleOperation_TUPLE_OPERATION_DELETE, 273 }, 274 } 275 276 if diff := cmp.Diff(expectedChanges, changes, cmpOpts...); diff != "" { 277 t.Fatalf("mismatch (-want +got):\n%s", diff) 278 } 279 }) 280 } 281 282 func TupleWritingAndReadingTest(t *testing.T, datastore storage.OpenFGADatastore) { 283 ctx := context.Background() 284 285 t.Run("deletes_would_succeed_and_write_would_fail,_fails_and_introduces_no_changes", func(t *testing.T) { 286 storeID := ulid.Make().String() 287 tks := []*openfgav1.TupleKey{ 288 { 289 Object: "doc:readme", 290 Relation: "owner", 291 User: "org:openfga#member", 292 }, 293 { 294 Object: "doc:readme", 295 Relation: "owner", 296 User: "domain:iam#member", 297 }, 298 { 299 Object: "doc:readme", 300 Relation: "viewer", 301 User: "org:openfgapb#viewer", 302 }, 303 } 304 expectedError := storage.InvalidWriteInputError(tks[2], openfgav1.TupleOperation_TUPLE_OPERATION_WRITE) 305 306 // Write tks. 307 err := datastore.Write(ctx, storeID, nil, tks) 308 require.NoError(t, err) 309 310 // Try to delete tks[0,1], and at the same time write tks[2]. It should fail with expectedError. 311 err = datastore.Write( 312 ctx, 313 storeID, 314 []*openfgav1.TupleKeyWithoutCondition{ 315 tuple.TupleKeyToTupleKeyWithoutCondition(tks[0]), 316 tuple.TupleKeyToTupleKeyWithoutCondition(tks[1]), 317 }, 318 []*openfgav1.TupleKey{tks[2]}, 319 ) 320 require.EqualError(t, err, expectedError.Error()) 321 322 tuples, _, err := datastore.ReadPage(ctx, storeID, nil, storage.PaginationOptions{PageSize: 50}) 323 require.NoError(t, err) 324 require.Equal(t, len(tks), len(tuples)) 325 }) 326 327 t.Run("delete_fails_if_the_tuple_does_not_exist", func(t *testing.T) { 328 storeID := ulid.Make().String() 329 tk := &openfgav1.TupleKey{Object: "doc:readme", Relation: "owner", User: "10"} 330 331 err := datastore.Write( 332 ctx, 333 storeID, 334 []*openfgav1.TupleKeyWithoutCondition{ 335 tuple.TupleKeyToTupleKeyWithoutCondition(tk), 336 }, 337 nil, 338 ) 339 require.ErrorContains(t, err, "cannot delete a tuple which does not exist") 340 }) 341 342 t.Run("deleting_a_tuple_which_exists_succeeds", func(t *testing.T) { 343 storeID := ulid.Make().String() 344 tk := &openfgav1.TupleKey{Object: "doc:readme", Relation: "owner", User: "10"} 345 346 // Write. 347 err := datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tk}) 348 require.NoError(t, err) 349 350 // Then delete. 351 err = datastore.Write( 352 ctx, 353 storeID, 354 []*openfgav1.TupleKeyWithoutCondition{ 355 tuple.TupleKeyToTupleKeyWithoutCondition(tk), 356 }, 357 nil, 358 ) 359 require.NoError(t, err) 360 361 // Ensure it is not there. 362 _, err = datastore.ReadUserTuple(ctx, storeID, tk) 363 require.ErrorIs(t, err, storage.ErrNotFound) 364 }) 365 366 t.Run("inserting_a_tuple_twice_fails", func(t *testing.T) { 367 storeID := ulid.Make().String() 368 tk := &openfgav1.TupleKey{Object: "doc:readme", Relation: "owner", User: "10"} 369 expectedError := storage.InvalidWriteInputError(tk, openfgav1.TupleOperation_TUPLE_OPERATION_WRITE) 370 371 // First write should succeed. 372 err := datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tk}) 373 require.NoError(t, err) 374 375 // Second write of the same tuple should fail. 376 err = datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tk}) 377 require.EqualError(t, err, expectedError.Error()) 378 }) 379 380 t.Run("inserting_a_tuple_twice_either_conditioned_or_not_fails", func(t *testing.T) { 381 storeID := ulid.Make().String() 382 tk := &openfgav1.TupleKey{Object: "doc:readme", Relation: "owner", User: "10"} 383 expectedError := storage.InvalidWriteInputError(tk, openfgav1.TupleOperation_TUPLE_OPERATION_WRITE) 384 385 // First write should succeed. 386 err := datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tk}) 387 require.NoError(t, err) 388 389 // Second write of the same tuple but conditioned should still fail. 390 err = datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{ 391 { 392 Object: tk.GetObject(), 393 Relation: tk.GetRelation(), 394 User: tk.GetUser(), 395 Condition: &openfgav1.RelationshipCondition{ 396 Name: "condition", 397 }, 398 }, 399 }) 400 require.EqualError(t, err, expectedError.Error()) 401 }) 402 403 t.Run("inserting_conditioned_tuple_and_deleting_tuple_succeeds", func(t *testing.T) { 404 storeID := ulid.Make().String() 405 tk := &openfgav1.TupleKey{Object: "doc:readme", Relation: "owner", User: "10"} 406 407 writes := []*openfgav1.TupleKey{ 408 { 409 Object: tk.GetObject(), 410 Relation: tk.GetRelation(), 411 User: tk.GetUser(), 412 Condition: &openfgav1.RelationshipCondition{ 413 Name: "condition", 414 }, 415 }, 416 } 417 418 deletes := []*openfgav1.TupleKeyWithoutCondition{ 419 { 420 Object: tk.GetObject(), 421 Relation: tk.GetRelation(), 422 User: tk.GetUser(), 423 }, 424 } 425 426 err := datastore.Write(ctx, storeID, nil, writes) 427 require.NoError(t, err) 428 429 err = datastore.Write(ctx, storeID, deletes, nil) 430 require.NoError(t, err) 431 }) 432 433 t.Run("reading_a_tuple_that_exists_succeeds", func(t *testing.T) { 434 storeID := ulid.Make().String() 435 tuple1 := tuple.NewTupleKey("doc:readme", "owner", "user:jon") 436 tuple2 := tuple.NewTupleKey("doc:readme", "viewer", "doc:other#viewer") 437 tuple3 := tuple.NewTupleKey("doc:readme", "viewer", "user:*") 438 tuple4 := &openfgav1.TupleKey{ 439 Object: "doc:readme", 440 Relation: "viewer", 441 User: "user:anne", 442 Condition: &openfgav1.RelationshipCondition{ 443 Name: "condition", 444 Context: &structpb.Struct{}, 445 }, 446 } 447 448 err := datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tuple1, tuple2, tuple3, tuple4}) 449 require.NoError(t, err) 450 451 gotTuple, err := datastore.ReadUserTuple(ctx, storeID, tuple1) 452 require.NoError(t, err) 453 454 if diff := cmp.Diff(tuple1, gotTuple.GetKey(), cmpOpts...); diff != "" { 455 require.FailNowf(t, "mismatch (-want +got):\n%s", diff) 456 } 457 458 gotTuple, err = datastore.ReadUserTuple(ctx, storeID, tuple2) 459 require.NoError(t, err) 460 461 if diff := cmp.Diff(tuple2, gotTuple.GetKey(), cmpOpts...); diff != "" { 462 require.FailNowf(t, "mismatch (-want +got):\n%s", diff) 463 } 464 465 gotTuple, err = datastore.ReadUserTuple(ctx, storeID, tuple3) 466 require.NoError(t, err) 467 468 if diff := cmp.Diff(tuple3, gotTuple.GetKey(), cmpOpts...); diff != "" { 469 require.FailNowf(t, "mismatch (-want +got):\n%s", diff) 470 } 471 472 gotTuple, err = datastore.ReadUserTuple(ctx, storeID, tuple4) 473 require.NoError(t, err) 474 475 if diff := cmp.Diff(tuple4, gotTuple.GetKey(), cmpOpts...); diff != "" { 476 require.FailNowf(t, "mismatch (-want +got):\n%s", diff) 477 } 478 }) 479 480 t.Run("reading_a_tuple_that_does_not_exist_returns_not_found", func(t *testing.T) { 481 storeID := ulid.Make().String() 482 tk := &openfgav1.TupleKey{Object: "doc:readme", Relation: "owner", User: "10"} 483 484 _, err := datastore.ReadUserTuple(ctx, storeID, tk) 485 require.ErrorIs(t, err, storage.ErrNotFound) 486 }) 487 488 t.Run("reading_userset_tuples_that_exists_succeeds", func(t *testing.T) { 489 storeID := ulid.Make().String() 490 tks := []*openfgav1.TupleKey{ 491 { 492 Object: "doc:readme", 493 Relation: "owner", 494 User: "org:openfga#member", 495 }, 496 { 497 Object: "doc:readme", 498 Relation: "owner", 499 User: "domain:iam#member", 500 }, 501 { 502 Object: "doc:readme", 503 Relation: "owner", 504 User: "user:*", 505 }, 506 { 507 Object: "doc:readme", 508 Relation: "viewer", 509 User: "org:openfgapb#viewer", 510 }, 511 } 512 513 err := datastore.Write(ctx, storeID, nil, tks) 514 require.NoError(t, err) 515 516 gotTuples, err := datastore.ReadUsersetTuples(ctx, storeID, storage.ReadUsersetTuplesFilter{ 517 Object: "doc:readme", 518 Relation: "owner", 519 }) 520 require.NoError(t, err) 521 522 iter := storage.NewTupleKeyIteratorFromTupleIterator(gotTuples) 523 defer iter.Stop() 524 525 var gotTupleKeys []*openfgav1.TupleKey 526 for { 527 tk, err := iter.Next(ctx) 528 if err != nil { 529 if errors.Is(err, storage.ErrIteratorDone) { 530 break 531 } 532 533 require.Fail(t, "unexpected error encountered") 534 } 535 536 gotTupleKeys = append(gotTupleKeys, tk) 537 } 538 539 // Then the iterator should run out. 540 _, err = gotTuples.Next(ctx) 541 require.ErrorIs(t, err, storage.ErrIteratorDone) 542 543 require.Len(t, gotTupleKeys, 3) 544 545 if diff := cmp.Diff(tks[:3], gotTupleKeys, cmpOpts...); diff != "" { 546 require.FailNowf(t, "mismatch (-want +got):\n%s", diff) 547 } 548 }) 549 550 t.Run("reading_userset_tuples_that_don't_exist_should_an_empty_iterator", func(t *testing.T) { 551 storeID := ulid.Make().String() 552 553 gotTuples, err := datastore.ReadUsersetTuples(ctx, storeID, storage.ReadUsersetTuplesFilter{Object: "doc:readme", Relation: "owner"}) 554 require.NoError(t, err) 555 defer gotTuples.Stop() 556 557 _, err = gotTuples.Next(ctx) 558 require.ErrorIs(t, err, storage.ErrIteratorDone) 559 }) 560 561 t.Run("reading_userset_tuples_with_filter_made_of_direct_relation_reference", func(t *testing.T) { 562 storeID := ulid.Make().String() 563 tks := []*openfgav1.TupleKey{ 564 tuple.NewTupleKey("document:1", "viewer", "user:*"), 565 tuple.NewTupleKey("document:1", "viewer", "users:*"), 566 tuple.NewTupleKey("document:1", "viewer", "group:eng#member"), 567 tuple.NewTupleKey("document:1", "viewer", "grouping:eng#member"), 568 } 569 570 err := datastore.Write(ctx, storeID, nil, tks) 571 require.NoError(t, err) 572 573 gotTuples, err := datastore.ReadUsersetTuples(ctx, storeID, storage.ReadUsersetTuplesFilter{ 574 Object: "document:1", 575 Relation: "viewer", 576 AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ 577 typesystem.DirectRelationReference("group", "member"), 578 }, 579 }) 580 require.NoError(t, err) 581 582 iter := storage.NewTupleKeyIteratorFromTupleIterator(gotTuples) 583 defer iter.Stop() 584 585 gotTk, err := iter.Next(ctx) 586 require.NoError(t, err) 587 588 expected := tuple.NewTupleKey("document:1", "viewer", "group:eng#member") 589 if diff := cmp.Diff(expected, gotTk, cmpOpts...); diff != "" { 590 require.FailNowf(t, "mismatch (-want +got):\n%s", diff) 591 } 592 593 _, err = iter.Next(ctx) 594 require.ErrorIs(t, err, storage.ErrIteratorDone) 595 }) 596 597 t.Run("reading_userset_tuples_with_filter_made_of_direct_relation_references", func(t *testing.T) { 598 storeID := ulid.Make().String() 599 tks := []*openfgav1.TupleKey{ 600 tuple.NewTupleKey("document:1", "viewer", "user:*"), 601 tuple.NewTupleKey("document:1", "viewer", "users:*"), 602 tuple.NewTupleKey("document:1", "viewer", "group:eng#member"), 603 tuple.NewTupleKey("document:1", "viewer", "grouping:eng#member"), 604 } 605 606 err := datastore.Write(ctx, storeID, nil, tks) 607 require.NoError(t, err) 608 609 gotTuples, err := datastore.ReadUsersetTuples(ctx, storeID, storage.ReadUsersetTuplesFilter{ 610 Object: "document:1", 611 Relation: "viewer", 612 AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ 613 typesystem.DirectRelationReference("group", "member"), 614 typesystem.DirectRelationReference("grouping", "member"), 615 }, 616 }) 617 require.NoError(t, err) 618 619 iter := storage.NewTupleKeyIteratorFromTupleIterator(gotTuples) 620 defer iter.Stop() 621 622 gotOne, err := iter.Next(ctx) 623 require.NoError(t, err) 624 gotTwo, err := iter.Next(ctx) 625 require.NoError(t, err) 626 627 expected := []*openfgav1.TupleKey{ 628 tuple.NewTupleKey("document:1", "viewer", "group:eng#member"), 629 tuple.NewTupleKey("document:1", "viewer", "grouping:eng#member"), 630 } 631 if diff := cmp.Diff(expected, []*openfgav1.TupleKey{gotOne, gotTwo}, cmpOpts...); diff != "" { 632 require.FailNowf(t, "mismatch (-want +got):\n%s", diff) 633 } 634 635 _, err = iter.Next(ctx) 636 require.ErrorIs(t, err, storage.ErrIteratorDone) 637 }) 638 639 t.Run("reading_userset_tuples_with_filter_made_of_wildcard_relation_reference", func(t *testing.T) { 640 storeID := ulid.Make().String() 641 tks := []*openfgav1.TupleKey{ 642 tuple.NewTupleKey("document:1", "viewer", "user:*"), 643 tuple.NewTupleKey("document:1", "viewer", "users:*"), 644 tuple.NewTupleKey("document:1", "viewer", "group:eng#member"), 645 tuple.NewTupleKey("document:1", "viewer", "grouping:eng#member"), 646 } 647 648 err := datastore.Write(ctx, storeID, nil, tks) 649 require.NoError(t, err) 650 651 gotTuples, err := datastore.ReadUsersetTuples(ctx, storeID, storage.ReadUsersetTuplesFilter{ 652 Object: "document:1", 653 Relation: "viewer", 654 AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ 655 typesystem.WildcardRelationReference("user"), 656 }, 657 }) 658 require.NoError(t, err) 659 660 iter := storage.NewTupleKeyIteratorFromTupleIterator(gotTuples) 661 defer iter.Stop() 662 663 got, err := iter.Next(ctx) 664 require.NoError(t, err) 665 666 expected := tuple.NewTupleKey("document:1", "viewer", "user:*") 667 if diff := cmp.Diff(expected, got, cmpOpts...); diff != "" { 668 require.FailNowf(t, "mismatch (-want +got):\n%s", diff) 669 } 670 671 _, err = iter.Next(ctx) 672 require.ErrorIs(t, err, storage.ErrIteratorDone) 673 }) 674 675 t.Run("reading_userset_tuples_with_filter_made_of_mix_references", func(t *testing.T) { 676 storeID := ulid.Make().String() 677 tks := []*openfgav1.TupleKey{ 678 tuple.NewTupleKey("document:1", "viewer", "user:*"), 679 tuple.NewTupleKey("document:1", "viewer", "users:*"), 680 tuple.NewTupleKey("document:1", "viewer", "group:eng#member"), 681 tuple.NewTupleKey("document:1", "viewer", "grouping:eng#member"), 682 } 683 684 err := datastore.Write(ctx, storeID, nil, tks) 685 require.NoError(t, err) 686 687 gotTuples, err := datastore.ReadUsersetTuples(ctx, storeID, storage.ReadUsersetTuplesFilter{ 688 Object: "document:1", 689 Relation: "viewer", 690 AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ 691 typesystem.DirectRelationReference("group", "member"), 692 typesystem.WildcardRelationReference("user"), 693 }, 694 }) 695 require.NoError(t, err) 696 697 iter := storage.NewTupleKeyIteratorFromTupleIterator(gotTuples) 698 defer iter.Stop() 699 700 gotOne, err := iter.Next(ctx) 701 require.NoError(t, err) 702 gotTwo, err := iter.Next(ctx) 703 require.NoError(t, err) 704 705 expected := []*openfgav1.TupleKey{ 706 tuple.NewTupleKey("document:1", "viewer", "group:eng#member"), 707 tuple.NewTupleKey("document:1", "viewer", "user:*"), 708 } 709 if diff := cmp.Diff(expected, []*openfgav1.TupleKey{gotOne, gotTwo}, cmpOpts...); diff != "" { 710 require.FailNowf(t, "mismatch (-want +got):\n%s", diff) 711 } 712 713 _, err = iter.Next(ctx) 714 require.ErrorIs(t, err, storage.ErrIteratorDone) 715 }) 716 717 t.Run("tuples_with_nil_condition", func(t *testing.T) { 718 // This test ensures we don't normalize nil conditions to an empty value. 719 storeID := ulid.Make().String() 720 721 tupleKey1 := tuple.NewTupleKey("document:1", "viewer", "user:jon") 722 tupleKey2 := tuple.NewTupleKey("group:1", "member", "group:2#member") 723 724 tks := []*openfgav1.TupleKey{ 725 { 726 Object: tupleKey1.GetObject(), 727 Relation: tupleKey1.GetRelation(), 728 User: tupleKey1.GetUser(), 729 Condition: nil, 730 }, 731 { 732 Object: tupleKey2.GetObject(), 733 Relation: tupleKey2.GetRelation(), 734 User: tupleKey2.GetUser(), 735 Condition: nil, 736 }, 737 } 738 739 err := datastore.Write(ctx, storeID, nil, tks) 740 require.NoError(t, err) 741 742 iter, err := datastore.Read(ctx, storeID, tupleKey1) 743 require.NoError(t, err) 744 defer iter.Stop() 745 746 tp, err := iter.Next(ctx) 747 require.NoError(t, err) 748 require.Nil(t, tp.GetKey().GetCondition()) 749 750 tuples, _, err := datastore.ReadPage(ctx, storeID, &openfgav1.TupleKey{}, storage.PaginationOptions{ 751 PageSize: 2, 752 }) 753 require.NoError(t, err) 754 require.Len(t, tuples, 2) 755 require.Nil(t, tuples[0].GetKey().GetCondition()) 756 require.Nil(t, tuples[1].GetKey().GetCondition()) 757 758 tp, err = datastore.ReadUserTuple(ctx, storeID, tupleKey1) 759 require.NoError(t, err) 760 require.Nil(t, tp.GetKey().GetCondition()) 761 762 iter, err = datastore.ReadUsersetTuples(ctx, storeID, storage.ReadUsersetTuplesFilter{ 763 Object: tupleKey2.GetObject(), 764 Relation: tupleKey2.GetRelation(), 765 }) 766 require.NoError(t, err) 767 defer iter.Stop() 768 769 tp, err = iter.Next(ctx) 770 require.NoError(t, err) 771 require.Nil(t, tp.GetKey().GetCondition()) 772 773 iter, err = datastore.ReadStartingWithUser(ctx, storeID, storage.ReadStartingWithUserFilter{ 774 ObjectType: tuple.GetType(tupleKey1.GetObject()), 775 Relation: tupleKey1.GetRelation(), 776 UserFilter: []*openfgav1.ObjectRelation{ 777 {Object: tupleKey1.GetUser()}, 778 }, 779 }) 780 require.NoError(t, err) 781 defer iter.Stop() 782 783 tp, err = iter.Next(ctx) 784 require.NoError(t, err) 785 require.Nil(t, tp.GetKey().GetCondition()) 786 787 changes, _, err := datastore.ReadChanges(ctx, storeID, "", storage.PaginationOptions{}, 0) 788 require.NoError(t, err) 789 require.Len(t, changes, 2) 790 require.Nil(t, changes[0].GetTupleKey().GetCondition()) 791 require.Nil(t, changes[1].GetTupleKey().GetCondition()) 792 }) 793 794 t.Run("normalize_empty_context", func(t *testing.T) { 795 // This test ensures we normalize nil or empty context as empty context in all reads. 796 storeID := ulid.Make().String() 797 798 tupleKey1 := tuple.NewTupleKey("document:1", "viewer", "user:jon") 799 tupleKey2 := tuple.NewTupleKey("group:1", "member", "group:2#member") 800 801 tks := []*openfgav1.TupleKey{ 802 { 803 Object: tupleKey1.GetObject(), 804 Relation: tupleKey1.GetRelation(), 805 User: tupleKey1.GetUser(), 806 Condition: &openfgav1.RelationshipCondition{ 807 Name: "somecondition", 808 Context: nil, 809 }, 810 }, 811 { 812 Object: tupleKey2.GetObject(), 813 Relation: tupleKey2.GetRelation(), 814 User: tupleKey2.GetUser(), 815 Condition: &openfgav1.RelationshipCondition{ 816 Name: "othercondition", 817 Context: nil, 818 }, 819 }, 820 } 821 822 err := datastore.Write(ctx, storeID, nil, tks) 823 require.NoError(t, err) 824 825 iter, err := datastore.Read(ctx, storeID, tupleKey1) 826 require.NoError(t, err) 827 defer iter.Stop() 828 829 tp, err := iter.Next(ctx) 830 require.NoError(t, err) 831 require.Equal(t, "somecondition", tp.GetKey().GetCondition().GetName()) 832 require.NotNil(t, tp.GetKey().GetCondition().GetContext()) 833 require.Empty(t, tp.GetKey().GetCondition().GetContext()) 834 835 tuples, _, err := datastore.ReadPage(ctx, storeID, &openfgav1.TupleKey{}, storage.PaginationOptions{ 836 PageSize: 2, 837 }) 838 require.NoError(t, err) 839 require.Len(t, tuples, 2) 840 require.NotNil(t, tuples[0].GetKey().GetCondition().GetContext()) 841 require.NotNil(t, tuples[1].GetKey().GetCondition().GetContext()) 842 843 tp, err = datastore.ReadUserTuple(ctx, storeID, tupleKey1) 844 require.NoError(t, err) 845 require.NotNil(t, tp.GetKey().GetCondition().GetContext()) 846 847 iter, err = datastore.ReadUsersetTuples(ctx, storeID, storage.ReadUsersetTuplesFilter{ 848 Object: tupleKey2.GetObject(), 849 Relation: tupleKey2.GetRelation(), 850 }) 851 require.NoError(t, err) 852 defer iter.Stop() 853 854 tp, err = iter.Next(ctx) 855 require.NoError(t, err) 856 require.NotNil(t, tp.GetKey().GetCondition().GetContext()) 857 858 iter, err = datastore.ReadStartingWithUser(ctx, storeID, storage.ReadStartingWithUserFilter{ 859 ObjectType: tuple.GetType(tupleKey1.GetObject()), 860 Relation: tupleKey1.GetRelation(), 861 UserFilter: []*openfgav1.ObjectRelation{ 862 {Object: tupleKey1.GetUser()}, 863 }, 864 }) 865 require.NoError(t, err) 866 defer iter.Stop() 867 868 tp, err = iter.Next(ctx) 869 require.NoError(t, err) 870 require.NotNil(t, tp.GetKey().GetCondition().GetContext()) 871 872 changes, _, err := datastore.ReadChanges(ctx, storeID, "", storage.PaginationOptions{}, 0) 873 require.NoError(t, err) 874 require.Len(t, changes, 2) 875 require.NotNil(t, changes[0].GetTupleKey().GetCondition().GetContext()) 876 require.NotNil(t, changes[1].GetTupleKey().GetCondition().GetContext()) 877 }) 878 } 879 880 func ReadPageTestCorrectnessOfContinuationTokens(t *testing.T, datastore storage.OpenFGADatastore) { 881 ctx := context.Background() 882 storeID := ulid.Make().String() 883 tk0 := &openfgav1.TupleKey{Object: "doc:readme", Relation: "owner", User: "10"} 884 tk1 := &openfgav1.TupleKey{Object: "doc:readme", Relation: "viewer", User: "11"} 885 886 err := datastore.Write(ctx, storeID, nil, []*openfgav1.TupleKey{tk0, tk1}) 887 require.NoError(t, err) 888 889 t.Run("readPage_pagination_works_properly", func(t *testing.T) { 890 tuples0, contToken0, err := datastore.ReadPage(ctx, storeID, &openfgav1.TupleKey{Object: "doc:readme"}, storage.PaginationOptions{PageSize: 1}) 891 require.NoError(t, err) 892 require.Len(t, tuples0, 1) 893 require.NotEmpty(t, contToken0) 894 895 if diff := cmp.Diff(tk0, tuples0[0].GetKey(), cmpOpts...); diff != "" { 896 t.Fatalf("mismatch (-want +got):\n%s", diff) 897 } 898 899 tuples1, contToken1, err := datastore.ReadPage(ctx, storeID, &openfgav1.TupleKey{Object: "doc:readme"}, storage.PaginationOptions{PageSize: 1, From: string(contToken0)}) 900 require.NoError(t, err) 901 require.Len(t, tuples1, 1) 902 require.Empty(t, contToken1) 903 904 if diff := cmp.Diff(tk1, tuples1[0].GetKey(), cmpOpts...); diff != "" { 905 t.Fatalf("mismatch (-want +got):\n%s", diff) 906 } 907 }) 908 909 t.Run("reading_a_page_completely_does_not_return_a_continuation_token", func(t *testing.T) { 910 tuples, contToken, err := datastore.ReadPage(ctx, storeID, &openfgav1.TupleKey{Object: "doc:readme"}, storage.PaginationOptions{PageSize: 2}) 911 require.NoError(t, err) 912 require.Len(t, tuples, 2) 913 require.Empty(t, contToken) 914 }) 915 916 t.Run("reading_a_page_partially_returns_a_continuation_token", func(t *testing.T) { 917 tuples, contToken, err := datastore.ReadPage(ctx, storeID, &openfgav1.TupleKey{Object: "doc:readme"}, storage.PaginationOptions{PageSize: 1}) 918 require.NoError(t, err) 919 require.Len(t, tuples, 1) 920 require.NotEmpty(t, contToken) 921 }) 922 923 t.Run("ReadPaginationWorks", func(t *testing.T) { 924 tuple0, contToken0, err := datastore.ReadPage(ctx, storeID, nil, storage.PaginationOptions{PageSize: 1}) 925 require.NoError(t, err) 926 require.Len(t, tuple0, 1) 927 require.NotEmpty(t, contToken0) 928 929 if diff := cmp.Diff(tk0, tuple0[0].GetKey(), cmpOpts...); diff != "" { 930 t.Fatalf("mismatch (-want +got):\n%s", diff) 931 } 932 933 tuple1, contToken1, err := datastore.ReadPage(ctx, storeID, nil, storage.PaginationOptions{PageSize: 1, From: string(contToken0)}) 934 require.NoError(t, err) 935 require.Len(t, tuple1, 1) 936 require.Empty(t, contToken1) 937 938 if diff := cmp.Diff(tk1, tuple1[0].GetKey(), cmpOpts...); diff != "" { 939 t.Fatalf("mismatch (-want +got):\n%s", diff) 940 } 941 }) 942 943 t.Run("reading_by_storeID_completely_does_not_return_a_continuation_token", func(t *testing.T) { 944 tuples, contToken, err := datastore.ReadPage(ctx, storeID, nil, storage.PaginationOptions{PageSize: 2}) 945 require.NoError(t, err) 946 require.Len(t, tuples, 2) 947 require.Empty(t, contToken) 948 }) 949 950 t.Run("reading_by_storeID_partially_returns_a_continuation_token", func(t *testing.T) { 951 tuples, contToken, err := datastore.ReadPage(ctx, storeID, nil, storage.PaginationOptions{PageSize: 1}) 952 require.NoError(t, err) 953 require.Len(t, tuples, 1) 954 require.NotEmpty(t, contToken) 955 }) 956 } 957 958 func ReadStartingWithUserTest(t *testing.T, datastore storage.OpenFGADatastore) { 959 ctx := context.Background() 960 961 tuples := []*openfgav1.TupleKey{ 962 tuple.NewTupleKey("document:doc1", "viewer", "user:jon"), 963 tuple.NewTupleKey("document:doc2", "viewer", "group:eng#member"), 964 tuple.NewTupleKey("document:doc3", "editor", "user:jon"), 965 tuple.NewTupleKey("folder:folder1", "viewer", "user:jon"), 966 { 967 Object: "document:doc4", 968 Relation: "viewer", 969 User: "user:jon", 970 Condition: &openfgav1.RelationshipCondition{ 971 Name: "condition", 972 }, 973 }, 974 } 975 976 t.Run("returns_results_with_two_user_filters", func(t *testing.T) { 977 storeID := ulid.Make().String() 978 979 err := datastore.Write(ctx, storeID, nil, tuples) 980 require.NoError(t, err) 981 982 tupleIterator, err := datastore.ReadStartingWithUser( 983 ctx, 984 storeID, 985 storage.ReadStartingWithUserFilter{ 986 ObjectType: "document", 987 Relation: "viewer", 988 UserFilter: []*openfgav1.ObjectRelation{ 989 { 990 Object: "user:jon", 991 }, 992 { 993 Object: "group:eng", 994 Relation: "member", 995 }, 996 }, 997 }, 998 ) 999 require.NoError(t, err) 1000 1001 objects := getObjects(t, tupleIterator) 1002 1003 require.ElementsMatch(t, []string{"document:doc1", "document:doc2", "document:doc4"}, objects) 1004 }) 1005 1006 t.Run("returns_no_results_if_the_input_users_do_not_match_the_tuples", func(t *testing.T) { 1007 storeID := ulid.Make().String() 1008 1009 err := datastore.Write(ctx, storeID, nil, tuples) 1010 require.NoError(t, err) 1011 1012 tupleIterator, err := datastore.ReadStartingWithUser( 1013 ctx, 1014 storeID, 1015 storage.ReadStartingWithUserFilter{ 1016 ObjectType: "document", 1017 Relation: "viewer", 1018 UserFilter: []*openfgav1.ObjectRelation{ 1019 { 1020 Object: "user:maria", 1021 }, 1022 }, 1023 }, 1024 ) 1025 require.NoError(t, err) 1026 1027 objects := getObjects(t, tupleIterator) 1028 1029 require.Empty(t, objects) 1030 }) 1031 1032 t.Run("returns_no_results_if_the_input_relation_does_not_match_any_tuples", func(t *testing.T) { 1033 storeID := ulid.Make().String() 1034 1035 err := datastore.Write(ctx, storeID, nil, tuples) 1036 require.NoError(t, err) 1037 1038 tupleIterator, err := datastore.ReadStartingWithUser( 1039 ctx, 1040 storeID, 1041 storage.ReadStartingWithUserFilter{ 1042 ObjectType: "document", 1043 Relation: "non-existing", 1044 UserFilter: []*openfgav1.ObjectRelation{ 1045 { 1046 Object: "user:jon", 1047 }, 1048 }, 1049 }, 1050 ) 1051 require.NoError(t, err) 1052 1053 objects := getObjects(t, tupleIterator) 1054 1055 require.Empty(t, objects) 1056 }) 1057 1058 t.Run("returns_no_results_if_the_input_object_type_does_not_match_any_tuples", func(t *testing.T) { 1059 storeID := ulid.Make().String() 1060 1061 err := datastore.Write(ctx, storeID, nil, tuples) 1062 require.NoError(t, err) 1063 1064 tupleIterator, err := datastore.ReadStartingWithUser( 1065 ctx, 1066 storeID, 1067 storage.ReadStartingWithUserFilter{ 1068 ObjectType: "nonexisting", 1069 Relation: "viewer", 1070 UserFilter: []*openfgav1.ObjectRelation{ 1071 { 1072 Object: "user:jon", 1073 }, 1074 }, 1075 }, 1076 ) 1077 require.NoError(t, err) 1078 1079 objects := getObjects(t, tupleIterator) 1080 1081 require.Empty(t, objects) 1082 }) 1083 } 1084 1085 func ReadTestCorrectnessOfTuples(t *testing.T, datastore storage.OpenFGADatastore) { 1086 ctx := context.Background() 1087 1088 tuples := []*openfgav1.TupleKey{ 1089 tuple.NewTupleKey("document:1", "reader", "user:anne"), 1090 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1091 tuple.NewTupleKey("document:1", "writer", "user:bob"), 1092 { 1093 Object: "document:2", 1094 Relation: "viewer", 1095 User: "user:anne", 1096 Condition: &openfgav1.RelationshipCondition{ 1097 Name: "condition", 1098 }, 1099 }, 1100 } 1101 1102 storeID := ulid.Make().String() 1103 1104 err := datastore.Write(ctx, storeID, nil, tuples) 1105 require.NoError(t, err) 1106 1107 t.Run("empty_filter_returns_all_tuples", func(t *testing.T) { 1108 tupleIterator, err := datastore.Read( 1109 ctx, 1110 storeID, 1111 tuple.NewTupleKey("", "", ""), 1112 ) 1113 require.NoError(t, err) 1114 defer tupleIterator.Stop() 1115 1116 expectedTupleKeys := []*openfgav1.TupleKey{ 1117 tuple.NewTupleKey("document:1", "reader", "user:anne"), 1118 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1119 tuple.NewTupleKey("document:1", "writer", "user:bob"), 1120 { 1121 Object: "document:2", 1122 Relation: "viewer", 1123 User: "user:anne", 1124 Condition: &openfgav1.RelationshipCondition{ 1125 Name: "condition", 1126 Context: &structpb.Struct{}, 1127 }, 1128 }, 1129 } 1130 1131 require.ElementsMatch(t, expectedTupleKeys, getTupleKeys(tupleIterator, t)) 1132 }) 1133 1134 t.Run("filter_by_user_and_relation_and_objectID", func(t *testing.T) { 1135 tupleIterator, err := datastore.Read( 1136 ctx, 1137 storeID, 1138 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1139 ) 1140 require.NoError(t, err) 1141 defer tupleIterator.Stop() 1142 1143 expectedTupleKeys := []*openfgav1.TupleKey{ 1144 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1145 } 1146 1147 require.ElementsMatch(t, expectedTupleKeys, getTupleKeys(tupleIterator, t)) 1148 }) 1149 1150 t.Run("filter_by_user_and_relation_and_objectType", func(t *testing.T) { 1151 tupleIterator, err := datastore.Read( 1152 ctx, 1153 storeID, 1154 tuple.NewTupleKey("document:", "reader", "user:bob"), 1155 ) 1156 require.NoError(t, err) 1157 defer tupleIterator.Stop() 1158 1159 expectedTupleKeys := []*openfgav1.TupleKey{ 1160 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1161 } 1162 1163 require.ElementsMatch(t, expectedTupleKeys, getTupleKeys(tupleIterator, t)) 1164 }) 1165 1166 t.Run("filter_by_user_and_objectType", func(t *testing.T) { 1167 tupleIterator, err := datastore.Read( 1168 ctx, 1169 storeID, 1170 tuple.NewTupleKey("document:", "", "user:bob"), 1171 ) 1172 require.NoError(t, err) 1173 defer tupleIterator.Stop() 1174 1175 expectedTupleKeys := []*openfgav1.TupleKey{ 1176 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1177 tuple.NewTupleKey("document:1", "writer", "user:bob"), 1178 } 1179 1180 require.ElementsMatch(t, expectedTupleKeys, getTupleKeys(tupleIterator, t)) 1181 }) 1182 1183 t.Run("filter_by_relation_and_objectID", func(t *testing.T) { 1184 tupleIterator, err := datastore.Read( 1185 ctx, 1186 storeID, 1187 tuple.NewTupleKey("document:1", "reader", ""), 1188 ) 1189 require.NoError(t, err) 1190 defer tupleIterator.Stop() 1191 1192 expectedTupleKeys := []*openfgav1.TupleKey{ 1193 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1194 tuple.NewTupleKey("document:1", "reader", "user:anne"), 1195 } 1196 1197 require.ElementsMatch(t, expectedTupleKeys, getTupleKeys(tupleIterator, t)) 1198 }) 1199 1200 t.Run("filter_by_objectID", func(t *testing.T) { 1201 tupleIterator, err := datastore.Read( 1202 ctx, 1203 storeID, 1204 tuple.NewTupleKey("document:1", "", ""), 1205 ) 1206 require.NoError(t, err) 1207 defer tupleIterator.Stop() 1208 1209 expectedTupleKeys := []*openfgav1.TupleKey{ 1210 tuple.NewTupleKey("document:1", "reader", "user:anne"), 1211 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1212 tuple.NewTupleKey("document:1", "writer", "user:bob"), 1213 } 1214 1215 require.ElementsMatch(t, expectedTupleKeys, getTupleKeys(tupleIterator, t)) 1216 }) 1217 1218 t.Run("filter_by_objectID_and_user", func(t *testing.T) { 1219 tupleIterator, err := datastore.Read( 1220 ctx, 1221 storeID, 1222 tuple.NewTupleKey("document:1", "", "user:bob"), 1223 ) 1224 require.NoError(t, err) 1225 defer tupleIterator.Stop() 1226 1227 expectedTupleKeys := []*openfgav1.TupleKey{ 1228 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1229 tuple.NewTupleKey("document:1", "writer", "user:bob"), 1230 } 1231 1232 require.ElementsMatch(t, expectedTupleKeys, getTupleKeys(tupleIterator, t)) 1233 }) 1234 } 1235 1236 func ReadPageTestCorrectnessOfContinuationTokensV2(t *testing.T, datastore storage.OpenFGADatastore) { 1237 ctx := context.Background() 1238 storeID := ulid.Make().String() 1239 1240 tuplesWritten := []*openfgav1.TupleKey{ 1241 tuple.NewTupleKey("document:1", "reader", "user:anne"), 1242 // read should skip over these 1243 tuple.NewTupleKey("document:2", "a", "user:anne"), 1244 tuple.NewTupleKey("document:2", "b", "user:anne"), 1245 tuple.NewTupleKey("document:2", "c", "user:anne"), 1246 tuple.NewTupleKey("document:2", "d", "user:anne"), 1247 tuple.NewTupleKey("document:2", "e", "user:anne"), 1248 tuple.NewTupleKey("document:2", "f", "user:anne"), 1249 tuple.NewTupleKey("document:2", "g", "user:anne"), 1250 tuple.NewTupleKey("document:2", "h", "user:anne"), 1251 tuple.NewTupleKey("document:2", "j", "user:anne"), 1252 // end of skip 1253 tuple.NewTupleKey("document:1", "admin", "user:anne"), 1254 } 1255 1256 err := datastore.Write(ctx, storeID, nil, tuplesWritten) 1257 require.NoError(t, err) 1258 1259 t.Run("returns_2_results_and_no_continuation_token_when_page_size_2", func(t *testing.T) { 1260 tuplesRead, contToken, err := datastore.ReadPage( 1261 ctx, 1262 storeID, 1263 tuple.NewTupleKey("document:1", "", "user:anne"), 1264 storage.PaginationOptions{ 1265 PageSize: 2, 1266 }, 1267 ) 1268 require.NoError(t, err) 1269 1270 expectedTuples := []*openfgav1.Tuple{ 1271 {Key: tuple.NewTupleKey("document:1", "admin", "user:anne")}, 1272 {Key: tuple.NewTupleKey("document:1", "reader", "user:anne")}, 1273 } 1274 1275 requireEqualTuples(t, expectedTuples, tuplesRead) 1276 require.Empty(t, contToken) 1277 }) 1278 1279 t.Run("returns_1_results_and_continuation_token_when_page_size_1", func(t *testing.T) { 1280 firstRead, contToken, err := datastore.ReadPage( 1281 ctx, 1282 storeID, 1283 tuple.NewTupleKey("document:1", "", "user:anne"), 1284 storage.PaginationOptions{ 1285 PageSize: 1, 1286 }, 1287 ) 1288 require.NoError(t, err) 1289 1290 require.Len(t, firstRead, 1) 1291 require.Equal(t, "document:1", firstRead[0].GetKey().GetObject()) 1292 require.Equal(t, "user:anne", firstRead[0].GetKey().GetUser()) 1293 require.NotEmpty(t, contToken) 1294 1295 // use the token 1296 1297 secondRead, contToken, err := datastore.ReadPage( 1298 ctx, 1299 storeID, 1300 tuple.NewTupleKey("document:1", "", "user:anne"), 1301 storage.PaginationOptions{ 1302 PageSize: 50, // fetch the remainder 1303 From: string(contToken), 1304 }, 1305 ) 1306 require.NoError(t, err) 1307 1308 require.Len(t, secondRead, 1) 1309 require.Equal(t, "document:1", secondRead[0].GetKey().GetObject()) 1310 require.Equal(t, "user:anne", secondRead[0].GetKey().GetUser()) 1311 require.NotEqual(t, firstRead[0].GetKey().GetRelation(), secondRead[0].GetKey().GetRelation()) 1312 require.Empty(t, contToken) 1313 }) 1314 } 1315 1316 func ReadPageTestCorrectnessOfTuples(t *testing.T, datastore storage.OpenFGADatastore) { 1317 ctx := context.Background() 1318 1319 tuples := []*openfgav1.TupleKey{ 1320 tuple.NewTupleKey("document:1", "reader", "user:anne"), 1321 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1322 tuple.NewTupleKey("document:1", "writer", "user:bob"), 1323 { 1324 Object: "document:2", 1325 Relation: "viewer", 1326 User: "user:anne", 1327 Condition: &openfgav1.RelationshipCondition{ 1328 Name: "condition", 1329 }, 1330 }, 1331 } 1332 1333 storeID := ulid.Make().String() 1334 1335 err := datastore.Write(ctx, storeID, nil, tuples) 1336 require.NoError(t, err) 1337 1338 t.Run("empty_filter_returns_all_tuples", func(t *testing.T) { 1339 gotTuples, contToken, err := datastore.ReadPage( 1340 ctx, 1341 storeID, 1342 tuple.NewTupleKey("", "", ""), 1343 storage.PaginationOptions{ 1344 PageSize: 50, 1345 }, 1346 ) 1347 require.NoError(t, err) 1348 1349 expectedTuples := []*openfgav1.Tuple{ 1350 {Key: tuple.NewTupleKey("document:1", "reader", "user:anne")}, 1351 {Key: tuple.NewTupleKey("document:1", "reader", "user:bob")}, 1352 {Key: tuple.NewTupleKey("document:1", "writer", "user:bob")}, 1353 {Key: tuple.NewTupleKeyWithCondition("document:2", "viewer", "user:anne", "condition", nil)}, 1354 } 1355 1356 requireEqualTuples(t, expectedTuples, gotTuples) 1357 require.Empty(t, contToken) 1358 }) 1359 1360 t.Run("filter_by_user_and_relation_and_objectID", func(t *testing.T) { 1361 gotTuples, contToken, err := datastore.ReadPage( 1362 ctx, 1363 storeID, 1364 tuple.NewTupleKey("document:1", "reader", "user:bob"), 1365 storage.PaginationOptions{ 1366 PageSize: 50, 1367 }, 1368 ) 1369 require.NoError(t, err) 1370 1371 expectedTuples := []*openfgav1.Tuple{ 1372 {Key: tuple.NewTupleKey("document:1", "reader", "user:bob")}, 1373 } 1374 1375 requireEqualTuples(t, expectedTuples, gotTuples) 1376 require.Empty(t, contToken) 1377 }) 1378 1379 t.Run("filter_by_user_and_relation_and_objectType", func(t *testing.T) { 1380 gotTuples, contToken, err := datastore.ReadPage( 1381 ctx, 1382 storeID, 1383 tuple.NewTupleKey("document:", "reader", "user:bob"), 1384 storage.PaginationOptions{ 1385 PageSize: 50, 1386 }, 1387 ) 1388 require.NoError(t, err) 1389 1390 expectedTuples := []*openfgav1.Tuple{ 1391 {Key: tuple.NewTupleKey("document:1", "reader", "user:bob")}, 1392 } 1393 1394 requireEqualTuples(t, expectedTuples, gotTuples) 1395 require.Empty(t, contToken) 1396 }) 1397 1398 t.Run("filter_by_user_and_objectType", func(t *testing.T) { 1399 gotTuples, contToken, err := datastore.ReadPage( 1400 ctx, 1401 storeID, 1402 tuple.NewTupleKey("document:", "", "user:bob"), 1403 storage.PaginationOptions{ 1404 PageSize: 50, 1405 }, 1406 ) 1407 require.NoError(t, err) 1408 1409 expectedTuples := []*openfgav1.Tuple{ 1410 {Key: tuple.NewTupleKey("document:1", "reader", "user:bob")}, 1411 {Key: tuple.NewTupleKey("document:1", "writer", "user:bob")}, 1412 } 1413 1414 requireEqualTuples(t, expectedTuples, gotTuples) 1415 require.Empty(t, contToken) 1416 }) 1417 1418 t.Run("filter_by_relation_and_objectID", func(t *testing.T) { 1419 gotTuples, contToken, err := datastore.ReadPage( 1420 ctx, 1421 storeID, 1422 tuple.NewTupleKey("document:1", "reader", ""), 1423 storage.PaginationOptions{ 1424 PageSize: 50, 1425 }, 1426 ) 1427 require.NoError(t, err) 1428 1429 expectedTuples := []*openfgav1.Tuple{ 1430 {Key: tuple.NewTupleKey("document:1", "reader", "user:bob")}, 1431 {Key: tuple.NewTupleKey("document:1", "reader", "user:anne")}, 1432 } 1433 1434 requireEqualTuples(t, expectedTuples, gotTuples) 1435 require.Empty(t, contToken) 1436 }) 1437 1438 t.Run("filter_by_objectID", func(t *testing.T) { 1439 gotTuples, contToken, err := datastore.ReadPage( 1440 ctx, 1441 storeID, 1442 tuple.NewTupleKey("document:1", "", ""), 1443 storage.PaginationOptions{ 1444 PageSize: 50, 1445 }, 1446 ) 1447 require.NoError(t, err) 1448 1449 expectedTuples := []*openfgav1.Tuple{ 1450 {Key: tuple.NewTupleKey("document:1", "reader", "user:anne")}, 1451 {Key: tuple.NewTupleKey("document:1", "reader", "user:bob")}, 1452 {Key: tuple.NewTupleKey("document:1", "writer", "user:bob")}, 1453 } 1454 1455 requireEqualTuples(t, expectedTuples, gotTuples) 1456 require.Empty(t, contToken) 1457 }) 1458 1459 t.Run("filter_by_objectID_and_user", func(t *testing.T) { 1460 gotTuples, contToken, err := datastore.ReadPage( 1461 ctx, 1462 storeID, 1463 tuple.NewTupleKey("document:1", "", "user:bob"), 1464 storage.PaginationOptions{ 1465 PageSize: 50, 1466 }, 1467 ) 1468 require.NoError(t, err) 1469 1470 expectedTuples := []*openfgav1.Tuple{ 1471 {Key: tuple.NewTupleKey("document:1", "reader", "user:bob")}, 1472 {Key: tuple.NewTupleKey("document:1", "writer", "user:bob")}, 1473 } 1474 1475 requireEqualTuples(t, expectedTuples, gotTuples) 1476 require.Empty(t, contToken) 1477 }) 1478 } 1479 1480 // getObjects returns all the objects from an iterator. 1481 // If the iterator throws an error, it fails the test. 1482 func getObjects(t *testing.T, tupleIterator storage.TupleIterator) []string { 1483 var objects []string 1484 for { 1485 tp, err := tupleIterator.Next(context.Background()) 1486 if err != nil { 1487 if err == storage.ErrIteratorDone { 1488 break 1489 } 1490 1491 t.Errorf(err.Error()) 1492 } 1493 1494 objects = append(objects, tp.GetKey().GetObject()) 1495 } 1496 return objects 1497 } 1498 1499 func getTupleKeys(tupleIterator storage.TupleIterator, t *testing.T) []*openfgav1.TupleKey { 1500 t.Helper() 1501 var tupleKeys []*openfgav1.TupleKey 1502 for { 1503 tp, err := tupleIterator.Next(context.Background()) 1504 if err != nil { 1505 if errors.Is(err, storage.ErrIteratorDone) { 1506 break 1507 } 1508 1509 require.Fail(t, err.Error()) 1510 } 1511 1512 tupleKeys = append(tupleKeys, tp.GetKey()) 1513 } 1514 return tupleKeys 1515 } 1516 1517 func requireEqualTuples(t *testing.T, expectedTuples []*openfgav1.Tuple, actualTuples []*openfgav1.Tuple) { 1518 cmpOpts := []cmp.Option{ 1519 protocmp.IgnoreFields(protoadapt.MessageV2Of(&openfgav1.Tuple{}), "timestamp"), 1520 testutils.TupleCmpTransformer, 1521 protocmp.Transform(), 1522 } 1523 diff := cmp.Diff(expectedTuples, actualTuples, cmpOpts...) 1524 require.Empty(t, diff) 1525 }