From 51b05b76ee51b95a06af5f64bf139b40f46f14c3 Mon Sep 17 00:00:00 2001 From: nverbos-godaddy <89865303+nverbos-godaddy@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:03:38 -0800 Subject: [PATCH] feat(model/test): add --max-types-per-authorization-model flag The embedded server used by `fga model test` inherits the default MaxTypesPerAuthorizationModel limit of 100 from the in-memory datastore. This causes local tests to fail for authorization models that exceed 100 type definitions, even though production OpenFGA servers can be configured with a higher limit. Add a --max-types-per-authorization-model flag to `fga model test` (default: 100, matching the current behavior) so users can raise the limit when testing larger models locally. This introduces a LocalServerConfig struct that can be extended with additional server options in the future (see #564). Closes #640 --- README.md | 3 +- cmd/model/test.go | 18 ++++++ internal/storetest/localstore.go | 5 +- internal/storetest/localstore_test.go | 92 +++++++++++++++++++++++++++ internal/storetest/tests.go | 10 ++- tests/fixtures/many-types-model.fga | 24 +++++++ tests/fixtures/many-types.fga.yaml | 13 ++++ tests/model-test-cases.yaml | 8 +++ 8 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 internal/storetest/localstore_test.go create mode 100644 tests/fixtures/many-types-model.fga create mode 100644 tests/fixtures/many-types.fga.yaml diff --git a/README.md b/README.md index 411460d1..4ab46e96 100644 --- a/README.md +++ b/README.md @@ -392,7 +392,7 @@ fga store **delete** | [Write Authorization Model ](#write-authorization-model) | `write` | `--store-id`, `--file` | `fga model write --store-id=01H0H015178Y2V4CX10C2KGHF4 --file model.fga` | | [Read a Single Authorization Model](#read-a-single-authorization-model) | `get` | `--store-id`, `--model-id` | `fga model get --store-id=01H0H015178Y2V4CX10C2KGHF4 --model-id=01GXSA8YR785C4FYS3C0RTG7B1` | | [Validate an Authorization Model](#validate-an-authorization-model) | `validate` | `--file`, `--format` | `fga model validate --file model.fga` | -| [Run Tests on an Authorization Model](#run-tests-on-an-authorization-model) | `test` | `--tests`, `--verbose` | `fga model test --tests "**/*.fga.yaml"` | +| [Run Tests on an Authorization Model](#run-tests-on-an-authorization-model) | `test` | `--tests`, `--verbose`, `--max-types-per-authorization-model` | `fga model test --tests "**/*.fga.yaml"` | | [Transform an Authorization Model](#transform-an-authorization-model) | `transform` | `--file`, `--input-format` | `fga model transform --file model.json` | @@ -541,6 +541,7 @@ fga model **test** * `--tests`: Name of the tests file, or a glob pattern to multiple files (for example `"tests/*.fga.yaml"`, or `"**/*.fga.yaml"`). Each file must be in yaml format. See [Store File Format](docs/STORE_FILE.md) for detailed documentation. * `--verbose`: Outputs the results in JSON +* `--max-types-per-authorization-model`: Max allowed number of type definitions per authorization model (default: 100). Increase this when testing models with more than 100 type definitions. If a model is provided, the test will run in a built-in OpenFGA instance (you do not need a separate server). Otherwise, the test will be run against the configured store of your OpenFGA instance. When running against a remote instance, the tuples will be sent as contextual tuples, and will have to abide by the OpenFGA server limits (20 contextual tuples per request). diff --git a/cmd/model/test.go b/cmd/model/test.go index fe3d2e09..f1776cb0 100644 --- a/cmd/model/test.go +++ b/cmd/model/test.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" + "github.com/openfga/cli/internal/clierrors" "github.com/openfga/cli/internal/cmdutils" "github.com/openfga/cli/internal/output" "github.com/openfga/cli/internal/storetest" @@ -52,6 +53,20 @@ var modelTestCmd = &cobra.Command{ return fmt.Errorf("failed to get suppress-summary flag: %w", err) } + maxTypes, err := cmd.Flags().GetInt("max-types-per-authorization-model") + if err != nil { + return fmt.Errorf("failed to get max-types-per-authorization-model flag: %w", err) + } + + if maxTypes <= 0 { + return clierrors.ValidationError("model test", + "max-types-per-authorization-model must be greater than 0") + } + + serverConfig := storetest.LocalServerConfig{ + MaxTypesPerAuthorizationModel: maxTypes, + } + fileNames, err := filepath.Glob(testsFileName) if err != nil { return fmt.Errorf("invalid tests pattern %s due to %w", testsFileName, err) @@ -89,6 +104,7 @@ var modelTestCmd = &cobra.Command{ fgaClient, storeData, format, + serverConfig, ) if err != nil { return fmt.Errorf("error running tests for %s due to %w", file, err) @@ -154,6 +170,8 @@ func init() { modelTestCmd.Flags().String("tests", "", "Path or glob of YAML test files") modelTestCmd.Flags().Bool("verbose", false, "Print verbose JSON output") modelTestCmd.Flags().Bool("suppress-summary", false, "Suppress the plain text summary output") + modelTestCmd.Flags().Int("max-types-per-authorization-model", 100, //nolint:mnd + "Max allowed number of type definitions per authorization model") if err := modelTestCmd.MarkFlagRequired("tests"); err != nil { fmt.Printf("error setting flag as required - %v: %v\n", "cmd/models/test", err) diff --git a/internal/storetest/localstore.go b/internal/storetest/localstore.go index ba4936a6..daa89e32 100644 --- a/internal/storetest/localstore.go +++ b/internal/storetest/localstore.go @@ -74,6 +74,7 @@ func initLocalStore( func getLocalServerModelAndTuples( storeData *StoreData, format authorizationmodel.ModelFormat, + serverConfig LocalServerConfig, ) (*server.Server, *authorizationmodel.AuthzModel, func(), error) { var fgaServer *server.Server @@ -86,7 +87,9 @@ func getLocalServerModelAndTuples( } // If we have at least one local test, initialize the local server - datastore := memory.New() + datastore := memory.New( + memory.WithMaxTypesPerAuthorizationModel(serverConfig.MaxTypesPerAuthorizationModel), + ) fgaServer, err := server.NewServerWithOpts( server.WithDatastore(datastore), diff --git a/internal/storetest/localstore_test.go b/internal/storetest/localstore_test.go new file mode 100644 index 00000000..4d7b18f1 --- /dev/null +++ b/internal/storetest/localstore_test.go @@ -0,0 +1,92 @@ +package storetest + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openfga/cli/internal/authorizationmodel" +) + +// buildModelWithNTypes returns a valid FGA model DSL string with n type definitions. +func buildModelWithNTypes(n int) string { + var builder strings.Builder + + builder.WriteString("model\n schema 1.1\n\ntype user\n") + + for i := 1; i < n; i++ { + fmt.Fprintf(&builder, "\ntype resource%d\n relations\n define owner: [user]\n", i) + } + + return builder.String() +} + +func TestGetLocalServerModelAndTuples_MaxTypesLimit(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + numTypes int + maxTypes int + expectError bool + }{ + { + name: "model within default limit succeeds", + numTypes: 6, + maxTypes: 100, + expectError: false, + }, + { + name: "model exceeding custom limit fails", + numTypes: 6, + maxTypes: 5, + expectError: true, + }, + { + name: "model within custom limit succeeds", + numTypes: 6, + maxTypes: 10, + expectError: false, + }, + { + name: "model at exact custom limit succeeds", + numTypes: 5, + maxTypes: 5, + expectError: false, + }, + } + + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + model := buildModelWithNTypes(testCase.numTypes) + storeData := &StoreData{Model: model} + config := LocalServerConfig{MaxTypesPerAuthorizationModel: testCase.maxTypes} + + fgaServer, authModel, stopFn, err := getLocalServerModelAndTuples( + storeData, authorizationmodel.ModelFormatDefault, config, + ) + require.NoError(t, err) + + defer stopFn() + + assert.NotNil(t, fgaServer) + assert.NotNil(t, authModel) + + // Try writing the model to the embedded server — this is where the limit is enforced + _, _, err = initLocalStore(context.Background(), fgaServer, authModel.GetProtoModel(), nil) + + if testCase.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), "exceeds the allowed limit") + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/storetest/tests.go b/internal/storetest/tests.go index e2c56c18..85cac1d6 100644 --- a/internal/storetest/tests.go +++ b/internal/storetest/tests.go @@ -15,6 +15,13 @@ type ModelTestOptions struct { Remote bool } +// LocalServerConfig holds configuration for the embedded OpenFGA server +// used during local model testing. Additional server options can be added +// here as needed (see https://github.com/openfga/cli/issues/564). +type LocalServerConfig struct { + MaxTypesPerAuthorizationModel int +} + func RunTest( ctx context.Context, fgaClient *client.OpenFgaClient, @@ -37,6 +44,7 @@ func RunTests( fgaClient *client.OpenFgaClient, storeData *StoreData, format authorizationmodel.ModelFormat, + serverConfig LocalServerConfig, ) (TestResults, error) { testResults := TestResults{} @@ -44,7 +52,7 @@ func RunTests( return testResults, err } - fgaServer, authModel, stopServerFn, err := getLocalServerModelAndTuples(storeData, format) + fgaServer, authModel, stopServerFn, err := getLocalServerModelAndTuples(storeData, format, serverConfig) if err != nil { return testResults, err } diff --git a/tests/fixtures/many-types-model.fga b/tests/fixtures/many-types-model.fga new file mode 100644 index 00000000..a4a2a47d --- /dev/null +++ b/tests/fixtures/many-types-model.fga @@ -0,0 +1,24 @@ +model + schema 1.1 + +type user + +type resource1 + relations + define owner: [user] + +type resource2 + relations + define owner: [user] + +type resource3 + relations + define owner: [user] + +type resource4 + relations + define owner: [user] + +type resource5 + relations + define owner: [user] diff --git a/tests/fixtures/many-types.fga.yaml b/tests/fixtures/many-types.fga.yaml new file mode 100644 index 00000000..3dce044d --- /dev/null +++ b/tests/fixtures/many-types.fga.yaml @@ -0,0 +1,13 @@ +name: Many Types Store +model_file: many-types-model.fga +tests: + - name: test-owner-check + tuples: + - user: user:anne + relation: owner + object: resource1:doc1 + check: + - user: user:anne + object: resource1:doc1 + assertions: + owner: true diff --git a/tests/model-test-cases.yaml b/tests/model-test-cases.yaml index c830cf4d..82b8d8bb 100644 --- a/tests/model-test-cases.yaml +++ b/tests/model-test-cases.yaml @@ -18,3 +18,11 @@ tests: command: fga model test --tests tests/fixtures/relative-path/relative-path-store.fga.yaml exit-code: 0 stderr: # Test Summary # + 005 - it fails when model exceeds max-types-per-authorization-model: + command: fga model test --tests tests/fixtures/many-types.fga.yaml --max-types-per-authorization-model 5 + exit-code: 1 + stderr: exceeds the allowed limit of 5 + 006 - it succeeds when max-types-per-authorization-model is sufficient: + command: fga model test --tests tests/fixtures/many-types.fga.yaml --max-types-per-authorization-model 10 + exit-code: 0 + stderr: # Test Summary #