github.com/openfga/openfga@v1.5.4-rc1/pkg/server/server_test.go (about) 1 package server 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "math" 8 "os" 9 "path" 10 "runtime" 11 "strconv" 12 "sync" 13 "testing" 14 "time" 15 16 "github.com/oklog/ulid/v2" 17 openfgav1 "github.com/openfga/api/proto/openfga/v1" 18 language "github.com/openfga/language/pkg/go/transformer" 19 "github.com/stretchr/testify/require" 20 "go.uber.org/goleak" 21 "go.uber.org/mock/gomock" 22 "google.golang.org/grpc" 23 "google.golang.org/grpc/codes" 24 "google.golang.org/grpc/status" 25 26 "github.com/openfga/openfga/cmd/migrate" 27 "github.com/openfga/openfga/cmd/util" 28 "github.com/openfga/openfga/internal/build" 29 "github.com/openfga/openfga/internal/graph" 30 mockstorage "github.com/openfga/openfga/internal/mocks" 31 serverconfig "github.com/openfga/openfga/internal/server/config" 32 "github.com/openfga/openfga/pkg/server/commands" 33 serverErrors "github.com/openfga/openfga/pkg/server/errors" 34 "github.com/openfga/openfga/pkg/server/test" 35 "github.com/openfga/openfga/pkg/storage" 36 "github.com/openfga/openfga/pkg/storage/memory" 37 "github.com/openfga/openfga/pkg/storage/mysql" 38 "github.com/openfga/openfga/pkg/storage/postgres" 39 "github.com/openfga/openfga/pkg/storage/sqlcommon" 40 "github.com/openfga/openfga/pkg/storage/storagewrappers" 41 storagefixtures "github.com/openfga/openfga/pkg/testfixtures/storage" 42 "github.com/openfga/openfga/pkg/testutils" 43 "github.com/openfga/openfga/pkg/tuple" 44 "github.com/openfga/openfga/pkg/typesystem" 45 ) 46 47 func init() { 48 _, filename, _, _ := runtime.Caller(0) 49 dir := path.Join(path.Dir(filename), "..", "..") 50 err := os.Chdir(dir) 51 if err != nil { 52 panic(err) 53 } 54 } 55 56 func ExampleNewServerWithOpts() { 57 datastore := memory.New() // other supported datastores include Postgres and MySQL 58 defer datastore.Close() 59 60 openfga, err := NewServerWithOpts(WithDatastore(datastore), 61 WithCheckQueryCacheEnabled(true), 62 // more options available 63 ) 64 if err != nil { 65 panic(err) 66 } 67 defer openfga.Close() 68 69 // create store 70 store, err := openfga.CreateStore(context.Background(), 71 &openfgav1.CreateStoreRequest{Name: "demo"}) 72 if err != nil { 73 panic(err) 74 } 75 76 model := language.MustTransformDSLToProto(` 77 model 78 schema 1.1 79 type user 80 81 type document 82 relations 83 define reader: [user]`) 84 85 // write the model to the store 86 authorizationModel, err := openfga.WriteAuthorizationModel(context.Background(), &openfgav1.WriteAuthorizationModelRequest{ 87 StoreId: store.GetId(), 88 TypeDefinitions: model.GetTypeDefinitions(), 89 Conditions: model.GetConditions(), 90 SchemaVersion: model.GetSchemaVersion(), 91 }) 92 if err != nil { 93 panic(err) 94 } 95 96 // write tuples to the store 97 _, err = openfga.Write(context.Background(), &openfgav1.WriteRequest{ 98 StoreId: store.GetId(), 99 Writes: &openfgav1.WriteRequestWrites{ 100 TupleKeys: []*openfgav1.TupleKey{ 101 {Object: "document:budget", Relation: "reader", User: "user:anne"}, 102 }, 103 }, 104 Deletes: nil, 105 }) 106 if err != nil { 107 panic(err) 108 } 109 110 // make an authorization check 111 checkResponse, err := openfga.Check(context.Background(), &openfgav1.CheckRequest{ 112 StoreId: store.GetId(), 113 AuthorizationModelId: authorizationModel.GetAuthorizationModelId(), // optional, but recommended for speed 114 TupleKey: &openfgav1.CheckRequestTupleKey{ 115 User: "user:anne", 116 Relation: "reader", 117 Object: "document:budget", 118 }, 119 }) 120 if err != nil { 121 panic(err) 122 } 123 fmt.Println(checkResponse.GetAllowed()) 124 // Output: true 125 } 126 127 func TestServerPanicIfNoDatastore(t *testing.T) { 128 require.PanicsWithError(t, "failed to construct the OpenFGA server: a datastore option must be provided", func() { 129 _ = MustNewServerWithOpts() 130 }) 131 } 132 133 func TestServerNotReadyDueToDatastoreRevision(t *testing.T) { 134 engines := []string{"postgres", "mysql"} 135 136 for _, engine := range engines { 137 t.Run(engine, func(t *testing.T) { 138 _, ds, uri := util.MustBootstrapDatastore(t, engine) 139 140 targetVersion := build.MinimumSupportedDatastoreSchemaRevision - 1 141 142 migrateCommand := migrate.NewMigrateCommand() 143 144 migrateCommand.SetArgs([]string{"--datastore-engine", engine, "--datastore-uri", uri, "--version", strconv.Itoa(int(targetVersion))}) 145 146 err := migrateCommand.Execute() 147 require.NoError(t, err) 148 149 status, _ := ds.IsReady(context.Background()) 150 require.Contains(t, status.Message, fmt.Sprintf("datastore requires migrations: at revision '%d', but requires '%d'.", targetVersion, build.MinimumSupportedDatastoreSchemaRevision)) 151 require.False(t, status.IsReady) 152 }) 153 } 154 } 155 156 func TestServerPanicIfEmptyRequestDurationDatastoreCountBuckets(t *testing.T) { 157 require.PanicsWithError(t, "failed to construct the OpenFGA server: request duration datastore count buckets must not be empty", func() { 158 mockController := gomock.NewController(t) 159 defer mockController.Finish() 160 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 161 _ = MustNewServerWithOpts( 162 WithDatastore(mockDatastore), 163 WithRequestDurationByQueryHistogramBuckets([]uint{}), 164 ) 165 }) 166 } 167 168 func TestServerPanicIfEmptyRequestDurationDispatchCountBuckets(t *testing.T) { 169 require.PanicsWithError(t, "failed to construct the OpenFGA server: request duration by dispatch count buckets must not be empty", func() { 170 mockController := gomock.NewController(t) 171 defer mockController.Finish() 172 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 173 _ = MustNewServerWithOpts( 174 WithDatastore(mockDatastore), 175 WithRequestDurationByDispatchCountHistogramBuckets([]uint{}), 176 ) 177 }) 178 } 179 180 func TestServerPanicIfDefaultDispatchThresholdGreaterThanMaxDispatchThreshold(t *testing.T) { 181 require.PanicsWithError(t, "failed to construct the OpenFGA server: default dispatch throttling threshold must be equal or smaller than max dispatch threshold", func() { 182 mockController := gomock.NewController(t) 183 defer mockController.Finish() 184 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 185 _ = MustNewServerWithOpts( 186 WithDatastore(mockDatastore), 187 WithDispatchThrottlingCheckResolverEnabled(true), 188 WithDispatchThrottlingCheckResolverThreshold(100), 189 WithDispatchThrottlingCheckResolverMaxThreshold(80), 190 ) 191 }) 192 } 193 194 func TestServerWithPostgresDatastore(t *testing.T) { 195 t.Cleanup(func() { 196 goleak.VerifyNone(t) 197 }) 198 _, ds, _ := util.MustBootstrapDatastore(t, "postgres") 199 200 test.RunAllTests(t, ds) 201 } 202 203 func TestServerWithPostgresDatastoreAndExplicitCredentials(t *testing.T) { 204 t.Cleanup(func() { 205 goleak.VerifyNone(t) 206 }) 207 testDatastore := storagefixtures.RunDatastoreTestContainer(t, "postgres") 208 209 uri := testDatastore.GetConnectionURI(false) 210 ds, err := postgres.New( 211 uri, 212 sqlcommon.NewConfig( 213 sqlcommon.WithUsername(testDatastore.GetUsername()), 214 sqlcommon.WithPassword(testDatastore.GetPassword()), 215 ), 216 ) 217 require.NoError(t, err) 218 defer ds.Close() 219 220 test.RunAllTests(t, ds) 221 } 222 223 func TestServerWithMemoryDatastore(t *testing.T) { 224 t.Cleanup(func() { 225 goleak.VerifyNone(t) 226 }) 227 _, ds, _ := util.MustBootstrapDatastore(t, "memory") 228 229 test.RunAllTests(t, ds) 230 } 231 232 func TestServerWithMySQLDatastore(t *testing.T) { 233 t.Cleanup(func() { 234 goleak.VerifyNone(t) 235 }) 236 _, ds, _ := util.MustBootstrapDatastore(t, "mysql") 237 238 test.RunAllTests(t, ds) 239 } 240 241 func TestServerWithMySQLDatastoreAndExplicitCredentials(t *testing.T) { 242 t.Cleanup(func() { 243 goleak.VerifyNone(t) 244 }) 245 testDatastore := storagefixtures.RunDatastoreTestContainer(t, "mysql") 246 247 uri := testDatastore.GetConnectionURI(false) 248 ds, err := mysql.New( 249 uri, 250 sqlcommon.NewConfig( 251 sqlcommon.WithUsername(testDatastore.GetUsername()), 252 sqlcommon.WithPassword(testDatastore.GetPassword()), 253 ), 254 ) 255 require.NoError(t, err) 256 defer ds.Close() 257 258 test.RunAllTests(t, ds) 259 } 260 261 func TestCheckResolverOuterLayerDefault(t *testing.T) { 262 t.Cleanup(func() { 263 goleak.VerifyNone(t) 264 }) 265 266 _, ds, _ := util.MustBootstrapDatastore(t, "memory") 267 268 s := MustNewServerWithOpts( 269 WithDatastore(ds), 270 ) 271 t.Cleanup(s.Close) 272 273 // the default (outer most layer) of the CheckResolver 274 // composition should always be CycleDetectionCheckResolver. 275 _, ok := s.checkResolver.(*graph.CycleDetectionCheckResolver) 276 require.True(t, ok) 277 } 278 279 func TestAvoidDeadlockAcrossCheckRequests(t *testing.T) { 280 t.Cleanup(func() { 281 goleak.VerifyNone(t) 282 }) 283 284 _, ds, _ := util.MustBootstrapDatastore(t, "memory") 285 286 s := MustNewServerWithOpts( 287 WithDatastore(ds), 288 ) 289 t.Cleanup(s.Close) 290 291 createStoreResp, err := s.CreateStore(context.Background(), &openfgav1.CreateStoreRequest{ 292 Name: "openfga-test", 293 }) 294 require.NoError(t, err) 295 296 storeID := createStoreResp.GetId() 297 298 model := testutils.MustTransformDSLToProtoWithID(`model 299 schema 1.1 300 301 type user 302 303 type document 304 relations 305 define viewer: [user, document#viewer] or editor 306 define editor: [user, document#viewer]`) 307 308 writeAuthModelResp, err := s.WriteAuthorizationModel(context.Background(), &openfgav1.WriteAuthorizationModelRequest{ 309 StoreId: storeID, 310 SchemaVersion: model.GetSchemaVersion(), 311 TypeDefinitions: model.GetTypeDefinitions(), 312 }) 313 require.NoError(t, err) 314 315 modelID := writeAuthModelResp.GetAuthorizationModelId() 316 317 _, err = s.Write(context.Background(), &openfgav1.WriteRequest{ 318 StoreId: storeID, 319 Writes: &openfgav1.WriteRequestWrites{ 320 TupleKeys: []*openfgav1.TupleKey{ 321 tuple.NewTupleKey("document:1", "editor", "document:1#viewer"), 322 tuple.NewTupleKey("document:1", "editor", "user:andres"), 323 }, 324 }, 325 }) 326 require.NoError(t, err) 327 328 var wg sync.WaitGroup 329 330 wg.Add(3) 331 332 var resp1 *openfgav1.CheckResponse 333 var err1 error 334 go func() { 335 defer wg.Done() 336 337 resp1, err1 = s.Check(context.Background(), &openfgav1.CheckRequest{ 338 StoreId: storeID, 339 AuthorizationModelId: modelID, 340 TupleKey: tuple.NewCheckRequestTupleKey("document:1", "editor", "user:jon"), 341 }) 342 }() 343 344 var resp2 *openfgav1.CheckResponse 345 var err2 error 346 go func() { 347 defer wg.Done() 348 349 resp2, err2 = s.Check(context.Background(), &openfgav1.CheckRequest{ 350 StoreId: storeID, 351 AuthorizationModelId: modelID, 352 TupleKey: tuple.NewCheckRequestTupleKey("document:1", "viewer", "user:jon"), 353 }) 354 }() 355 356 var resp3 *openfgav1.CheckResponse 357 var err3 error 358 go func() { 359 defer wg.Done() 360 361 resp3, err3 = s.Check(context.Background(), &openfgav1.CheckRequest{ 362 StoreId: storeID, 363 AuthorizationModelId: modelID, 364 TupleKey: tuple.NewCheckRequestTupleKey("document:1", "viewer", "user:andres"), 365 }) 366 }() 367 368 wg.Wait() 369 370 require.NoError(t, err1) 371 require.NotNil(t, resp1) 372 require.False(t, resp1.GetAllowed()) 373 374 require.NoError(t, err2) 375 require.NotNil(t, resp2) 376 377 require.NoError(t, err3) 378 require.NotNil(t, resp3) 379 require.True(t, resp3.GetAllowed()) 380 } 381 382 func TestAvoidDeadlockWithinSingleCheckRequest(t *testing.T) { 383 t.Cleanup(func() { 384 goleak.VerifyNone(t) 385 }) 386 387 _, ds, _ := util.MustBootstrapDatastore(t, "memory") 388 389 s := MustNewServerWithOpts( 390 WithDatastore(ds), 391 ) 392 t.Cleanup(s.Close) 393 394 createStoreResp, err := s.CreateStore(context.Background(), &openfgav1.CreateStoreRequest{ 395 Name: "openfga-test", 396 }) 397 require.NoError(t, err) 398 399 storeID := createStoreResp.GetId() 400 401 model := testutils.MustTransformDSLToProtoWithID(`model 402 schema 1.1 403 404 type user 405 406 type document 407 relations 408 define editor1: [user, document#viewer1] 409 410 define viewer2: [document#viewer1] or editor1 411 define viewer1: [user] or viewer2 412 define can_view: viewer1 or editor1`) 413 414 writeAuthModelResp, err := s.WriteAuthorizationModel(context.Background(), &openfgav1.WriteAuthorizationModelRequest{ 415 StoreId: storeID, 416 SchemaVersion: model.GetSchemaVersion(), 417 TypeDefinitions: model.GetTypeDefinitions(), 418 }) 419 require.NoError(t, err) 420 421 modelID := writeAuthModelResp.GetAuthorizationModelId() 422 423 _, err = s.Write(context.Background(), &openfgav1.WriteRequest{ 424 StoreId: storeID, 425 Writes: &openfgav1.WriteRequestWrites{ 426 TupleKeys: []*openfgav1.TupleKey{ 427 tuple.NewTupleKey("document:1", "editor1", "document:1#viewer1"), 428 }, 429 }, 430 }) 431 require.NoError(t, err) 432 433 resp, err := s.Check(context.Background(), &openfgav1.CheckRequest{ 434 StoreId: storeID, 435 AuthorizationModelId: modelID, 436 TupleKey: tuple.NewCheckRequestTupleKey("document:1", "can_view", "user:jon"), 437 }) 438 439 require.NoError(t, err) 440 require.NotNil(t, resp) 441 require.False(t, resp.GetAllowed()) 442 } 443 444 func TestThreeProngThroughVariousLayers(t *testing.T) { 445 t.Cleanup(func() { 446 goleak.VerifyNone(t) 447 }) 448 449 _, ds, _ := util.MustBootstrapDatastore(t, "memory") 450 451 s := MustNewServerWithOpts( 452 WithDatastore(ds), 453 ) 454 t.Cleanup(func() { 455 s.Close() 456 }) 457 458 createStoreResp, err := s.CreateStore(context.Background(), &openfgav1.CreateStoreRequest{ 459 Name: "openfga-test", 460 }) 461 require.NoError(t, err) 462 463 storeID := createStoreResp.GetId() 464 465 model := testutils.MustTransformDSLToProtoWithID(`model 466 schema 1.1 467 468 type user 469 type module 470 relations 471 define owner: [user] or owner from parent 472 define parent: [document, module] 473 define viewer: [user] or owner or viewer from parent 474 type folder 475 relations 476 define owner: [user] or owner from parent 477 define parent: [module, folder] 478 define viewer: [user] or owner or viewer from parent 479 type document 480 relations 481 define owner: [user] or owner from parent 482 define parent: [folder, document] 483 define viewer: [user] or owner or viewer from parent`) 484 485 writeAuthModelResp, err := s.WriteAuthorizationModel(context.Background(), &openfgav1.WriteAuthorizationModelRequest{ 486 StoreId: storeID, 487 SchemaVersion: model.GetSchemaVersion(), 488 TypeDefinitions: model.GetTypeDefinitions(), 489 }) 490 require.NoError(t, err) 491 492 modelID := writeAuthModelResp.GetAuthorizationModelId() 493 494 _, err = s.Write(context.Background(), &openfgav1.WriteRequest{ 495 StoreId: storeID, 496 Writes: &openfgav1.WriteRequestWrites{ 497 TupleKeys: []*openfgav1.TupleKey{ 498 tuple.NewTupleKey("module:a", "owner", "user:anne"), 499 tuple.NewTupleKey("folder:a", "parent", "module:a"), 500 tuple.NewTupleKey("document:a", "parent", "folder:a"), 501 tuple.NewTupleKey("module:b", "parent", "document:a"), 502 tuple.NewTupleKey("folder:b", "parent", "module:b"), 503 tuple.NewTupleKey("document:b", "parent", "folder:b"), 504 tuple.NewTupleKey("module:a", "parent", "document:b"), 505 }, 506 }, 507 }) 508 require.NoError(t, err) 509 510 for i := 0; i < 100; i++ { 511 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 512 tupleKeys := []*openfgav1.CheckRequestTupleKey{ 513 tuple.NewCheckRequestTupleKey("module:a", "viewer", "user:anne"), 514 tuple.NewCheckRequestTupleKey("module:b", "viewer", "user:anne"), 515 tuple.NewCheckRequestTupleKey("folder:a", "viewer", "user:anne"), 516 tuple.NewCheckRequestTupleKey("folder:b", "viewer", "user:anne"), 517 tuple.NewCheckRequestTupleKey("document:a", "viewer", "user:anne"), 518 tuple.NewCheckRequestTupleKey("document:b", "viewer", "user:anne"), 519 } 520 521 for _, tupleKey := range tupleKeys { 522 resp, err := s.Check(context.Background(), &openfgav1.CheckRequest{ 523 StoreId: storeID, 524 AuthorizationModelId: modelID, 525 TupleKey: tupleKey, 526 }) 527 require.NoError(t, err) 528 require.True(t, resp.GetAllowed()) 529 } 530 }) 531 } 532 } 533 534 func TestCheckDispatchThrottledTimeout(t *testing.T) { 535 t.Cleanup(func() { 536 goleak.VerifyNone(t) 537 }) 538 539 const dispatchFrequency = 5 * time.Millisecond 540 const dispatchThreshold = 5 541 542 _, ds, _ := util.MustBootstrapDatastore(t, "memory") 543 s := MustNewServerWithOpts( 544 WithDatastore(ds), 545 WithDispatchThrottlingCheckResolverFrequency(dispatchFrequency), 546 WithDispatchThrottlingCheckResolverEnabled(true), 547 WithDispatchThrottlingCheckResolverThreshold(dispatchThreshold), 548 ) 549 t.Cleanup(s.Close) 550 551 createStoreResp, err := s.CreateStore(context.Background(), &openfgav1.CreateStoreRequest{ 552 Name: "openfga-test", 553 }) 554 require.NoError(t, err) 555 556 storeID := createStoreResp.GetId() 557 558 model := testutils.MustTransformDSLToProtoWithID(`model 559 schema 1.1 560 561 type user 562 563 type group 564 relations 565 define member: [user, group#member] 566 `) 567 568 writeAuthModelResp, err := s.WriteAuthorizationModel(context.Background(), &openfgav1.WriteAuthorizationModelRequest{ 569 StoreId: storeID, 570 SchemaVersion: model.GetSchemaVersion(), 571 TypeDefinitions: model.GetTypeDefinitions(), 572 }) 573 require.NoError(t, err) 574 575 modelID := writeAuthModelResp.GetAuthorizationModelId() 576 577 _, err = s.Write(context.Background(), &openfgav1.WriteRequest{ 578 StoreId: storeID, 579 Writes: &openfgav1.WriteRequestWrites{ 580 TupleKeys: []*openfgav1.TupleKey{ 581 tuple.NewTupleKey("group:x", "member", "group:1#member"), 582 tuple.NewTupleKey("group:x", "member", "group:2#member"), 583 tuple.NewTupleKey("group:x", "member", "group:3#member"), 584 tuple.NewTupleKey("group:x", "member", "group:4#member"), 585 tuple.NewTupleKey("group:x", "member", "group:5#member"), 586 tuple.NewTupleKey("group:x", "member", "group:6#member"), 587 tuple.NewTupleKey("group:x", "member", "group:7#member"), 588 tuple.NewTupleKey("group:x", "member", "group:8#member"), 589 tuple.NewTupleKey("group:x", "member", "group:9#member"), 590 tuple.NewTupleKey("group:x", "member", "group:10#member"), 591 }, 592 }, 593 }) 594 require.NoError(t, err) 595 596 // we know that the above query will take 10 dispatches 597 // Since the threshold level is 5 and each dispatch will be throttled by 5ms 598 // The request will take at least 25 ms and will be timeout since timeout is 20ms. 599 ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) 600 defer cancel() 601 602 _, err = s.Check(ctx, &openfgav1.CheckRequest{ 603 StoreId: storeID, 604 AuthorizationModelId: modelID, 605 TupleKey: tuple.NewCheckRequestTupleKey("group:x", "member", "user:anne"), 606 }) 607 require.ErrorIs(t, err, serverErrors.ThrottledTimeout) 608 } 609 610 func BenchmarkOpenFGAServer(b *testing.B) { 611 b.Cleanup(func() { 612 goleak.VerifyNone(b, 613 // https://github.com/uber-go/goleak/discussions/89 614 goleak.IgnoreTopFunction("testing.(*B).run1"), 615 goleak.IgnoreTopFunction("testing.(*B).doBench"), 616 ) 617 }) 618 b.Run("BenchmarkPostgresDatastore", func(b *testing.B) { 619 testDatastore := storagefixtures.RunDatastoreTestContainer(b, "postgres") 620 621 uri := testDatastore.GetConnectionURI(true) 622 ds, err := postgres.New(uri, sqlcommon.NewConfig()) 623 require.NoError(b, err) 624 b.Cleanup(ds.Close) 625 test.RunAllBenchmarks(b, ds) 626 }) 627 628 b.Run("BenchmarkMemoryDatastore", func(b *testing.B) { 629 ds := memory.New() 630 b.Cleanup(ds.Close) 631 test.RunAllBenchmarks(b, ds) 632 }) 633 634 b.Run("BenchmarkMySQLDatastore", func(b *testing.B) { 635 testDatastore := storagefixtures.RunDatastoreTestContainer(b, "mysql") 636 637 uri := testDatastore.GetConnectionURI(true) 638 ds, err := mysql.New(uri, sqlcommon.NewConfig()) 639 require.NoError(b, err) 640 b.Cleanup(ds.Close) 641 test.RunAllBenchmarks(b, ds) 642 }) 643 } 644 645 func TestCheckDoesNotThrowBecauseDirectTupleWasFound(t *testing.T) { 646 t.Cleanup(func() { 647 goleak.VerifyNone(t) 648 }) 649 ctx := context.Background() 650 storeID := ulid.Make().String() 651 modelID := ulid.Make().String() 652 653 typedefs := language.MustTransformDSLToProto(`model 654 schema 1.1 655 type user 656 657 type repo 658 relations 659 define reader: [user] 660 `).GetTypeDefinitions() 661 662 tk := tuple.NewCheckRequestTupleKey("repo:openfga", "reader", "user:anne") 663 returnedTuple := &openfgav1.Tuple{Key: tuple.ConvertCheckRequestTupleKeyToTupleKey(tk)} 664 665 mockController := gomock.NewController(t) 666 defer mockController.Finish() 667 668 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 669 670 mockDatastore.EXPECT(). 671 ReadAuthorizationModel(gomock.Any(), storeID, modelID). 672 AnyTimes(). 673 Return(&openfgav1.AuthorizationModel{ 674 SchemaVersion: typesystem.SchemaVersion1_1, 675 TypeDefinitions: typedefs, 676 }, nil) 677 678 // it could happen that one of the following two mocks won't be necessary because the goroutine will be short-circuited 679 mockDatastore.EXPECT(). 680 ReadUserTuple(gomock.Any(), storeID, gomock.Any()). 681 AnyTimes(). 682 Return(returnedTuple, nil) 683 684 mockDatastore.EXPECT(). 685 ReadUsersetTuples(gomock.Any(), storeID, gomock.Any()). 686 AnyTimes(). 687 DoAndReturn( 688 func(_ context.Context, _ string, _ storage.ReadUsersetTuplesFilter) (storage.TupleIterator, error) { 689 time.Sleep(50 * time.Millisecond) 690 return nil, errors.New("some error") 691 }) 692 693 s := MustNewServerWithOpts( 694 WithDatastore(mockDatastore), 695 ) 696 t.Cleanup(s.Close) 697 698 checkResponse, err := s.Check(ctx, &openfgav1.CheckRequest{ 699 StoreId: storeID, 700 TupleKey: tk, 701 AuthorizationModelId: modelID, 702 }) 703 require.NoError(t, err) 704 require.True(t, checkResponse.GetAllowed()) 705 } 706 707 func TestReleasesConnections(t *testing.T) { 708 t.Cleanup(func() { 709 goleak.VerifyNone(t) 710 }) 711 712 testDatastore := storagefixtures.RunDatastoreTestContainer(t, "postgres") 713 714 uri := testDatastore.GetConnectionURI(true) 715 ds, err := postgres.New(uri, sqlcommon.NewConfig( 716 sqlcommon.WithMaxOpenConns(1), 717 sqlcommon.WithMaxTuplesPerWrite(2000), 718 )) 719 require.NoError(t, err) 720 defer ds.Close() 721 722 s := MustNewServerWithOpts( 723 WithDatastore(storagewrappers.NewContextWrapper(ds)), 724 WithExperimentals(ExperimentalEnableListUsers), 725 ) 726 t.Cleanup(s.Close) 727 728 storeID := ulid.Make().String() 729 730 writeAuthzModelResp, err := s.WriteAuthorizationModel(context.Background(), &openfgav1.WriteAuthorizationModelRequest{ 731 StoreId: storeID, 732 TypeDefinitions: language.MustTransformDSLToProto(`model 733 schema 1.1 734 type user 735 736 type document 737 relations 738 define editor: [user]`).GetTypeDefinitions(), 739 SchemaVersion: typesystem.SchemaVersion1_1, 740 }) 741 require.NoError(t, err) 742 743 modelID := writeAuthzModelResp.GetAuthorizationModelId() 744 745 numTuples := 2000 746 747 t.Run("list_objects", func(t *testing.T) { 748 tuples := make([]*openfgav1.TupleKey, 0, numTuples) 749 for i := 0; i < numTuples; i++ { 750 tk := tuple.NewTupleKey(fmt.Sprintf("document:%d", i), "editor", "user:jon") 751 752 tuples = append(tuples, tk) 753 } 754 755 _, err = s.Write(context.Background(), &openfgav1.WriteRequest{ 756 StoreId: storeID, 757 AuthorizationModelId: modelID, 758 Writes: &openfgav1.WriteRequestWrites{ 759 TupleKeys: tuples, 760 }, 761 }) 762 require.NoError(t, err) 763 764 _, err = s.ListObjects(context.Background(), &openfgav1.ListObjectsRequest{ 765 StoreId: storeID, 766 AuthorizationModelId: modelID, 767 Type: "document", 768 Relation: "editor", 769 User: "user:jon", 770 }) 771 require.NoError(t, err) 772 773 timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 3*time.Second) 774 defer timeoutCancel() 775 776 // If ListObjects is still hogging the database connection pool even after responding, then this fails. 777 // If ListObjects is closing up its connections effectively then this will not fail. 778 status, err := ds.IsReady(timeoutCtx) 779 require.NoError(t, err) 780 require.True(t, status.IsReady) 781 }) 782 783 t.Run("list_users", func(t *testing.T) { 784 tuples := make([]*openfgav1.TupleKey, 0, numTuples) 785 for i := 0; i < numTuples; i++ { 786 tk := tuple.NewTupleKey("document:1", "editor", fmt.Sprintf("user:%d", i)) 787 788 tuples = append(tuples, tk) 789 } 790 791 _, err = s.Write(context.Background(), &openfgav1.WriteRequest{ 792 StoreId: storeID, 793 AuthorizationModelId: modelID, 794 Writes: &openfgav1.WriteRequestWrites{ 795 TupleKeys: tuples, 796 }, 797 }) 798 require.NoError(t, err) 799 800 _, err = s.ListUsers(context.Background(), &openfgav1.ListUsersRequest{ 801 StoreId: storeID, 802 AuthorizationModelId: modelID, 803 Relation: "editor", 804 Object: &openfgav1.Object{ 805 Type: "document", 806 Id: "1", 807 }, 808 UserFilters: []*openfgav1.UserTypeFilter{{Type: "user"}}, 809 }) 810 require.NoError(t, err) 811 812 timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 3*time.Second) 813 defer timeoutCancel() 814 815 // If ListUsers is still hogging the database connection pool even after responding, then this fails. 816 // If ListUsers is closing up its connections effectively then this will not fail. 817 status, err := ds.IsReady(timeoutCtx) 818 require.NoError(t, err) 819 require.True(t, status.IsReady) 820 }) 821 } 822 823 func TestOperationsWithInvalidModel(t *testing.T) { 824 t.Cleanup(func() { 825 goleak.VerifyNone(t) 826 }) 827 828 ctx := context.Background() 829 storeID := ulid.Make().String() 830 modelID := ulid.Make().String() 831 832 // The model is invalid 833 typedefs := language.MustTransformDSLToProto(`model 834 schema 1.1 835 type user 836 837 type repo 838 relations 839 define admin: [user] 840 define r1: [user] and r2 and r3 841 define r2: [user] and r1 and r3 842 define r3: [user] and r1 and r2`).GetTypeDefinitions() 843 844 tk := tuple.NewCheckRequestTupleKey("repo:openfga", "r1", "user:anne") 845 mockController := gomock.NewController(t) 846 defer mockController.Finish() 847 848 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 849 850 mockDatastore.EXPECT(). 851 ReadAuthorizationModel(gomock.Any(), storeID, modelID). 852 AnyTimes(). 853 Return(&openfgav1.AuthorizationModel{ 854 Id: modelID, 855 SchemaVersion: typesystem.SchemaVersion1_1, 856 TypeDefinitions: typedefs, 857 }, nil) 858 859 // the model is error and err should return 860 861 s := MustNewServerWithOpts( 862 WithDatastore(mockDatastore), 863 WithExperimentals(ExperimentalEnableListUsers), 864 ) 865 t.Cleanup(s.Close) 866 867 _, err := s.Check(ctx, &openfgav1.CheckRequest{ 868 StoreId: storeID, 869 TupleKey: tk, 870 AuthorizationModelId: modelID, 871 }) 872 require.Error(t, err) 873 e, ok := status.FromError(err) 874 require.True(t, ok) 875 require.Equal(t, codes.Code(openfgav1.ErrorCode_validation_error), e.Code()) 876 877 _, err = s.ListObjects(ctx, &openfgav1.ListObjectsRequest{ 878 StoreId: storeID, 879 AuthorizationModelId: modelID, 880 Type: "repo", 881 Relation: "r1", 882 User: "user:anne", 883 }) 884 require.Error(t, err) 885 e, ok = status.FromError(err) 886 require.True(t, ok) 887 require.Equal(t, codes.Code(openfgav1.ErrorCode_validation_error), e.Code()) 888 889 err = s.StreamedListObjects(&openfgav1.StreamedListObjectsRequest{ 890 StoreId: storeID, 891 AuthorizationModelId: modelID, 892 Type: "repo", 893 Relation: "r1", 894 User: "user:anne", 895 }, NewMockStreamServer()) 896 require.Error(t, err) 897 e, ok = status.FromError(err) 898 require.True(t, ok) 899 require.Equal(t, codes.Code(openfgav1.ErrorCode_validation_error), e.Code()) 900 901 _, err = s.ListUsers(ctx, &openfgav1.ListUsersRequest{ 902 StoreId: storeID, 903 AuthorizationModelId: modelID, 904 Relation: tk.GetRelation(), 905 Object: &openfgav1.Object{Type: "repo", Id: "openfga"}, 906 UserFilters: []*openfgav1.UserTypeFilter{{Type: "user"}}, 907 }) 908 require.Error(t, err) 909 e, ok = status.FromError(err) 910 require.True(t, ok) 911 require.Equal(t, codes.Code(openfgav1.ErrorCode_validation_error), e.Code()) 912 913 _, err = s.Expand(ctx, &openfgav1.ExpandRequest{ 914 StoreId: storeID, 915 AuthorizationModelId: modelID, 916 TupleKey: tuple.NewExpandRequestTupleKey(tk.GetObject(), tk.GetRelation()), 917 }) 918 require.Error(t, err) 919 e, ok = status.FromError(err) 920 require.True(t, ok) 921 require.Equal(t, codes.Code(openfgav1.ErrorCode_validation_error), e.Code()) 922 } 923 924 func TestShortestPathToSolutionWins(t *testing.T) { 925 t.Cleanup(func() { 926 goleak.VerifyNone(t) 927 }) 928 929 ctx := context.Background() 930 931 storeID := ulid.Make().String() 932 modelID := ulid.Make().String() 933 934 typedefs := language.MustTransformDSLToProto(`model 935 schema 1.1 936 type user 937 938 type repo 939 relations 940 define reader: [user:*]`).GetTypeDefinitions() 941 942 tk := tuple.NewCheckRequestTupleKey("repo:openfga", "reader", "user:*") 943 returnedTuple := &openfgav1.Tuple{Key: tuple.ConvertCheckRequestTupleKeyToTupleKey(tk)} 944 945 mockController := gomock.NewController(t) 946 defer mockController.Finish() 947 948 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 949 950 mockDatastore.EXPECT(). 951 ReadAuthorizationModel(gomock.Any(), storeID, modelID). 952 AnyTimes(). 953 Return(&openfgav1.AuthorizationModel{ 954 SchemaVersion: typesystem.SchemaVersion1_1, 955 TypeDefinitions: typedefs, 956 }, nil) 957 958 // it could happen that one of the following two mocks won't be necessary because the goroutine will be short-circuited 959 mockDatastore.EXPECT(). 960 ReadUserTuple(gomock.Any(), storeID, gomock.Any()). 961 AnyTimes(). 962 DoAndReturn( 963 func(ctx context.Context, _ string, _ *openfgav1.TupleKey) (storage.TupleIterator, error) { 964 select { 965 case <-ctx.Done(): 966 return nil, ctx.Err() 967 case <-time.After(500 * time.Millisecond): 968 return nil, storage.ErrNotFound 969 } 970 }) 971 972 mockDatastore.EXPECT(). 973 ReadUsersetTuples(gomock.Any(), storeID, gomock.Any()). 974 AnyTimes(). 975 DoAndReturn( 976 func(_ context.Context, _ string, _ storage.ReadUsersetTuplesFilter) (storage.TupleIterator, error) { 977 time.Sleep(100 * time.Millisecond) 978 return storage.NewStaticTupleIterator([]*openfgav1.Tuple{returnedTuple}), nil 979 }) 980 981 s := MustNewServerWithOpts( 982 WithDatastore(mockDatastore), 983 ) 984 t.Cleanup(s.Close) 985 986 start := time.Now() 987 checkResponse, err := s.Check(ctx, &openfgav1.CheckRequest{ 988 StoreId: storeID, 989 TupleKey: tk, 990 AuthorizationModelId: modelID, 991 }) 992 end := time.Since(start) 993 994 // we expect the Check call to be short-circuited after ReadUsersetTuples runs 995 require.Lessf(t, end, 200*time.Millisecond, fmt.Sprintf("end was %s", end)) 996 require.NoError(t, err) 997 require.True(t, checkResponse.GetAllowed()) 998 } 999 1000 func TestCheckWithCachedResolution(t *testing.T) { 1001 t.Cleanup(func() { 1002 goleak.VerifyNone(t) 1003 }) 1004 1005 ctx := context.Background() 1006 1007 storeID := ulid.Make().String() 1008 modelID := ulid.Make().String() 1009 1010 typedefs := language.MustTransformDSLToProto(`model 1011 schema 1.1 1012 type user 1013 1014 type repo 1015 relations 1016 define reader: [user]`).GetTypeDefinitions() 1017 1018 tk := tuple.NewCheckRequestTupleKey("repo:openfga", "reader", "user:mike") 1019 returnedTuple := &openfgav1.Tuple{Key: tuple.ConvertCheckRequestTupleKeyToTupleKey(tk)} 1020 1021 mockController := gomock.NewController(t) 1022 defer mockController.Finish() 1023 1024 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 1025 1026 mockDatastore.EXPECT(). 1027 ReadAuthorizationModel(gomock.Any(), storeID, modelID). 1028 AnyTimes(). 1029 Return(&openfgav1.AuthorizationModel{ 1030 SchemaVersion: typesystem.SchemaVersion1_1, 1031 TypeDefinitions: typedefs, 1032 }, nil) 1033 1034 mockDatastore.EXPECT(). 1035 ReadUserTuple(gomock.Any(), storeID, gomock.Any()). 1036 Times(1). 1037 Return(returnedTuple, nil) 1038 1039 s := MustNewServerWithOpts( 1040 WithDatastore(mockDatastore), 1041 WithCheckQueryCacheEnabled(true), 1042 WithCheckQueryCacheLimit(10), 1043 WithCheckQueryCacheTTL(1*time.Minute), 1044 ) 1045 t.Cleanup(s.Close) 1046 1047 checkResponse, err := s.Check(ctx, &openfgav1.CheckRequest{ 1048 StoreId: storeID, 1049 TupleKey: tk, 1050 AuthorizationModelId: modelID, 1051 }) 1052 1053 require.NoError(t, err) 1054 require.True(t, checkResponse.GetAllowed()) 1055 1056 // If we check for the same request, data should come from cache and number of ReadUserTuple should still be 1 1057 checkResponse, err = s.Check(ctx, &openfgav1.CheckRequest{ 1058 StoreId: storeID, 1059 TupleKey: tk, 1060 AuthorizationModelId: modelID, 1061 }) 1062 1063 require.NoError(t, err) 1064 require.True(t, checkResponse.GetAllowed()) 1065 } 1066 1067 func TestWriteAssertionModelDSError(t *testing.T) { 1068 t.Cleanup(func() { 1069 goleak.VerifyNone(t) 1070 }) 1071 1072 ctx := context.Background() 1073 1074 storeID := ulid.Make().String() 1075 modelID := ulid.Make().String() 1076 1077 typedefs := language.MustTransformDSLToProto(`model 1078 schema 1.1 1079 type user 1080 1081 type repo 1082 relations 1083 define reader: [user]`).GetTypeDefinitions() 1084 1085 mockController := gomock.NewController(t) 1086 defer mockController.Finish() 1087 1088 mockDSOldSchema := mockstorage.NewMockOpenFGADatastore(mockController) 1089 1090 mockDSOldSchema.EXPECT(). 1091 ReadAuthorizationModel(gomock.Any(), storeID, modelID). 1092 AnyTimes(). 1093 Return(&openfgav1.AuthorizationModel{ 1094 SchemaVersion: typesystem.SchemaVersion1_0, 1095 TypeDefinitions: typedefs, 1096 }, nil) 1097 1098 mockDSBadReadAuthModel := mockstorage.NewMockOpenFGADatastore(mockController) 1099 1100 mockDSBadReadAuthModel.EXPECT(). 1101 ReadAuthorizationModel(gomock.Any(), storeID, modelID). 1102 AnyTimes(). 1103 Return(nil, fmt.Errorf("unable to read")) 1104 1105 mockDSBadWriteAssertions := mockstorage.NewMockOpenFGADatastore(mockController) 1106 mockDSBadWriteAssertions.EXPECT(). 1107 ReadAuthorizationModel(gomock.Any(), storeID, modelID). 1108 AnyTimes(). 1109 Return(&openfgav1.AuthorizationModel{ 1110 SchemaVersion: typesystem.SchemaVersion1_1, 1111 TypeDefinitions: typedefs, 1112 }, nil) 1113 mockDSBadWriteAssertions.EXPECT(). 1114 WriteAssertions(gomock.Any(), storeID, modelID, gomock.Any()). 1115 AnyTimes(). 1116 Return(fmt.Errorf("unable to write")) 1117 1118 tests := []struct { 1119 name string 1120 assertions []*openfgav1.Assertion 1121 mockDatastore *mockstorage.MockOpenFGADatastore 1122 expectedError error 1123 }{ 1124 { 1125 name: "unsupported_schema", 1126 assertions: []*openfgav1.Assertion{}, 1127 mockDatastore: mockDSOldSchema, 1128 expectedError: serverErrors.ValidationError( 1129 fmt.Errorf("invalid schema version"), 1130 ), 1131 }, 1132 { 1133 name: "failed_to_read", 1134 assertions: []*openfgav1.Assertion{}, 1135 mockDatastore: mockDSBadReadAuthModel, 1136 expectedError: serverErrors.NewInternalError( 1137 "", fmt.Errorf("unable to read"), 1138 ), 1139 }, 1140 { 1141 name: "failed_to_write", 1142 assertions: []*openfgav1.Assertion{}, 1143 mockDatastore: mockDSBadWriteAssertions, 1144 expectedError: serverErrors.NewInternalError( 1145 "", fmt.Errorf("unable to write"), 1146 ), 1147 }, 1148 } 1149 1150 for _, curTest := range tests { 1151 t.Run(curTest.name, func(t *testing.T) { 1152 request := &openfgav1.WriteAssertionsRequest{ 1153 StoreId: storeID, 1154 Assertions: curTest.assertions, 1155 AuthorizationModelId: modelID, 1156 } 1157 1158 writeAssertionCmd := commands.NewWriteAssertionsCommand(curTest.mockDatastore) 1159 _, err := writeAssertionCmd.Execute(ctx, request) 1160 require.ErrorIs(t, curTest.expectedError, err) 1161 }) 1162 } 1163 } 1164 1165 func TestReadAssertionModelDSError(t *testing.T) { 1166 t.Cleanup(func() { 1167 goleak.VerifyNone(t) 1168 }) 1169 1170 ctx := context.Background() 1171 1172 storeID := ulid.Make().String() 1173 modelID := ulid.Make().String() 1174 1175 mockController := gomock.NewController(t) 1176 defer mockController.Finish() 1177 1178 mockDSBadReadAssertions := mockstorage.NewMockOpenFGADatastore(mockController) 1179 mockDSBadReadAssertions.EXPECT(). 1180 ReadAssertions(gomock.Any(), storeID, modelID). 1181 AnyTimes(). 1182 Return(nil, fmt.Errorf("unable to read")) 1183 1184 readAssertionQuery := commands.NewReadAssertionsQuery(mockDSBadReadAssertions) 1185 _, err := readAssertionQuery.Execute(ctx, storeID, modelID) 1186 expectedError := serverErrors.NewInternalError( 1187 "", fmt.Errorf("unable to read"), 1188 ) 1189 require.ErrorIs(t, expectedError, err) 1190 } 1191 1192 func TestResolveAuthorizationModel(t *testing.T) { 1193 t.Cleanup(func() { 1194 goleak.VerifyNone(t) 1195 }) 1196 1197 ctx := context.Background() 1198 1199 t.Run("no_latest_authorization_model_id_found", func(t *testing.T) { 1200 store := ulid.Make().String() 1201 1202 mockController := gomock.NewController(t) 1203 defer mockController.Finish() 1204 1205 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 1206 mockDatastore.EXPECT().FindLatestAuthorizationModel(gomock.Any(), store).Return(nil, storage.ErrNotFound) 1207 1208 s := MustNewServerWithOpts( 1209 WithDatastore(mockDatastore), 1210 ) 1211 t.Cleanup(s.Close) 1212 1213 expectedError := serverErrors.LatestAuthorizationModelNotFound(store) 1214 1215 _, err := s.resolveTypesystem(ctx, store, "") 1216 require.ErrorIs(t, err, expectedError) 1217 }) 1218 1219 t.Run("read_existing_authorization_model", func(t *testing.T) { 1220 store := ulid.Make().String() 1221 modelID := ulid.Make().String() 1222 1223 mockController := gomock.NewController(t) 1224 defer mockController.Finish() 1225 1226 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 1227 mockDatastore.EXPECT().FindLatestAuthorizationModel(gomock.Any(), store).Return( 1228 &openfgav1.AuthorizationModel{ 1229 Id: modelID, 1230 SchemaVersion: typesystem.SchemaVersion1_1, 1231 }, 1232 nil, 1233 ) 1234 1235 s := MustNewServerWithOpts( 1236 WithDatastore(mockDatastore), 1237 ) 1238 t.Cleanup(s.Close) 1239 1240 typesys, err := s.resolveTypesystem(ctx, store, "") 1241 require.NoError(t, err) 1242 require.Equal(t, modelID, typesys.GetAuthorizationModelID()) 1243 }) 1244 1245 t.Run("non-valid_modelID_returns_error", func(t *testing.T) { 1246 store := ulid.Make().String() 1247 modelID := "foo" 1248 want := serverErrors.AuthorizationModelNotFound(modelID) 1249 1250 mockController := gomock.NewController(t) 1251 defer mockController.Finish() 1252 1253 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 1254 1255 s := MustNewServerWithOpts( 1256 WithDatastore(mockDatastore), 1257 ) 1258 t.Cleanup(s.Close) 1259 1260 _, err := s.resolveTypesystem(ctx, store, modelID) 1261 require.Equal(t, want, err) 1262 }) 1263 } 1264 1265 type mockStreamServer struct { 1266 grpc.ServerStream 1267 } 1268 1269 func NewMockStreamServer() *mockStreamServer { 1270 return &mockStreamServer{} 1271 } 1272 1273 func (m *mockStreamServer) Context() context.Context { 1274 return context.Background() 1275 } 1276 1277 func (m *mockStreamServer) Send(*openfgav1.StreamedListObjectsResponse) error { 1278 return nil 1279 } 1280 1281 // This runs ListObjects and StreamedListObjects many times over to ensure no race conditions (see https://github.com/openfga/openfga/pull/762) 1282 func BenchmarkListObjectsNoRaceCondition(b *testing.B) { 1283 b.Cleanup(func() { 1284 goleak.VerifyNone(b, 1285 // https://github.com/uber-go/goleak/discussions/89 1286 goleak.IgnoreTopFunction("testing.(*B).run1"), 1287 goleak.IgnoreTopFunction("testing.(*B).doBench"), 1288 ) 1289 }) 1290 ctx := context.Background() 1291 store := ulid.Make().String() 1292 modelID := ulid.Make().String() 1293 1294 mockController := gomock.NewController(b) 1295 defer mockController.Finish() 1296 1297 typedefs := language.MustTransformDSLToProto(`model 1298 schema 1.1 1299 type user 1300 1301 type repo 1302 relations 1303 define allowed: [user] 1304 define viewer: [user] and allowed`).GetTypeDefinitions() 1305 1306 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 1307 1308 mockDatastore.EXPECT().ReadAuthorizationModel(gomock.Any(), store, modelID).AnyTimes().Return(&openfgav1.AuthorizationModel{ 1309 SchemaVersion: typesystem.SchemaVersion1_1, 1310 TypeDefinitions: typedefs, 1311 }, nil) 1312 mockDatastore.EXPECT().ReadStartingWithUser(gomock.Any(), store, gomock.Any()).AnyTimes().Return(nil, errors.New("error reading from storage")) 1313 1314 s := MustNewServerWithOpts( 1315 WithDatastore(mockDatastore), 1316 ) 1317 b.Cleanup(func() { 1318 s.Close() 1319 }) 1320 1321 b.ResetTimer() 1322 for i := 0; i < b.N; i++ { 1323 _, err := s.ListObjects(ctx, &openfgav1.ListObjectsRequest{ 1324 StoreId: store, 1325 AuthorizationModelId: modelID, 1326 Type: "repo", 1327 Relation: "viewer", 1328 User: "user:bob", 1329 }) 1330 1331 require.ErrorIs(b, err, serverErrors.NewInternalError("", errors.New("error reading from storage"))) 1332 1333 err = s.StreamedListObjects(&openfgav1.StreamedListObjectsRequest{ 1334 StoreId: store, 1335 AuthorizationModelId: modelID, 1336 Type: "repo", 1337 Relation: "viewer", 1338 User: "user:bob", 1339 }, NewMockStreamServer()) 1340 1341 require.ErrorIs(b, err, serverErrors.NewInternalError("", errors.New("error reading from storage"))) 1342 } 1343 } 1344 1345 func TestListObjects_ErrorCases(t *testing.T) { 1346 t.Cleanup(func() { 1347 goleak.VerifyNone(t) 1348 }) 1349 1350 ctx := context.Background() 1351 store := ulid.Make().String() 1352 1353 mockController := gomock.NewController(t) 1354 defer mockController.Finish() 1355 1356 t.Run("database_errors", func(t *testing.T) { 1357 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 1358 1359 s := MustNewServerWithOpts( 1360 WithDatastore(mockDatastore), 1361 ) 1362 t.Cleanup(s.Close) 1363 1364 modelID := ulid.Make().String() 1365 1366 mockDatastore.EXPECT().ReadAuthorizationModel(gomock.Any(), store, modelID).AnyTimes().Return(&openfgav1.AuthorizationModel{ 1367 SchemaVersion: typesystem.SchemaVersion1_1, 1368 TypeDefinitions: language.MustTransformDSLToProto(`model 1369 schema 1.1 1370 type user 1371 1372 type document 1373 relations 1374 define viewer: [user, user:*]`).GetTypeDefinitions(), 1375 }, nil) 1376 1377 mockDatastore.EXPECT().ReadStartingWithUser(gomock.Any(), store, storage.ReadStartingWithUserFilter{ 1378 ObjectType: "document", 1379 Relation: "viewer", 1380 UserFilter: []*openfgav1.ObjectRelation{ 1381 {Object: "user:*"}, 1382 {Object: "user:bob"}, 1383 }}).AnyTimes().Return(nil, errors.New("error reading from storage")) 1384 1385 t.Run("error_listing_objects_from_storage_in_non-streaming_version", func(t *testing.T) { 1386 res, err := s.ListObjects(ctx, &openfgav1.ListObjectsRequest{ 1387 StoreId: store, 1388 AuthorizationModelId: modelID, 1389 Type: "document", 1390 Relation: "viewer", 1391 User: "user:bob", 1392 }) 1393 1394 require.Nil(t, res) 1395 require.ErrorIs(t, err, serverErrors.NewInternalError("", errors.New("error reading from storage"))) 1396 }) 1397 1398 t.Run("error_listing_objects_from_storage_in_streaming_version", func(t *testing.T) { 1399 err := s.StreamedListObjects(&openfgav1.StreamedListObjectsRequest{ 1400 StoreId: store, 1401 AuthorizationModelId: modelID, 1402 Type: "document", 1403 Relation: "viewer", 1404 User: "user:bob", 1405 }, NewMockStreamServer()) 1406 1407 require.ErrorIs(t, err, serverErrors.NewInternalError("", errors.New("error reading from storage"))) 1408 }) 1409 }) 1410 1411 t.Run("graph_resolution_errors", func(t *testing.T) { 1412 s := MustNewServerWithOpts( 1413 WithDatastore(memory.New()), 1414 WithResolveNodeLimit(2), 1415 ) 1416 t.Cleanup(s.Close) 1417 1418 writeModelResp, err := s.WriteAuthorizationModel(ctx, &openfgav1.WriteAuthorizationModelRequest{ 1419 StoreId: store, 1420 SchemaVersion: typesystem.SchemaVersion1_1, 1421 TypeDefinitions: language.MustTransformDSLToProto(`model 1422 schema 1.1 1423 type user 1424 1425 type group 1426 relations 1427 define member: [user, group#member] 1428 1429 type document 1430 relations 1431 define viewer: [group#member]`).GetTypeDefinitions(), 1432 }) 1433 require.NoError(t, err) 1434 1435 _, err = s.Write(ctx, &openfgav1.WriteRequest{ 1436 StoreId: store, 1437 Writes: &openfgav1.WriteRequestWrites{ 1438 TupleKeys: []*openfgav1.TupleKey{ 1439 tuple.NewTupleKey("document:1", "viewer", "group:1#member"), 1440 tuple.NewTupleKey("group:1", "member", "group:2#member"), 1441 tuple.NewTupleKey("group:2", "member", "group:3#member"), 1442 tuple.NewTupleKey("group:3", "member", "user:jon"), 1443 }, 1444 }, 1445 }) 1446 require.NoError(t, err) 1447 1448 t.Run("resolution_depth_exceeded_error_unary", func(t *testing.T) { 1449 res, err := s.ListObjects(ctx, &openfgav1.ListObjectsRequest{ 1450 StoreId: store, 1451 AuthorizationModelId: writeModelResp.GetAuthorizationModelId(), 1452 Type: "document", 1453 Relation: "viewer", 1454 User: "user:jon", 1455 }) 1456 1457 require.Nil(t, res) 1458 require.ErrorIs(t, err, serverErrors.AuthorizationModelResolutionTooComplex) 1459 }) 1460 1461 t.Run("resolution_depth_exceeded_error_streaming", func(t *testing.T) { 1462 err := s.StreamedListObjects(&openfgav1.StreamedListObjectsRequest{ 1463 StoreId: store, 1464 AuthorizationModelId: writeModelResp.GetAuthorizationModelId(), 1465 Type: "document", 1466 Relation: "viewer", 1467 User: "user:jon", 1468 }, NewMockStreamServer()) 1469 1470 require.ErrorIs(t, err, serverErrors.AuthorizationModelResolutionTooComplex) 1471 }) 1472 }) 1473 } 1474 1475 func TestAuthorizationModelInvalidSchemaVersion(t *testing.T) { 1476 t.Cleanup(func() { 1477 goleak.VerifyNone(t) 1478 }) 1479 1480 ctx := context.Background() 1481 store := ulid.Make().String() 1482 modelID := ulid.Make().String() 1483 1484 mockController := gomock.NewController(t) 1485 defer mockController.Finish() 1486 1487 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 1488 1489 mockDatastore.EXPECT().ReadAuthorizationModel(gomock.Any(), store, modelID).AnyTimes().Return(&openfgav1.AuthorizationModel{ 1490 SchemaVersion: typesystem.SchemaVersion1_0, 1491 TypeDefinitions: []*openfgav1.TypeDefinition{ 1492 { 1493 Type: "user", 1494 }, 1495 { 1496 Type: "team", 1497 Relations: map[string]*openfgav1.Userset{ 1498 "member": typesystem.This(), 1499 }, 1500 }, 1501 }, 1502 }, nil) 1503 1504 s := MustNewServerWithOpts( 1505 WithDatastore(mockDatastore), 1506 ) 1507 t.Cleanup(s.Close) 1508 1509 t.Run("invalid_schema_error_in_check", func(t *testing.T) { 1510 _, err := s.Check(ctx, &openfgav1.CheckRequest{ 1511 StoreId: store, 1512 AuthorizationModelId: modelID, 1513 TupleKey: tuple.NewCheckRequestTupleKey( 1514 "team:abc", 1515 "member", 1516 "user:anne"), 1517 }) 1518 require.Error(t, err) 1519 e, ok := status.FromError(err) 1520 require.True(t, ok) 1521 require.Equal(t, codes.Code(openfgav1.ErrorCode_validation_error), e.Code()) 1522 }) 1523 1524 t.Run("invalid_schema_error_in_list_objects", func(t *testing.T) { 1525 _, err := s.ListObjects(ctx, &openfgav1.ListObjectsRequest{ 1526 StoreId: store, 1527 AuthorizationModelId: modelID, 1528 Type: "team", 1529 Relation: "member", 1530 User: "user:anne", 1531 }) 1532 require.Error(t, err) 1533 e, ok := status.FromError(err) 1534 require.True(t, ok) 1535 require.Equal(t, codes.Code(openfgav1.ErrorCode_validation_error), e.Code()) 1536 }) 1537 1538 t.Run("invalid_schema_error_in_streamed_list_objects", func(t *testing.T) { 1539 err := s.StreamedListObjects(&openfgav1.StreamedListObjectsRequest{ 1540 StoreId: store, 1541 AuthorizationModelId: modelID, 1542 Type: "team", 1543 Relation: "member", 1544 User: "user:anne", 1545 }, NewMockStreamServer()) 1546 require.Error(t, err) 1547 e, ok := status.FromError(err) 1548 require.True(t, ok) 1549 require.Equal(t, codes.Code(openfgav1.ErrorCode_validation_error), e.Code()) 1550 }) 1551 1552 t.Run("invalid_schema_error_in_write", func(t *testing.T) { 1553 _, err := s.Write(ctx, &openfgav1.WriteRequest{ 1554 StoreId: store, 1555 AuthorizationModelId: modelID, 1556 Writes: &openfgav1.WriteRequestWrites{ 1557 TupleKeys: []*openfgav1.TupleKey{ 1558 { 1559 Object: "repo:openfga/openfga", 1560 Relation: "reader", 1561 User: "user:anne", 1562 }, 1563 }, 1564 }, 1565 }) 1566 require.Error(t, err) 1567 e, ok := status.FromError(err) 1568 require.True(t, ok) 1569 require.Equal(t, codes.Code(openfgav1.ErrorCode_validation_error), e.Code()) 1570 }) 1571 1572 t.Run("invalid_schema_error_in_write_model", func(t *testing.T) { 1573 mockDatastore.EXPECT().MaxTypesPerAuthorizationModel().Return(100) 1574 1575 _, err := s.WriteAuthorizationModel(ctx, &openfgav1.WriteAuthorizationModelRequest{ 1576 StoreId: store, 1577 SchemaVersion: typesystem.SchemaVersion1_0, 1578 TypeDefinitions: language.MustTransformDSLToProto(`model 1579 schema 1.1 1580 type repo 1581 `).GetTypeDefinitions(), 1582 }) 1583 require.Error(t, err) 1584 e, ok := status.FromError(err) 1585 require.True(t, ok) 1586 require.Equal(t, codes.Code(openfgav1.ErrorCode_invalid_authorization_model), e.Code(), err) 1587 }) 1588 1589 t.Run("invalid_schema_error_in_write_assertion", func(t *testing.T) { 1590 _, err := s.WriteAssertions(ctx, &openfgav1.WriteAssertionsRequest{ 1591 StoreId: store, 1592 AuthorizationModelId: modelID, 1593 Assertions: []*openfgav1.Assertion{{ 1594 TupleKey: tuple.NewAssertionTupleKey("repo:test", "reader", "user:elbuo"), 1595 Expectation: false, 1596 }}, 1597 }) 1598 require.Error(t, err) 1599 e, ok := status.FromError(err) 1600 require.True(t, ok) 1601 require.Equal(t, codes.Code(openfgav1.ErrorCode_validation_error), e.Code()) 1602 }) 1603 } 1604 1605 func TestDefaultMaxConcurrentReadSettings(t *testing.T) { 1606 t.Cleanup(func() { 1607 goleak.VerifyNone(t) 1608 }) 1609 1610 cfg := serverconfig.DefaultConfig() 1611 require.EqualValues(t, math.MaxUint32, cfg.MaxConcurrentReadsForCheck) 1612 require.EqualValues(t, math.MaxUint32, cfg.MaxConcurrentReadsForListObjects) 1613 require.EqualValues(t, math.MaxUint32, cfg.MaxConcurrentReadsForListUsers) 1614 1615 s := MustNewServerWithOpts( 1616 WithDatastore(memory.New()), 1617 ) 1618 t.Cleanup(s.Close) 1619 require.EqualValues(t, math.MaxUint32, s.maxConcurrentReadsForCheck) 1620 require.EqualValues(t, math.MaxUint32, s.maxConcurrentReadsForListObjects) 1621 require.EqualValues(t, math.MaxUint32, s.maxConcurrentReadsForListUsers) 1622 } 1623 1624 func TestDelegateCheckResolver(t *testing.T) { 1625 t.Cleanup(func() { 1626 goleak.VerifyNone(t) 1627 }) 1628 t.Run("default_check_resolver_alone", func(t *testing.T) { 1629 cfg := serverconfig.DefaultConfig() 1630 require.False(t, cfg.DispatchThrottling.Enabled) 1631 require.False(t, cfg.CheckQueryCache.Enabled) 1632 1633 ds := memory.New() 1634 t.Cleanup(ds.Close) 1635 s := MustNewServerWithOpts( 1636 WithDatastore(ds), 1637 ) 1638 t.Cleanup(s.Close) 1639 require.Nil(t, s.dispatchThrottlingCheckResolver) 1640 require.False(t, s.dispatchThrottlingCheckResolverEnabled) 1641 1642 require.False(t, s.checkQueryCacheEnabled) 1643 require.Nil(t, s.cachedCheckResolver) 1644 1645 require.NotNil(t, s.checkResolver) 1646 cycleDetectionCheckResolver, ok := s.checkResolver.(*graph.CycleDetectionCheckResolver) 1647 require.True(t, ok) 1648 1649 localCheckResolver, ok := cycleDetectionCheckResolver.GetDelegate().(*graph.LocalChecker) 1650 require.True(t, ok) 1651 1652 _, ok = localCheckResolver.GetDelegate().(*graph.CycleDetectionCheckResolver) 1653 require.True(t, ok) 1654 }) 1655 1656 t.Run("dispatch_throttling_check_resolver_enabled", func(t *testing.T) { 1657 ds := memory.New() 1658 t.Cleanup(ds.Close) 1659 const dispatchThreshold = 50 1660 s := MustNewServerWithOpts( 1661 WithDatastore(ds), 1662 WithDispatchThrottlingCheckResolverEnabled(true), 1663 WithDispatchThrottlingCheckResolverThreshold(dispatchThreshold), 1664 ) 1665 t.Cleanup(s.Close) 1666 1667 require.False(t, s.checkQueryCacheEnabled) 1668 require.Nil(t, s.cachedCheckResolver) 1669 1670 require.True(t, s.dispatchThrottlingCheckResolverEnabled) 1671 require.EqualValues(t, dispatchThreshold, s.dispatchThrottlingDefaultThreshold) 1672 require.EqualValues(t, 0, s.dispatchThrottlingMaxThreshold) 1673 require.NotNil(t, s.dispatchThrottlingCheckResolver) 1674 require.NotNil(t, s.checkResolver) 1675 cycleDetectionCheckResolver, ok := s.checkResolver.(*graph.CycleDetectionCheckResolver) 1676 require.True(t, ok) 1677 1678 dispatchThrottlingResolver, ok := cycleDetectionCheckResolver.GetDelegate().(*graph.DispatchThrottlingCheckResolver) 1679 require.True(t, ok) 1680 1681 localChecker, ok := dispatchThrottlingResolver.GetDelegate().(*graph.LocalChecker) 1682 require.True(t, ok) 1683 1684 _, ok = localChecker.GetDelegate().(*graph.CycleDetectionCheckResolver) 1685 require.True(t, ok) 1686 }) 1687 1688 t.Run("dispatch_throttling_check_resolver_enabled_zero_max_threshold", func(t *testing.T) { 1689 ds := memory.New() 1690 t.Cleanup(ds.Close) 1691 const dispatchThreshold = 50 1692 s := MustNewServerWithOpts( 1693 WithDatastore(ds), 1694 WithDispatchThrottlingCheckResolverEnabled(true), 1695 WithDispatchThrottlingCheckResolverThreshold(dispatchThreshold), 1696 WithDispatchThrottlingCheckResolverMaxThreshold(0), 1697 ) 1698 t.Cleanup(s.Close) 1699 1700 require.False(t, s.checkQueryCacheEnabled) 1701 require.Nil(t, s.cachedCheckResolver) 1702 1703 require.True(t, s.dispatchThrottlingCheckResolverEnabled) 1704 require.EqualValues(t, dispatchThreshold, s.dispatchThrottlingDefaultThreshold) 1705 require.EqualValues(t, 0, s.dispatchThrottlingMaxThreshold) 1706 require.NotNil(t, s.dispatchThrottlingCheckResolver) 1707 require.NotNil(t, s.checkResolver) 1708 cycleDetectionCheckResolver, ok := s.checkResolver.(*graph.CycleDetectionCheckResolver) 1709 require.True(t, ok) 1710 1711 dispatchThrottlingResolver, ok := cycleDetectionCheckResolver.GetDelegate().(*graph.DispatchThrottlingCheckResolver) 1712 require.True(t, ok) 1713 1714 localChecker, ok := dispatchThrottlingResolver.GetDelegate().(*graph.LocalChecker) 1715 require.True(t, ok) 1716 1717 _, ok = localChecker.GetDelegate().(*graph.CycleDetectionCheckResolver) 1718 require.True(t, ok) 1719 }) 1720 1721 t.Run("dispatch_throttling_check_resolver_enabled_non_zero_max_threshold", func(t *testing.T) { 1722 ds := memory.New() 1723 t.Cleanup(ds.Close) 1724 const dispatchThreshold = 50 1725 const maxDispatchThreshold = 60 1726 1727 s := MustNewServerWithOpts( 1728 WithDatastore(ds), 1729 WithDispatchThrottlingCheckResolverEnabled(true), 1730 WithDispatchThrottlingCheckResolverThreshold(dispatchThreshold), 1731 WithDispatchThrottlingCheckResolverMaxThreshold(maxDispatchThreshold), 1732 ) 1733 t.Cleanup(s.Close) 1734 1735 require.False(t, s.checkQueryCacheEnabled) 1736 require.Nil(t, s.cachedCheckResolver) 1737 1738 require.True(t, s.dispatchThrottlingCheckResolverEnabled) 1739 require.EqualValues(t, dispatchThreshold, s.dispatchThrottlingDefaultThreshold) 1740 require.EqualValues(t, maxDispatchThreshold, s.dispatchThrottlingMaxThreshold) 1741 require.NotNil(t, s.dispatchThrottlingCheckResolver) 1742 require.NotNil(t, s.checkResolver) 1743 cycleDetectionCheckResolver, ok := s.checkResolver.(*graph.CycleDetectionCheckResolver) 1744 require.True(t, ok) 1745 1746 dispatchThrottlingResolver, ok := cycleDetectionCheckResolver.GetDelegate().(*graph.DispatchThrottlingCheckResolver) 1747 require.True(t, ok) 1748 1749 localChecker, ok := dispatchThrottlingResolver.GetDelegate().(*graph.LocalChecker) 1750 require.True(t, ok) 1751 1752 _, ok = localChecker.GetDelegate().(*graph.CycleDetectionCheckResolver) 1753 require.True(t, ok) 1754 }) 1755 1756 t.Run("cache_check_resolver_enabled", func(t *testing.T) { 1757 ds := memory.New() 1758 t.Cleanup(ds.Close) 1759 s := MustNewServerWithOpts( 1760 WithDatastore(ds), 1761 WithCheckQueryCacheEnabled(true), 1762 ) 1763 t.Cleanup(s.Close) 1764 1765 require.False(t, s.dispatchThrottlingCheckResolverEnabled) 1766 require.Nil(t, s.dispatchThrottlingCheckResolver) 1767 1768 require.True(t, s.checkQueryCacheEnabled) 1769 require.NotNil(t, s.cachedCheckResolver) 1770 require.NotNil(t, s.checkResolver) 1771 cycleDetectionCheckResolver, ok := s.checkResolver.(*graph.CycleDetectionCheckResolver) 1772 require.True(t, ok) 1773 1774 cachedCheckResolver, ok := cycleDetectionCheckResolver.GetDelegate().(*graph.CachedCheckResolver) 1775 require.True(t, ok) 1776 1777 localChecker, ok := cachedCheckResolver.GetDelegate().(*graph.LocalChecker) 1778 require.True(t, ok) 1779 1780 _, ok = localChecker.GetDelegate().(*graph.CycleDetectionCheckResolver) 1781 require.True(t, ok) 1782 }) 1783 1784 t.Run("both_dispatch_throttling_and_cache_check_resolver_enabled", func(t *testing.T) { 1785 ds := memory.New() 1786 t.Cleanup(ds.Close) 1787 s := MustNewServerWithOpts( 1788 WithDatastore(ds), 1789 WithCheckQueryCacheEnabled(true), 1790 WithDispatchThrottlingCheckResolverEnabled(true), 1791 WithDispatchThrottlingCheckResolverThreshold(50), 1792 WithDispatchThrottlingCheckResolverMaxThreshold(100), 1793 ) 1794 t.Cleanup(s.Close) 1795 1796 require.True(t, s.dispatchThrottlingCheckResolverEnabled) 1797 require.EqualValues(t, 50, s.dispatchThrottlingDefaultThreshold) 1798 require.EqualValues(t, 100, s.dispatchThrottlingMaxThreshold) 1799 require.NotNil(t, s.dispatchThrottlingCheckResolver) 1800 require.NotNil(t, s.checkResolver) 1801 cycleDetectionCheckResolver, ok := s.checkResolver.(*graph.CycleDetectionCheckResolver) 1802 require.True(t, ok) 1803 1804 dispatchThrottlingResolver, ok := cycleDetectionCheckResolver.GetDelegate().(*graph.DispatchThrottlingCheckResolver) 1805 require.True(t, ok) 1806 1807 require.True(t, s.checkQueryCacheEnabled) 1808 require.NotNil(t, s.cachedCheckResolver) 1809 1810 cachedCheckResolver, ok := dispatchThrottlingResolver.GetDelegate().(*graph.CachedCheckResolver) 1811 require.True(t, ok) 1812 1813 localChecker, ok := cachedCheckResolver.GetDelegate().(*graph.LocalChecker) 1814 require.True(t, ok) 1815 1816 _, ok = localChecker.GetDelegate().(*graph.CycleDetectionCheckResolver) 1817 require.True(t, ok) 1818 }) 1819 } 1820 1821 func TestWriteAuthorizationModelWithSchema12(t *testing.T) { 1822 t.Cleanup(func() { 1823 goleak.VerifyNone(t) 1824 }) 1825 ctx := context.Background() 1826 storeID := ulid.Make().String() 1827 1828 mockController := gomock.NewController(t) 1829 defer mockController.Finish() 1830 1831 mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController) 1832 1833 t.Run("accepts_request_with_schema_version_1.2", func(t *testing.T) { 1834 s := MustNewServerWithOpts( 1835 WithDatastore(mockDatastore), 1836 ) 1837 defer s.Close() 1838 1839 mockDatastore.EXPECT().MaxTypesPerAuthorizationModel().Return(100) 1840 mockDatastore.EXPECT().WriteAuthorizationModel(gomock.Any(), storeID, gomock.Any()).Return(nil) 1841 1842 _, err := s.WriteAuthorizationModel(ctx, &openfgav1.WriteAuthorizationModelRequest{ 1843 StoreId: storeID, 1844 SchemaVersion: typesystem.SchemaVersion1_2, 1845 TypeDefinitions: []*openfgav1.TypeDefinition{ 1846 { 1847 Type: "user", 1848 Metadata: &openfgav1.Metadata{ 1849 Relations: nil, 1850 Module: "usermanagement", 1851 SourceInfo: nil, 1852 }, 1853 }, 1854 }, 1855 }) 1856 1857 require.NoError(t, err) 1858 }) 1859 } 1860 1861 func TestIsExperimentallyEnabled(t *testing.T) { 1862 someExperimentalFlag := ExperimentalFeatureFlag("some-experimental-feature-to-enable") 1863 1864 server := Server{} 1865 1866 t.Run("returns_false_if_experimentals_is_empty", func(t *testing.T) { 1867 require.False(t, server.IsExperimentallyEnabled(someExperimentalFlag)) 1868 }) 1869 1870 t.Run("returns_true_if_experimentals_has_matching_element", func(t *testing.T) { 1871 server.experimentals = []ExperimentalFeatureFlag{someExperimentalFlag} 1872 1873 require.True(t, server.IsExperimentallyEnabled(someExperimentalFlag)) 1874 }) 1875 1876 t.Run("returns_true_if_experimentals_has_matching_element_and_other_matching_element", func(t *testing.T) { 1877 server.experimentals = []ExperimentalFeatureFlag{someExperimentalFlag, ExperimentalFeatureFlag("some-other-feature")} 1878 1879 require.True(t, server.IsExperimentallyEnabled(someExperimentalFlag)) 1880 }) 1881 1882 t.Run("returns_false_if_experimentals_has_no_matching_element", func(t *testing.T) { 1883 server.experimentals = []ExperimentalFeatureFlag{ExperimentalFeatureFlag("some-other-feature")} 1884 1885 require.False(t, server.IsExperimentallyEnabled(someExperimentalFlag)) 1886 }) 1887 }