From a0b72fa348ce5acc381b294760df5d7a8e830e11 Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Fri, 16 Jan 2026 22:17:23 -0300 Subject: [PATCH 1/3] fix: support importing more than 100 assertions --- cmd/store/import.go | 22 ++++++++-- cmd/store/import_test.go | 86 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/cmd/store/import.go b/cmd/store/import.go index 16bdecc8..e06bfcd9 100644 --- a/cmd/store/import.go +++ b/cmd/store/import.go @@ -44,6 +44,7 @@ const ( progressBarSleepDelay = 10 // time.Millisecond progressBarThrottleValue = 65 progressBarUpdateDelay = 5 * time.Millisecond + maxAssertionsPerWrite = 100 ) // createStore creates a new store with the given client configuration and store data. @@ -226,9 +227,14 @@ func importAssertions( StoreId: &storeID, } - _, err := fgaClient.WriteAssertions(ctx).Body(assertions).Options(writeOptions).Execute() - if err != nil { - return fmt.Errorf("failed to import assertions: %w", err) + for index := 0; index < len(assertions); index += maxAssertionsPerWrite { + end := min(index+maxAssertionsPerWrite, len(assertions)) + batch := assertions[index:end] + + _, err := fgaClient.WriteAssertions(ctx).Body(batch).Options(writeOptions).Execute() + if err != nil { + return fmt.Errorf("failed to import assertions: %w", err) + } } } @@ -236,7 +242,15 @@ func importAssertions( } func getCheckAssertions(checkTests []storetest.ModelTestCheck) []client.ClientAssertion { - var assertions []client.ClientAssertion + totalAssertions := 0 + + for _, checkTest := range checkTests { + users := storetest.GetEffectiveUsers(checkTest) + objects := storetest.GetEffectiveObjects(checkTest) + totalAssertions += len(users) * len(objects) * len(checkTest.Assertions) + } + + assertions := make([]client.ClientAssertion, 0, totalAssertions) for _, checkTest := range checkTests { users := storetest.GetEffectiveUsers(checkTest) diff --git a/cmd/store/import_test.go b/cmd/store/import_test.go index edb0e031..7ca86f1f 100644 --- a/cmd/store/import_test.go +++ b/cmd/store/import_test.go @@ -12,6 +12,11 @@ import ( "github.com/openfga/cli/internal/storetest" ) +const ( + testModelID = "model-1" + testStoreID = "store-1" +) + func TestImportStore(t *testing.T) { t.Parallel() @@ -51,7 +56,7 @@ func TestImportStore(t *testing.T) { Expectation: true, }, } - modelID, storeID := "model-1", "store-1" + modelID, storeID := testModelID, testStoreID expectedOptions := client.ClientWriteAssertionsOptions{AuthorizationModelId: &modelID, StoreId: &storeID} importStoreTests := []struct { @@ -215,6 +220,66 @@ func TestImportStore(t *testing.T) { } } +func TestImportStoreWithBatchedAssertions(t *testing.T) { + t.Parallel() + + modelID, storeID := testModelID, testStoreID + expectedOptions := client.ClientWriteAssertionsOptions{AuthorizationModelId: &modelID, StoreId: &storeID} + + // Generate 150 users to create 150 assertions (exceeding 100 limit) + users := make([]string, 150) + for i := range 150 { + users[i] = "user:" + string(rune('a'+i/26)) + string(rune('a'+i%26)) + } + + // Expected assertions split into batches + allAssertions := make([]client.ClientAssertion, 150) + for i := range 150 { + allAssertions[i] = client.ClientAssertion{ + User: users[i], + Relation: "reader", + Object: "document:doc1", + Expectation: true, + } + } + + batch1 := allAssertions[:100] + batch2 := allAssertions[100:] + + mockCtrl := gomock.NewController(t) + mockFgaClient := mockclient.NewMockSdkClient(mockCtrl) + + defer mockCtrl.Finish() + + setupBatchedWriteAssertionsMock(mockCtrl, mockFgaClient, [][]client.ClientAssertion{batch1, batch2}, expectedOptions) + setupWriteModelMock(mockCtrl, mockFgaClient, modelID) + setupCreateStoreMock(mockCtrl, mockFgaClient, storeID) + + testStore := storetest.StoreData{ + Model: `type user + type document + relations + define reader: [user]`, + Tests: []storetest.ModelTest{ + { + Name: "Test", + Check: []storetest.ModelTestCheck{ + { + Users: users, + Object: "document:doc1", + Assertions: map[string]bool{"reader": true}, + }, + }, + }, + }, + } + + _, err := importStore(t.Context(), &fga.ClientConfig{}, mockFgaClient, &testStore, "", "", 10, 1, "") + if err != nil { + t.Errorf("expected no error, got %v", err) + } +} + func TestUpdateStore(t *testing.T) { t.Parallel() @@ -225,8 +290,8 @@ func TestUpdateStore(t *testing.T) { Expectation: true, }} - modelID := "model-1" - storeID := "store-1" + modelID := testModelID + storeID := testStoreID sampleTime := time.Now() expectedOptions := client.ClientWriteAssertionsOptions{ AuthorizationModelId: &modelID, @@ -361,3 +426,18 @@ func setupWriteAssertionsMock( mockWriteAssertions.EXPECT().Options(expectedOptions).Return(mockWriteAssertions) mockWriteAssertions.EXPECT().Execute().Return(nil, nil) } + +func setupBatchedWriteAssertionsMock( + mockCtrl *gomock.Controller, + mockFgaClient *mockclient.MockSdkClient, + expectedBatches [][]client.ClientAssertion, + expectedOptions client.ClientWriteAssertionsOptions, +) { + for _, batch := range expectedBatches { + mockWriteAssertions := mockclient.NewMockSdkClientWriteAssertionsRequestInterface(mockCtrl) + mockFgaClient.EXPECT().WriteAssertions(gomock.Any()).Return(mockWriteAssertions) + mockWriteAssertions.EXPECT().Body(batch).Return(mockWriteAssertions) + mockWriteAssertions.EXPECT().Options(expectedOptions).Return(mockWriteAssertions) + mockWriteAssertions.EXPECT().Execute().Return(nil, nil) + } +} From fdd392a20e51d809910d32093ff9895635da1ad5 Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Fri, 16 Jan 2026 22:17:33 -0300 Subject: [PATCH 2/3] fix: lint fixes --- internal/storetest/localtest.go | 9 ++++----- internal/storetest/remotetest.go | 15 +++++++-------- internal/storetest/testresult.go | 6 +++--- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/internal/storetest/localtest.go b/internal/storetest/localtest.go index a297d896..17870108 100644 --- a/internal/storetest/localtest.go +++ b/internal/storetest/localtest.go @@ -26,10 +26,10 @@ func RunLocalCheckTest( tuples []client.ClientContextualTupleKey, options ModelTestOptions, ) []ModelTestCheckSingleResult { - results := []ModelTestCheckSingleResult{} users := GetEffectiveUsers(checkTest) - objects := GetEffectiveObjects(checkTest) + results := make([]ModelTestCheckSingleResult, 0, len(users)*len(objects)*len(checkTest.Assertions)) + for _, user := range users { for _, object := range objects { for relation, expectation := range checkTest.Assertions { @@ -101,7 +101,7 @@ func RunLocalListObjectsTest( tuples []client.ClientContextualTupleKey, options ModelTestOptions, ) []ModelTestListObjectsSingleResult { - results := []ModelTestListObjectsSingleResult{} + results := make([]ModelTestListObjectsSingleResult, 0, len(listObjectsTest.Assertions)) for relation, expectation := range listObjectsTest.Assertions { result := ModelTestListObjectsSingleResult{ @@ -168,8 +168,7 @@ func RunLocalListUsersTest( tuples []client.ClientContextualTupleKey, options ModelTestOptions, ) []ModelTestListUsersSingleResult { - results := []ModelTestListUsersSingleResult{} - + results := make([]ModelTestListUsersSingleResult, 0, len(listUsersTest.Assertions)) object, pbObject := convertStoreObjectToObject(listUsersTest.Object) userFilter := &pb.UserTypeFilter{ diff --git a/internal/storetest/remotetest.go b/internal/storetest/remotetest.go index ebebcf62..9112a444 100644 --- a/internal/storetest/remotetest.go +++ b/internal/storetest/remotetest.go @@ -33,10 +33,9 @@ func RunRemoteCheckTest( checkTest ModelTestCheck, tuples []client.ClientContextualTupleKey, ) []ModelTestCheckSingleResult { - results := []ModelTestCheckSingleResult{} - users := GetEffectiveUsers(checkTest) objects := GetEffectiveObjects(checkTest) + results := make([]ModelTestCheckSingleResult, 0, len(users)*len(objects)*len(checkTest.Assertions)) for _, user := range users { for _, object := range objects { @@ -89,7 +88,7 @@ func RunRemoteListObjectsTest( listObjectsTest ModelTestListObjects, tuples []client.ClientContextualTupleKey, ) []ModelTestListObjectsSingleResult { - results := []ModelTestListObjectsSingleResult{} + results := make([]ModelTestListObjectsSingleResult, 0, len(listObjectsTest.Assertions)) for relation, expectation := range listObjectsTest.Assertions { result := RunSingleRemoteListObjectsTest(ctx, fgaClient, @@ -138,9 +137,9 @@ func RunRemoteListUsersTest( listUsersTest ModelTestListUsers, tuples []client.ClientContextualTupleKey, ) []ModelTestListUsersSingleResult { - results := []ModelTestListUsersSingleResult{} - + results := make([]ModelTestListUsersSingleResult, 0, len(listUsersTest.Assertions)) object, _ := convertStoreObjectToObject(listUsersTest.Object) + for relation, expectation := range listUsersTest.Assertions { result := RunSingleRemoteListUsersTest(ctx, fgaClient, client.ClientListUsersRequest{ @@ -165,21 +164,21 @@ func RunRemoteTest( test ModelTest, testTuples []client.ClientContextualTupleKey, ) TestResult { - checkResults := []ModelTestCheckSingleResult{} + checkResults := make([]ModelTestCheckSingleResult, 0, len(test.Check)) for index := range test.Check { results := RunRemoteCheckTest(ctx, fgaClient, test.Check[index], testTuples) checkResults = append(checkResults, results...) } - listObjectResults := []ModelTestListObjectsSingleResult{} + listObjectResults := make([]ModelTestListObjectsSingleResult, 0, len(test.ListObjects)) for index := range test.ListObjects { results := RunRemoteListObjectsTest(ctx, fgaClient, test.ListObjects[index], testTuples) listObjectResults = append(listObjectResults, results...) } - listUserResults := []ModelTestListUsersSingleResult{} + listUserResults := make([]ModelTestListUsersSingleResult, 0, len(test.ListUsers)) for index := range test.ListUsers { results := RunRemoteListUsersTest(ctx, fgaClient, test.ListUsers[index], testTuples) diff --git a/internal/storetest/testresult.go b/internal/storetest/testresult.go index 266930de..9b921400 100644 --- a/internal/storetest/testresult.go +++ b/internal/storetest/testresult.go @@ -366,12 +366,12 @@ func (test TestResults) FriendlyDisplay() string { //nolint:cyclop func (test TestResults) FriendlyBody() string { fullOutput := test.FriendlyDisplay() - headerIndex := strings.Index(fullOutput, "# Test Summary #") - if headerIndex == -1 { + before, _, ok := strings.Cut(fullOutput, "# Test Summary #") + if !ok { return fullOutput } - return strings.TrimSpace(fullOutput[:headerIndex]) + return strings.TrimSpace(before) } func buildTestSummary(failedTestCount int, summary string, totalTestCount int, From 30a052d8849395f1095cc4bb8c32da90a7be6433 Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Sun, 18 Jan 2026 11:10:26 -0300 Subject: [PATCH 3/3] fix: batching does not work, writing first 100 assertions and displaying a warning instead --- cmd/store/import.go | 15 ++++++++------- cmd/store/import_test.go | 31 +++++++------------------------ 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/cmd/store/import.go b/cmd/store/import.go index e06bfcd9..2a3d9198 100644 --- a/cmd/store/import.go +++ b/cmd/store/import.go @@ -227,14 +227,15 @@ func importAssertions( StoreId: &storeID, } - for index := 0; index < len(assertions); index += maxAssertionsPerWrite { - end := min(index+maxAssertionsPerWrite, len(assertions)) - batch := assertions[index:end] + if len(assertions) > maxAssertionsPerWrite { + fmt.Fprintf(os.Stderr, "Warning: %d test assertions found, but only the first %d will be written\n", + len(assertions), maxAssertionsPerWrite) + assertions = assertions[:maxAssertionsPerWrite] + } - _, err := fgaClient.WriteAssertions(ctx).Body(batch).Options(writeOptions).Execute() - if err != nil { - return fmt.Errorf("failed to import assertions: %w", err) - } + _, err := fgaClient.WriteAssertions(ctx).Body(assertions).Options(writeOptions).Execute() + if err != nil { + return fmt.Errorf("failed to import test assertions: %w", err) } } diff --git a/cmd/store/import_test.go b/cmd/store/import_test.go index 7ca86f1f..c65b20af 100644 --- a/cmd/store/import_test.go +++ b/cmd/store/import_test.go @@ -220,7 +220,7 @@ func TestImportStore(t *testing.T) { } } -func TestImportStoreWithBatchedAssertions(t *testing.T) { +func TestImportStoreWithTruncatedAssertions(t *testing.T) { t.Parallel() modelID, storeID := testModelID, testStoreID @@ -232,10 +232,10 @@ func TestImportStoreWithBatchedAssertions(t *testing.T) { users[i] = "user:" + string(rune('a'+i/26)) + string(rune('a'+i%26)) } - // Expected assertions split into batches - allAssertions := make([]client.ClientAssertion, 150) - for i := range 150 { - allAssertions[i] = client.ClientAssertion{ + // Only the first 100 assertions should be written + first100Assertions := make([]client.ClientAssertion, 100) + for i := range 100 { + first100Assertions[i] = client.ClientAssertion{ User: users[i], Relation: "reader", Object: "document:doc1", @@ -243,15 +243,13 @@ func TestImportStoreWithBatchedAssertions(t *testing.T) { } } - batch1 := allAssertions[:100] - batch2 := allAssertions[100:] - mockCtrl := gomock.NewController(t) mockFgaClient := mockclient.NewMockSdkClient(mockCtrl) defer mockCtrl.Finish() - setupBatchedWriteAssertionsMock(mockCtrl, mockFgaClient, [][]client.ClientAssertion{batch1, batch2}, expectedOptions) + // Only expect a single write with the first 100 assertions + setupWriteAssertionsMock(mockCtrl, mockFgaClient, first100Assertions, expectedOptions) setupWriteModelMock(mockCtrl, mockFgaClient, modelID) setupCreateStoreMock(mockCtrl, mockFgaClient, storeID) @@ -426,18 +424,3 @@ func setupWriteAssertionsMock( mockWriteAssertions.EXPECT().Options(expectedOptions).Return(mockWriteAssertions) mockWriteAssertions.EXPECT().Execute().Return(nil, nil) } - -func setupBatchedWriteAssertionsMock( - mockCtrl *gomock.Controller, - mockFgaClient *mockclient.MockSdkClient, - expectedBatches [][]client.ClientAssertion, - expectedOptions client.ClientWriteAssertionsOptions, -) { - for _, batch := range expectedBatches { - mockWriteAssertions := mockclient.NewMockSdkClientWriteAssertionsRequestInterface(mockCtrl) - mockFgaClient.EXPECT().WriteAssertions(gomock.Any()).Return(mockWriteAssertions) - mockWriteAssertions.EXPECT().Body(batch).Return(mockWriteAssertions) - mockWriteAssertions.EXPECT().Options(expectedOptions).Return(mockWriteAssertions) - mockWriteAssertions.EXPECT().Execute().Return(nil, nil) - } -}