github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/datastore/datastore_test.go (about) 1 package datastore 2 3 import ( 4 "context" 5 "testing" 6 7 "github.com/authzed/spicedb/pkg/datastore/options" 8 "github.com/authzed/spicedb/pkg/tuple" 9 10 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 11 "github.com/stretchr/testify/require" 12 ) 13 14 func TestRelationshipsFilterFromPublicFilter(t *testing.T) { 15 tests := []struct { 16 name string 17 input *v1.RelationshipFilter 18 expected RelationshipsFilter 19 expectedError string 20 }{ 21 { 22 "empty", 23 &v1.RelationshipFilter{}, 24 RelationshipsFilter{}, 25 "at least one filter field must be set", 26 }, 27 { 28 "resource id prefix", 29 &v1.RelationshipFilter{OptionalResourceIdPrefix: "someprefix"}, 30 RelationshipsFilter{OptionalResourceIDPrefix: "someprefix"}, 31 "", 32 }, 33 { 34 "resource id and prefix", 35 &v1.RelationshipFilter{OptionalResourceIdPrefix: "someprefix", OptionalResourceId: "someid"}, 36 RelationshipsFilter{}, 37 "cannot specify both OptionalResourceId and OptionalResourceIDPrefix", 38 }, 39 { 40 "only resource name", 41 &v1.RelationshipFilter{ResourceType: "sometype"}, 42 RelationshipsFilter{OptionalResourceType: "sometype"}, 43 "", 44 }, 45 { 46 "only relation name", 47 &v1.RelationshipFilter{OptionalRelation: "somerel"}, 48 RelationshipsFilter{OptionalResourceRelation: "somerel"}, 49 "", 50 }, 51 { 52 "resource name and id", 53 &v1.RelationshipFilter{ResourceType: "sometype", OptionalResourceId: "someid"}, 54 RelationshipsFilter{OptionalResourceType: "sometype", OptionalResourceIds: []string{"someid"}}, 55 "", 56 }, 57 { 58 "resource name and relation", 59 &v1.RelationshipFilter{ResourceType: "sometype", OptionalRelation: "somerel"}, 60 RelationshipsFilter{OptionalResourceType: "sometype", OptionalResourceRelation: "somerel"}, 61 "", 62 }, 63 { 64 "resource and subject", 65 &v1.RelationshipFilter{ResourceType: "sometype", OptionalSubjectFilter: &v1.SubjectFilter{SubjectType: "someothertype"}}, 66 RelationshipsFilter{OptionalResourceType: "sometype", OptionalSubjectsSelectors: []SubjectsSelector{ 67 { 68 OptionalSubjectType: "someothertype", 69 }, 70 }}, 71 "", 72 }, 73 { 74 "resource and subject with optional relation", 75 &v1.RelationshipFilter{ 76 ResourceType: "sometype", 77 OptionalSubjectFilter: &v1.SubjectFilter{ 78 SubjectType: "someothertype", 79 OptionalRelation: &v1.SubjectFilter_RelationFilter{Relation: "somerel"}, 80 }, 81 }, 82 RelationshipsFilter{OptionalResourceType: "sometype", OptionalSubjectsSelectors: []SubjectsSelector{ 83 { 84 OptionalSubjectType: "someothertype", 85 RelationFilter: SubjectRelationFilter{}.WithNonEllipsisRelation("somerel"), 86 }, 87 }}, 88 "", 89 }, 90 { 91 "resource and subject with ellipsis relation", 92 &v1.RelationshipFilter{ 93 ResourceType: "sometype", 94 OptionalSubjectFilter: &v1.SubjectFilter{ 95 SubjectType: "someothertype", 96 OptionalRelation: &v1.SubjectFilter_RelationFilter{Relation: ""}, 97 }, 98 }, 99 RelationshipsFilter{OptionalResourceType: "sometype", OptionalSubjectsSelectors: []SubjectsSelector{ 100 { 101 OptionalSubjectType: "someothertype", 102 RelationFilter: SubjectRelationFilter{}.WithEllipsisRelation(), 103 }, 104 }}, 105 "", 106 }, 107 { 108 "full", 109 &v1.RelationshipFilter{ 110 ResourceType: "sometype", 111 OptionalResourceId: "someid", 112 OptionalRelation: "somerel", 113 OptionalSubjectFilter: &v1.SubjectFilter{ 114 SubjectType: "someothertype", 115 OptionalSubjectId: "somesubjectid", 116 OptionalRelation: &v1.SubjectFilter_RelationFilter{Relation: ""}, 117 }, 118 }, 119 RelationshipsFilter{ 120 OptionalResourceType: "sometype", 121 OptionalResourceIds: []string{"someid"}, 122 OptionalResourceRelation: "somerel", 123 OptionalSubjectsSelectors: []SubjectsSelector{ 124 { 125 OptionalSubjectType: "someothertype", 126 OptionalSubjectIds: []string{"somesubjectid"}, 127 RelationFilter: SubjectRelationFilter{}.WithEllipsisRelation(), 128 }, 129 }, 130 }, 131 "", 132 }, 133 } 134 135 for _, test := range tests { 136 test := test 137 t.Run(test.name, func(t *testing.T) { 138 computed, err := RelationshipsFilterFromPublicFilter(test.input) 139 if test.expectedError != "" { 140 require.ErrorContains(t, err, test.expectedError) 141 return 142 } 143 144 require.Equal(t, test.expected, computed) 145 require.NoError(t, err) 146 }) 147 } 148 } 149 150 func TestConvertedRelationshipFilterTest(t *testing.T) { 151 tcs := []struct { 152 name string 153 filter *v1.RelationshipFilter 154 relationshipString string 155 expected bool 156 }{ 157 { 158 name: "namespace filter match", 159 filter: &v1.RelationshipFilter{ 160 ResourceType: "foo", 161 }, 162 relationshipString: "foo:something#viewer@user:fred", 163 expected: true, 164 }, 165 { 166 name: "namespace filter mismatch", 167 filter: &v1.RelationshipFilter{ 168 ResourceType: "foo", 169 }, 170 relationshipString: "bar:something#viewer@user:fred", 171 expected: false, 172 }, 173 { 174 name: "resource id filter match", 175 filter: &v1.RelationshipFilter{ 176 ResourceType: "foo", 177 OptionalResourceId: "something", 178 }, 179 relationshipString: "foo:something#viewer@user:fred", 180 expected: true, 181 }, 182 { 183 name: "resource id filter mismatch", 184 filter: &v1.RelationshipFilter{ 185 ResourceType: "foo", 186 OptionalResourceId: "something", 187 }, 188 relationshipString: "foo:somethingelse#viewer@user:fred", 189 expected: false, 190 }, 191 { 192 name: "resource id prefix filter match", 193 filter: &v1.RelationshipFilter{ 194 OptionalResourceIdPrefix: "some", 195 }, 196 relationshipString: "foo:something#viewer@user:fred", 197 expected: true, 198 }, 199 { 200 name: "resource id prefix filter mismatch", 201 filter: &v1.RelationshipFilter{ 202 OptionalResourceIdPrefix: "some", 203 }, 204 relationshipString: "foo:else#viewer@user:fred", 205 expected: false, 206 }, 207 { 208 name: "relation filter match", 209 filter: &v1.RelationshipFilter{ 210 ResourceType: "foo", 211 OptionalRelation: "viewer", 212 }, 213 relationshipString: "foo:something#viewer@user:fred", 214 expected: true, 215 }, 216 { 217 name: "relation filter mismatch", 218 filter: &v1.RelationshipFilter{ 219 ResourceType: "foo", 220 OptionalRelation: "viewer", 221 }, 222 relationshipString: "foo:something#editor@user:fred", 223 expected: false, 224 }, 225 { 226 name: "subject type filter match", 227 filter: &v1.RelationshipFilter{ 228 ResourceType: "foo", 229 OptionalSubjectFilter: &v1.SubjectFilter{ 230 SubjectType: "user", 231 }, 232 }, 233 relationshipString: "foo:something#viewer@user:fred", 234 expected: true, 235 }, 236 { 237 name: "subject type filter mismatch", 238 filter: &v1.RelationshipFilter{ 239 ResourceType: "foo", 240 OptionalSubjectFilter: &v1.SubjectFilter{ 241 SubjectType: "user", 242 }, 243 }, 244 relationshipString: "foo:something#viewer@group:foo", 245 expected: false, 246 }, 247 { 248 name: "subject id filter match", 249 filter: &v1.RelationshipFilter{ 250 ResourceType: "foo", 251 OptionalSubjectFilter: &v1.SubjectFilter{ 252 SubjectType: "user", 253 OptionalSubjectId: "fred", 254 }, 255 }, 256 relationshipString: "foo:something#viewer@user:fred", 257 expected: true, 258 }, 259 { 260 name: "subject id filter mismatch", 261 filter: &v1.RelationshipFilter{ 262 ResourceType: "foo", 263 OptionalSubjectFilter: &v1.SubjectFilter{ 264 SubjectType: "user", 265 OptionalSubjectId: "fred", 266 }, 267 }, 268 relationshipString: "foo:something#viewer@user:alice", 269 expected: false, 270 }, 271 { 272 name: "subject relation filter match", 273 filter: &v1.RelationshipFilter{ 274 ResourceType: "foo", 275 OptionalSubjectFilter: &v1.SubjectFilter{ 276 SubjectType: "user", 277 OptionalRelation: &v1.SubjectFilter_RelationFilter{Relation: "far"}, 278 }, 279 }, 280 relationshipString: "foo:something#viewer@user:fred#far", 281 expected: true, 282 }, 283 { 284 name: "subject relation filter mismatch", 285 filter: &v1.RelationshipFilter{ 286 ResourceType: "foo", 287 OptionalSubjectFilter: &v1.SubjectFilter{ 288 SubjectType: "user", 289 OptionalRelation: &v1.SubjectFilter_RelationFilter{Relation: "far"}, 290 }, 291 }, 292 relationshipString: "foo:something#viewer@user:fred#bar", 293 expected: false, 294 }, 295 } 296 297 for _, tc := range tcs { 298 tc := tc 299 t.Run(tc.name, func(t *testing.T) { 300 filter, err := RelationshipsFilterFromPublicFilter(tc.filter) 301 require.NoError(t, err) 302 303 relationship := tuple.MustParse(tc.relationshipString) 304 require.Equal(t, tc.expected, filter.Test(relationship)) 305 }) 306 } 307 } 308 309 func TestRelationshipsFilterTest(t *testing.T) { 310 tcs := []struct { 311 name string 312 filter RelationshipsFilter 313 relationshipString string 314 expected bool 315 }{ 316 { 317 name: "namespace filter match", 318 filter: RelationshipsFilter{OptionalResourceType: "foo"}, 319 relationshipString: "foo:something#viewer@user:fred", 320 expected: true, 321 }, 322 { 323 name: "resource id filter match", 324 filter: RelationshipsFilter{ 325 OptionalResourceType: "foo", 326 OptionalResourceIds: []string{"something"}, 327 }, 328 relationshipString: "foo:something#viewer@user:fred", 329 expected: true, 330 }, 331 { 332 name: "resource id filter mismatch", 333 filter: RelationshipsFilter{ 334 OptionalResourceType: "foo", 335 OptionalResourceIds: []string{"something"}, 336 }, 337 relationshipString: "foo:somethingelse#viewer@user:fred", 338 expected: false, 339 }, 340 { 341 name: "resource id prefix filter match", 342 filter: RelationshipsFilter{ 343 OptionalResourceIDPrefix: "some", 344 }, 345 relationshipString: "foo:something#viewer@user:fred", 346 expected: true, 347 }, 348 { 349 name: "resource id prefix filter mismatch", 350 filter: RelationshipsFilter{ 351 OptionalResourceIDPrefix: "some", 352 }, 353 relationshipString: "foo:else#viewer@user:fred", 354 expected: false, 355 }, 356 { 357 name: "relation filter match", 358 filter: RelationshipsFilter{ 359 OptionalResourceType: "foo", 360 OptionalResourceRelation: "viewer", 361 }, 362 relationshipString: "foo:something#viewer@user:fred", 363 expected: true, 364 }, 365 { 366 name: "relation filter mismatch", 367 filter: RelationshipsFilter{ 368 OptionalResourceType: "foo", 369 OptionalResourceRelation: "viewer", 370 }, 371 relationshipString: "foo:something#editor@user:fred", 372 expected: false, 373 }, 374 { 375 name: "subject type filter match", 376 filter: RelationshipsFilter{ 377 OptionalResourceType: "foo", 378 OptionalSubjectsSelectors: []SubjectsSelector{ 379 { 380 OptionalSubjectType: "user", 381 }, 382 }, 383 }, 384 relationshipString: "foo:something#viewer@user:fred", 385 expected: true, 386 }, 387 { 388 name: "subject type filter mismatch", 389 filter: RelationshipsFilter{ 390 OptionalResourceType: "foo", 391 OptionalSubjectsSelectors: []SubjectsSelector{ 392 { 393 OptionalSubjectType: "user", 394 }, 395 }, 396 }, 397 relationshipString: "foo:something#viewer@group:foo", 398 expected: false, 399 }, 400 { 401 name: "subject id filter match", 402 filter: RelationshipsFilter{ 403 OptionalResourceType: "foo", 404 OptionalSubjectsSelectors: []SubjectsSelector{ 405 { 406 OptionalSubjectType: "user", 407 OptionalSubjectIds: []string{"fred"}, 408 }, 409 }, 410 }, 411 relationshipString: "foo:something#viewer@user:fred", 412 expected: true, 413 }, 414 { 415 name: "subject id filter mismatch", 416 filter: RelationshipsFilter{ 417 OptionalResourceType: "foo", 418 OptionalSubjectsSelectors: []SubjectsSelector{ 419 { 420 OptionalSubjectType: "user", 421 OptionalSubjectIds: []string{"fred"}, 422 }, 423 }, 424 }, 425 relationshipString: "foo:something#viewer@user:alice", 426 expected: false, 427 }, 428 { 429 name: "subject relation filter match", 430 filter: RelationshipsFilter{ 431 OptionalResourceType: "foo", 432 OptionalSubjectsSelectors: []SubjectsSelector{ 433 { 434 OptionalSubjectType: "user", 435 RelationFilter: SubjectRelationFilter{}.WithNonEllipsisRelation("far"), 436 }, 437 }, 438 }, 439 relationshipString: "foo:something#viewer@user:fred#far", 440 expected: true, 441 }, 442 { 443 name: "subject relation filter mismatch", 444 filter: RelationshipsFilter{ 445 OptionalResourceType: "foo", 446 OptionalSubjectsSelectors: []SubjectsSelector{ 447 { 448 OptionalSubjectType: "user", 449 RelationFilter: SubjectRelationFilter{}.WithNonEllipsisRelation("far"), 450 }, 451 }, 452 }, 453 relationshipString: "foo:something#viewer@user:fred#bar", 454 expected: false, 455 }, 456 { 457 name: "multiple subject filter matches", 458 filter: RelationshipsFilter{ 459 OptionalResourceType: "foo", 460 OptionalSubjectsSelectors: []SubjectsSelector{ 461 { 462 OptionalSubjectType: "user", 463 OptionalSubjectIds: []string{"fred"}, 464 }, 465 { 466 OptionalSubjectType: "user", 467 OptionalSubjectIds: []string{"alice"}, 468 }, 469 }, 470 }, 471 relationshipString: "foo:something#viewer@user:alice", 472 expected: true, 473 }, 474 { 475 name: "multiple subject filter mismatches", 476 filter: RelationshipsFilter{ 477 OptionalResourceType: "foo", 478 OptionalSubjectsSelectors: []SubjectsSelector{ 479 { 480 OptionalSubjectType: "user", 481 OptionalSubjectIds: []string{"fred"}, 482 }, 483 { 484 OptionalSubjectType: "user", 485 OptionalSubjectIds: []string{"alice"}, 486 }, 487 }, 488 }, 489 relationshipString: "foo:something#viewer@user:tom", 490 expected: false, 491 }, 492 { 493 name: "caveat filter match", 494 filter: RelationshipsFilter{ 495 OptionalCaveatName: "bar", 496 }, 497 relationshipString: "foo:something#viewer@user:fred[bar]", 498 expected: true, 499 }, 500 { 501 name: "caveat filter mismatch", 502 filter: RelationshipsFilter{ 503 OptionalCaveatName: "bar", 504 }, 505 relationshipString: "foo:something#viewer@user:fred[baz]", 506 expected: false, 507 }, 508 { 509 name: "non-ellipsis subject filter match", 510 filter: RelationshipsFilter{ 511 OptionalSubjectsSelectors: []SubjectsSelector{ 512 { 513 OptionalSubjectType: "user", 514 RelationFilter: SubjectRelationFilter{}.WithOnlyNonEllipsisRelations(), 515 }, 516 }, 517 }, 518 relationshipString: "foo:something#viewer@user:fred#foo", 519 expected: true, 520 }, 521 { 522 name: "non-ellipsis subject filter mismatch", 523 filter: RelationshipsFilter{ 524 OptionalSubjectsSelectors: []SubjectsSelector{ 525 { 526 OptionalSubjectType: "user", 527 RelationFilter: SubjectRelationFilter{}.WithOnlyNonEllipsisRelations(), 528 }, 529 }, 530 }, 531 relationshipString: "foo:something#viewer@user:fred", 532 expected: false, 533 }, 534 { 535 name: "multi subject filter match", 536 filter: RelationshipsFilter{ 537 OptionalSubjectsSelectors: []SubjectsSelector{ 538 { 539 OptionalSubjectType: "user", 540 RelationFilter: SubjectRelationFilter{}.WithEllipsisRelation().WithNonEllipsisRelation("foo"), 541 }, 542 }, 543 }, 544 relationshipString: "foo:something#viewer@user:fred", 545 expected: true, 546 }, 547 } 548 549 for _, tc := range tcs { 550 tc := tc 551 t.Run(tc.name, func(t *testing.T) { 552 relationship := tuple.MustParse(tc.relationshipString) 553 require.Equal(t, tc.expected, tc.filter.Test(relationship)) 554 }) 555 } 556 } 557 558 func TestUnwrapAs(t *testing.T) { 559 result := UnwrapAs[error](nil) 560 require.Nil(t, result) 561 562 ds := fakeDatastore{delegate: fakeDatastore{fakeDatastoreError{}}} 563 result = UnwrapAs[error](ds) 564 require.NotNil(t, result) 565 require.IsType(t, fakeDatastoreError{}, result) 566 567 errorable := fakeDatastoreError{} 568 result = UnwrapAs[error](errorable) 569 require.NotNil(t, result) 570 require.IsType(t, fakeDatastoreError{}, result) 571 } 572 573 type fakeDatastoreError struct { 574 fakeDatastore 575 } 576 577 func (e fakeDatastoreError) Error() string { 578 return "" 579 } 580 581 type fakeDatastore struct { 582 delegate Datastore 583 } 584 585 func (f fakeDatastore) Unwrap() Datastore { 586 return f.delegate 587 } 588 589 func (f fakeDatastore) SnapshotReader(_ Revision) Reader { 590 return nil 591 } 592 593 func (f fakeDatastore) ReadWriteTx(_ context.Context, _ TxUserFunc, _ ...options.RWTOptionsOption) (Revision, error) { 594 return nil, nil 595 } 596 597 func (f fakeDatastore) OptimizedRevision(_ context.Context) (Revision, error) { 598 return nil, nil 599 } 600 601 func (f fakeDatastore) HeadRevision(_ context.Context) (Revision, error) { 602 return nil, nil 603 } 604 605 func (f fakeDatastore) CheckRevision(_ context.Context, _ Revision) error { 606 return nil 607 } 608 609 func (f fakeDatastore) RevisionFromString(_ string) (Revision, error) { 610 return nil, nil 611 } 612 613 func (f fakeDatastore) Watch(_ context.Context, _ Revision, _ WatchOptions) (<-chan *RevisionChanges, <-chan error) { 614 return nil, nil 615 } 616 617 func (f fakeDatastore) ReadyState(_ context.Context) (ReadyState, error) { 618 return ReadyState{}, nil 619 } 620 621 func (f fakeDatastore) Features(_ context.Context) (*Features, error) { 622 return nil, nil 623 } 624 625 func (f fakeDatastore) Statistics(_ context.Context) (Stats, error) { 626 return Stats{}, nil 627 } 628 629 func (f fakeDatastore) Close() error { 630 return nil 631 }