Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |


Expand Down Expand Up @@ -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).

Expand Down
18 changes: 18 additions & 0 deletions cmd/model/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion internal/storetest/localstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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),
Expand Down
92 changes: 92 additions & 0 deletions internal/storetest/localstore_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
10 changes: 9 additions & 1 deletion internal/storetest/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -37,14 +44,15 @@ func RunTests(
fgaClient *client.OpenFgaClient,
storeData *StoreData,
format authorizationmodel.ModelFormat,
serverConfig LocalServerConfig,
) (TestResults, error) {
testResults := TestResults{}

if err := storeData.Validate(); err != nil {
return testResults, err
}

fgaServer, authModel, stopServerFn, err := getLocalServerModelAndTuples(storeData, format)
fgaServer, authModel, stopServerFn, err := getLocalServerModelAndTuples(storeData, format, serverConfig)
if err != nil {
return testResults, err
}
Expand Down
24 changes: 24 additions & 0 deletions tests/fixtures/many-types-model.fga
Original file line number Diff line number Diff line change
@@ -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]
13 changes: 13 additions & 0 deletions tests/fixtures/many-types.fga.yaml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions tests/model-test-cases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
Loading