diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index ef6fbfd80..d963e7837 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -65,6 +65,8 @@ jobs: - name: Run SDK Tests run: make test-client-go + env: + OPEN_API_REF: 0ac19aac54f21f3c78970126b84b4c69c6e3b9a2 - name: Check for SDK changes run: | diff --git a/config/clients/go/CHANGELOG.md.mustache b/config/clients/go/CHANGELOG.md.mustache index fb35b4c49..3ced48c61 100644 --- a/config/clients/go/CHANGELOG.md.mustache +++ b/config/clients/go/CHANGELOG.md.mustache @@ -3,6 +3,9 @@ ## [Unreleased](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v{{packageVersion}}...HEAD) - feat: add support for custom headers per request. See [documentation](https://github.com/openfga/go-sdk#custom-headers). +- feat: add support for conflict options for Write operations**: (#229) + The client now supports setting `Conflict` on `ClientWriteOptions` to control behavior when writing duplicate tuples or deleting non-existent tuples. This feature requires OpenFGA server [v1.10.0](https://github.com/openfga/openfga/releases/tag/v1.10.0) or later. + See [Conflict Options for Write Operations](./README.md#conflict-options-for-write-operations) for more. ## {{packageVersion}} diff --git a/config/clients/go/config.overrides.json b/config/clients/go/config.overrides.json index 169f8e7ed..64d7471bd 100644 --- a/config/clients/go/config.overrides.json +++ b/config/clients/go/config.overrides.json @@ -104,6 +104,10 @@ "destinationFilename": "internal/utils/ulid_test.go", "templateType": "SupportingFiles" }, + "models_test.mustache": { + "destinationFilename": "models_test.go", + "templateType": "SupportingFiles" + }, "example/Makefile": {}, "example/README.md": {}, "example/example1/example1.go": {}, diff --git a/config/clients/go/template/README_calling_api.mustache b/config/clients/go/template/README_calling_api.mustache index cbaee09fe..e468ec0b5 100644 --- a/config/clients/go/template/README_calling_api.mustache +++ b/config/clients/go/template/README_calling_api.mustache @@ -409,6 +409,42 @@ data, err := fgaClient.Write(context.Background()).Body(body).Options(options).E // }] ``` +#### Conflict Options for Write Operations + +The SDK supports conflict options for write operations, allowing you to control how the API handles duplicate writes and missing deletes. + +> Note: This requires OpenFGA [v1.10.0](https://github.com/openfga/openfga/releases/tag/v1.10.0) or later. + +```go +options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + // Control what happens when writing a tuple that already exists + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, // or CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_ERROR (the current default behavior) + + // Control what happens when deleting a tuple that doesn't exist + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE, // or CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_ERROR (the current default behavior) + }, +} + +body := ClientWriteRequest{ + Writes: []ClientTupleKey{ { + User: "user:anne", + Relation: "writer", + Object: "document:2021-budget", + } }, + Deletes: []ClientTupleKeyWithoutCondition{ { + User: "user:bob", + Relation: "reader", + Object: "document:2021-budget", + } }, +} + +data, err := fgaClient.Write(context.Background()). + Body(body). + Options(options). + Execute() +``` + #### Relationship Queries ##### Check diff --git a/config/clients/go/template/api_test.mustache b/config/clients/go/template/api_test.mustache index cb2d16dbd..3b60623c3 100644 --- a/config/clients/go/template/api_test.mustache +++ b/config/clients/go/template/api_test.mustache @@ -789,6 +789,308 @@ func Test{{appShortName}}Api(t *testing.T) { } }) + t.Run("Write (Write Tuple with OnDuplicate ignore)", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + onDuplicateIgnore := "ignore" +{{=<% %>=}} + requestBody := WriteRequest{ + Writes: &WriteRequestWrites{ + TupleKeys: []TupleKey{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + OnDuplicate: &onDuplicateIgnore, + }, + AuthorizationModelId: PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"), + } +<%={{ }}=%> + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", configuration.ApiUrl, "01GXSB9YR785C4FYS3C0RTG7B2", test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnDuplicate field + var body WriteRequest + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if body.Writes.OnDuplicate == nil || *body.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore', got %v", body.Writes.OnDuplicate) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + _, response, err := apiClient.{{appShortName}}Api.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("{{appShortName}}%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus) + } + }) + + t.Run("Write (Write Tuple with OnDuplicate error)", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + onDuplicateError := "error" +{{=<% %>=}} + requestBody := WriteRequest{ + Writes: &WriteRequestWrites{ + TupleKeys: []TupleKey{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + OnDuplicate: &onDuplicateError, + }, + AuthorizationModelId: PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"), + } +<%={{ }}=%> + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", configuration.ApiUrl, "01GXSB9YR785C4FYS3C0RTG7B2", test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnDuplicate field + var body WriteRequest + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if body.Writes.OnDuplicate == nil || *body.Writes.OnDuplicate != "error" { + t.Errorf("Expected OnDuplicate to be 'error', got %v", body.Writes.OnDuplicate) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + _, response, err := apiClient.{{appShortName}}Api.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("{{appShortName}}%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus) + } + }) + + t.Run("Write (Delete Tuple with OnMissing ignore)", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + onMissingIgnore := "ignore" +{{=<% %>=}} + requestBody := WriteRequest{ + Deletes: &WriteRequestDeletes{ + TupleKeys: []TupleKeyWithoutCondition{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + OnMissing: &onMissingIgnore, + }, + AuthorizationModelId: PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"), + } +<%={{ }}=%> + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", configuration.ApiUrl, "01GXSB9YR785C4FYS3C0RTG7B2", test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnMissing field + var body WriteRequest + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if body.Deletes.OnMissing == nil || *body.Deletes.OnMissing != "ignore" { + t.Errorf("Expected OnMissing to be 'ignore', got %v", body.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + _, response, err := apiClient.{{appShortName}}Api.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("{{appShortName}}%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus) + } + }) + + t.Run("Write (Delete Tuple with OnMissing error)", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + onMissingError := "error" +{{=<% %>=}} + requestBody := WriteRequest{ + Deletes: &WriteRequestDeletes{ + TupleKeys: []TupleKeyWithoutCondition{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + OnMissing: &onMissingError, + }, + AuthorizationModelId: PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"), + } +<%={{ }}=%> + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", configuration.ApiUrl, "01GXSB9YR785C4FYS3C0RTG7B2", test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnMissing field + var body WriteRequest + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if body.Deletes.OnMissing == nil || *body.Deletes.OnMissing != "error" { + t.Errorf("Expected OnMissing to be 'error', got %v", body.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + _, response, err := apiClient.{{appShortName}}Api.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("{{appShortName}}%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus) + } + }) + + t.Run("Write (Mixed writes and deletes with conflict options)", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + onDuplicateIgnore := "ignore" + onMissingIgnore := "ignore" +{{=<% %>=}} + requestBody := WriteRequest{ + Writes: &WriteRequestWrites{ + TupleKeys: []TupleKey{{ + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + OnDuplicate: &onDuplicateIgnore, + }, + Deletes: &WriteRequestDeletes{ + TupleKeys: []TupleKeyWithoutCondition{{ + User: "user:another-user", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }}, + OnMissing: &onMissingIgnore, + }, + AuthorizationModelId: PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"), + } +<%={{ }}=%> + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", configuration.ApiUrl, "01GXSB9YR785C4FYS3C0RTG7B2", test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains both OnDuplicate and OnMissing fields + var body WriteRequest + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if body.Writes.OnDuplicate == nil || *body.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore', got %v", body.Writes.OnDuplicate) + } + if body.Deletes.OnMissing == nil || *body.Deletes.OnMissing != "ignore" { + t.Errorf("Expected OnMissing to be 'ignore', got %v", body.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + _, response, err := apiClient.{{appShortName}}Api.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("{{appShortName}}%v().Execute() = %v, want %v", test.Name, response.StatusCode, test.ResponseStatus) + } + }) + t.Run("Expand", func(t *testing.T) { test := TestDefinition{ Name: "Expand", diff --git a/config/clients/go/template/client/client.mustache b/config/clients/go/template/client/client.mustache index bf20c583b..60d14d00e 100644 --- a/config/clients/go/template/client/client.mustache +++ b/config/clients/go/template/client/client.mustache @@ -1509,12 +1509,62 @@ type TransactionOptions struct { MaxParallelRequests int32 `json:"max_parallel_requests,omitempty"` } +// ClientWriteRequestOnDuplicateWrites indicates what to do when a write conflicts with an existing tuple +type ClientWriteRequestOnDuplicateWrites string + +func (w *ClientWriteRequestOnDuplicateWrites) ToString() *string { + if w == nil { + return nil + } + + str := string(*w) + + return &str +} + +const ( + // CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_ERROR returns an error if a write conflicts with an existing tuple (default) + CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_ERROR ClientWriteRequestOnDuplicateWrites = "error" + // CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE ignores writes that conflict with existing tuples (they must match exactly, including conditions) + CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE ClientWriteRequestOnDuplicateWrites = "ignore" +) + +// ClientWriteRequestOnMissingDeletes indicates what to do when a delete is issued for a tuple that does not exist +type ClientWriteRequestOnMissingDeletes string + +func (d *ClientWriteRequestOnMissingDeletes) ToString() *string { + if d == nil { + return nil + } + + str := string(*d) + + return &str +} + +const ( + // CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_ERROR returns an error if a delete is issued for a tuple that does not exist (default) + CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_ERROR ClientWriteRequestOnMissingDeletes = "error" + // CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE ignores deletes for tuples that do not exist + CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE ClientWriteRequestOnMissingDeletes = "ignore" +) + +type ClientWriteConflictOptions struct { + // OnDuplicateWrites defines what to do when a write conflicts with an existing tuple + // Options are: "error" (default) or "ignore" + OnDuplicateWrites ClientWriteRequestOnDuplicateWrites `json:"on_duplicate_writes,omitempty"` + // OnMissingDeletes defines what to do when a delete is issued for a tuple that does not exist + // Options are: "error" (default) or "ignore" + OnMissingDeletes ClientWriteRequestOnMissingDeletes `json:"on_missing_deletes,omitempty"` +} + type ClientWriteOptions struct { - RequestOptions + RequestOptions - AuthorizationModelId *string `json:"authorization_model_id,omitempty"` - StoreId *string `json:"store_id,omitempty"` - Transaction *TransactionOptions `json:"transaction_options,omitempty"` + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` + StoreId *string `json:"store_id,omitempty"` + Transaction *TransactionOptions `json:"transaction_options,omitempty"` + Conflict ClientWriteConflictOptions } type ClientWriteStatus string @@ -1652,6 +1702,9 @@ func (client *{{appShortName}}Client) WriteExecute(request SdkClientWriteRequest } if len(request.GetBody().Writes) > 0 { writes := fgaSdk.WriteRequestWrites{} + if options != nil { + writes.OnDuplicate = options.Conflict.OnDuplicateWrites.ToString() + } for index := 0; index < len(request.GetBody().Writes); index++ { writes.TupleKeys = append(writes.TupleKeys, (request.GetBody().Writes)[index]) } @@ -1659,6 +1712,9 @@ func (client *{{appShortName}}Client) WriteExecute(request SdkClientWriteRequest } if len(request.GetBody().Deletes) > 0 { deletes := fgaSdk.WriteRequestDeletes{} + if options != nil { + deletes.OnMissing = options.Conflict.OnMissingDeletes.ToString() + } for index := 0; index < len(request.GetBody().Deletes); index++ { deletes.TupleKeys = append(deletes.TupleKeys, (request.GetBody().Deletes)[index]) } @@ -1744,6 +1800,7 @@ func (client *{{appShortName}}Client) WriteExecute(request SdkClientWriteRequest RequestOptions: options.RequestOptions, AuthorizationModelId: authorizationModelId, StoreId: request.GetStoreIdOverride(), + Conflict: options.Conflict, }, }) @@ -1789,6 +1846,7 @@ func (client *{{appShortName}}Client) WriteExecute(request SdkClientWriteRequest RequestOptions: options.RequestOptions, AuthorizationModelId: authorizationModelId, StoreId: request.GetStoreIdOverride(), + Conflict: options.Conflict, }, }) diff --git a/config/clients/go/template/client/client_test.mustache b/config/clients/go/template/client/client_test.mustache index 009ff3ba6..c9b8195b9 100644 --- a/config/clients/go/template/client/client_test.mustache +++ b/config/clients/go/template/client/client_test.mustache @@ -12,7 +12,7 @@ import ( "github.com/jarcoal/httpmock" - openfga "{{gitHost}}/{{gitUserId}}/{{gitRepoId}}" + {{packageName}} "{{gitHost}}/{{gitUserId}}/{{gitRepoId}}" . "{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/client" ) @@ -216,7 +216,7 @@ func Test{{appShortName}}Client(t *testing.T) { Method: http.MethodGet, } - var expectedResponse openfga.ListStoresResponse + var expectedResponse {{packageName}}.ListStoresResponse if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { t.Fatalf("%v", err) } @@ -238,7 +238,7 @@ func Test{{appShortName}}Client(t *testing.T) { ) options := ClientListStoresOptions{ - Name: openfga.PtrString(filteredStoreName), + Name: {{packageName}}.PtrString(filteredStoreName), } got, err := fgaClient.ListStores(context.Background()).Options(options).Execute() if err != nil { @@ -1535,13 +1535,13 @@ func Test{{appShortName}}Client(t *testing.T) { // store can be overridden storeOverrideOptions := ClientWriteOptions{ - AuthorizationModelId: openfga.PtrString(authModelId), + AuthorizationModelId: {{packageName}}.PtrString(authModelId), Transaction: &TransactionOptions{ Disable: true, MaxParallelRequests: 5, MaxPerChunk: 1, }, - StoreId: openfga.PtrString("7777HCE4YVKPQEKZQHT2R89MQV"), + StoreId: {{packageName}}.PtrString("7777HCE4YVKPQEKZQHT2R89MQV"), } httpmock.Reset() httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, *storeOverrideOptions.StoreId, test.RequestPath), @@ -1561,7 +1561,7 @@ func Test{{appShortName}}Client(t *testing.T) { for index := 0; index < len(data.Writes); index++ { response := data.Writes[index] if response.Error != nil { - t.Fatalf("OpenFgaClient.%v()|%d/ %v", test.Name, index, response.Error) + t.Fatalf("{{appShortName}}Client.%v()|%d/ %v", test.Name, index, response.Error) } } }) @@ -1592,7 +1592,7 @@ func Test{{appShortName}}Client(t *testing.T) { } const authModelId = "01GAHCE4YVKPQEKZQHT2R89MQV" options := ClientWriteOptions{ - AuthorizationModelId: openfga.PtrString(authModelId), + AuthorizationModelId: {{packageName}}.PtrString(authModelId), Transaction: &TransactionOptions{ Disable: true, MaxPerChunk: 1, @@ -1622,20 +1622,20 @@ func Test{{appShortName}}Client(t *testing.T) { } if len(data.Writes) != 2 { - t.Fatalf("OpenFgaClient.%v() - expected %v Writes, got %v", test.Name, 2, len(data.Writes)) + t.Fatalf("{{appShortName}}Client.%v() - expected %v Writes, got %v", test.Name, 2, len(data.Writes)) } if len(data.Deletes) != 1 { - t.Fatalf("OpenFgaClient.%v() - expected %v Deletes, got %v", test.Name, 1, len(data.Deletes)) + t.Fatalf("{{appShortName}}Client.%v() - expected %v Deletes, got %v", test.Name, 1, len(data.Deletes)) } for index := 0; index < len(data.Writes); index++ { response := data.Writes[index] if response.Error != nil { - t.Fatalf("OpenFgaClient.%v()|%d/ %v", test.Name, index, response.Error) + t.Fatalf("{{appShortName}}Client.%v()|%d/ %v", test.Name, index, response.Error) } if response.HttpResponse.StatusCode != test.ResponseStatus { - t.Fatalf("OpenFgaClient.%v() = %v, want %v", test.Name, response.HttpResponse.StatusCode, test.ResponseStatus) + t.Fatalf("{{appShortName}}Client.%v() = %v, want %v", test.Name, response.HttpResponse.StatusCode, test.ResponseStatus) } _, err := response.MarshalJSON() @@ -1647,10 +1647,10 @@ func Test{{appShortName}}Client(t *testing.T) { for index := 0; index < len(data.Deletes); index++ { response := data.Deletes[index] if response.Error != nil { - t.Fatalf("OpenFgaClient.%v()|%d/ %v", test.Name, index, response.Error) + t.Fatalf("{{appShortName}}Client.%v()|%d/ %v", test.Name, index, response.Error) } if response.HttpResponse.StatusCode != test.ResponseStatus { - t.Fatalf("OpenFgaClient.%v() = %v, want %v", test.Name, response.HttpResponse.StatusCode, test.ResponseStatus) + t.Fatalf("{{appShortName}}Client.%v() = %v, want %v", test.Name, response.HttpResponse.StatusCode, test.ResponseStatus) } _, err := response.MarshalJSON() @@ -1661,13 +1661,13 @@ func Test{{appShortName}}Client(t *testing.T) { // store can be overridden storeOverrideOptions := ClientWriteOptions{ - AuthorizationModelId: openfga.PtrString(authModelId), + AuthorizationModelId: {{packageName}}.PtrString(authModelId), Transaction: &TransactionOptions{ Disable: true, MaxParallelRequests: 5, MaxPerChunk: 1, }, - StoreId: openfga.PtrString("7777HCE4YVKPQEKZQHT2R89MQV"), + StoreId: {{packageName}}.PtrString("7777HCE4YVKPQEKZQHT2R89MQV"), } httpmock.Reset() httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, *storeOverrideOptions.StoreId, test.RequestPath), @@ -1687,7 +1687,7 @@ func Test{{appShortName}}Client(t *testing.T) { for index := 0; index < len(data.Writes); index++ { response := data.Writes[index] if response.Error != nil { - t.Fatalf("OpenFgaClient.%v()|%d/ %v", test.Name, index, response.Error) + t.Fatalf("{{appShortName}}Client.%v()|%d/ %v", test.Name, index, response.Error) } } }) @@ -2473,12 +2473,12 @@ func Test{{appShortName}}Client(t *testing.T) { } options := BatchCheckOptions{ - AuthorizationModelId: openfga.PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"), - MaxParallelRequests: openfga.PtrInt32(5), - MaxBatchSize: openfga.PtrInt32(10), + AuthorizationModelId: {{packageName}}.PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"), + MaxParallelRequests: {{packageName}}.PtrInt32(5), + MaxBatchSize: {{packageName}}.PtrInt32(10), } - var expectedResponse openfga.BatchCheckResponse + var expectedResponse {{packageName}}.BatchCheckResponse if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { t.Fatalf("%v", err) } @@ -2503,11 +2503,11 @@ func Test{{appShortName}}Client(t *testing.T) { } if httpmock.GetTotalCallCount() != 1 { - t.Fatalf("OpenFgaClient.%v() - wanted %v call to /batch-check, got %v", test.Name, 1, httpmock.GetTotalCallCount()) + t.Fatalf("{{appShortName}}Client.%v() - wanted %v call to /batch-check, got %v", test.Name, 1, httpmock.GetTotalCallCount()) } if len(got.GetResult()) != len(requestBody.Checks) { - t.Fatalf("OpenFgaClient.%v() - Response Length = %v, want %v", test.Name, len(got.GetResult()), len(requestBody.Checks)) + t.Fatalf("{{appShortName}}Client.%v() - Response Length = %v, want %v", test.Name, len(got.GetResult()), len(requestBody.Checks)) } // BatchCheck without options should work @@ -2522,8 +2522,8 @@ func Test{{appShortName}}Client(t *testing.T) { httpmock.ZeroCallCounters() // BatchCheck with invalid auth model ID should fail badOptions := BatchCheckOptions{ - AuthorizationModelId: openfga.PtrString("INVALID"), - MaxParallelRequests: openfga.PtrInt32(5), + AuthorizationModelId: {{packageName}}.PtrString("INVALID"), + MaxParallelRequests: {{packageName}}.PtrInt32(5), } ctxBad, cancelBad := context.WithTimeout(context.Background(), 5*time.Second) defer cancelBad() @@ -2534,8 +2534,8 @@ func Test{{appShortName}}Client(t *testing.T) { // Invalid store ID should fail badStoreOptions := BatchCheckOptions{ - StoreId: openfga.PtrString("INVALID"), - MaxParallelRequests: openfga.PtrInt32(5), + StoreId: {{packageName}}.PtrString("INVALID"), + MaxParallelRequests: {{packageName}}.PtrInt32(5), } ctxStore, cancelStore := context.WithTimeout(context.Background(), 5*time.Second) defer cancelStore() @@ -2546,8 +2546,8 @@ func Test{{appShortName}}Client(t *testing.T) { // Store should be overridden storeOverrideOptions := BatchCheckOptions{ - StoreId: openfga.PtrString("7777HCE4YVKPQEKZQHT2R89MQV"), - MaxParallelRequests: openfga.PtrInt32(5), + StoreId: {{packageName}}.PtrString("7777HCE4YVKPQEKZQHT2R89MQV"), + MaxParallelRequests: {{packageName}}.PtrInt32(5), } httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, *storeOverrideOptions.StoreId, test.RequestPath), @@ -3162,15 +3162,14 @@ func Test{{appShortName}}Client(t *testing.T) { Id: "roadmap", }, Relation: "can_read", -{{=<% %>=}} // API does not allow sending multiple filters, done here for testing purposes - UserFilters: []openfga.UserTypeFilter{{ + UserFilters: []{{packageName}}.UserTypeFilter{ { Type: "user", }, { Type: "team", - Relation: openfga.PtrString("member"), - }}, - ContextualTuples: []ClientContextualTupleKey{{ + Relation: {{packageName}}.PtrString("member"), + } }, + ContextualTuples: []ClientContextualTupleKey{ { User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation: "editor", Object: "folder:product", @@ -3178,8 +3177,7 @@ func Test{{appShortName}}Client(t *testing.T) { User: "folder:product", Relation: "parent", Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", - }}, -<%={{ }}=%> + } }, Context: &map[string]interface{}{"ViewCount": 100}, } options := ClientListUsersOptions{ @@ -3299,15 +3297,14 @@ func Test{{appShortName}}Client(t *testing.T) { Id: "roadmap", }, Relation: "can_read", -{{=<% %>=}} // API does not allow sending multiple filters, done here for testing purposes - UserFilters: []openfga.UserTypeFilter{{ + UserFilters: []{{packageName}}.UserTypeFilter{ { Type: "user", }, { Type: "team", - Relation: openfga.PtrString("member"), - }}, - ContextualTuples: []ClientContextualTupleKey{{ + Relation: {{packageName}}.PtrString("member"), + } }, + ContextualTuples: []ClientContextualTupleKey{ { User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation: "editor", Object: "folder:product", @@ -3315,13 +3312,12 @@ func Test{{appShortName}}Client(t *testing.T) { User: "folder:product", Relation: "parent", Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", - }}, -<%={{ }}=%> + } }, Context: &map[string]interface{}{"ViewCount": 100}, } options := ClientListUsersOptions{ AuthorizationModelId: {{packageName}}.PtrString("01GAHCE4YVKPQEKZQHT2R89MQV"), - Consistency: openfga.CONSISTENCYPREFERENCE_MINIMIZE_LATENCY.Ptr(), + Consistency: {{packageName}}.CONSISTENCYPREFERENCE_MINIMIZE_LATENCY.Ptr(), } var expectedResponse {{packageName}}.ListUsersResponse @@ -3563,10 +3559,10 @@ func Test{{appShortName}}Client(t *testing.T) { t.Fatalf("%v", err) } - result := map[string]openfga.BatchCheckSingleResult{ - "test1": {Allowed: openfga.PtrBool(true)}, + result := map[string]{{packageName}}.BatchCheckSingleResult{ + "test1": {Allowed: {{packageName}}.PtrBool(true)}, } - response := openfga.BatchCheckResponse{ + response := {{packageName}}.BatchCheckResponse{ Result: &result, } @@ -3614,7 +3610,7 @@ func Test{{appShortName}}Client(t *testing.T) { } options := BatchCheckOptions{ - MaxBatchSize: openfga.PtrInt32(1), + MaxBatchSize: {{packageName}}.PtrInt32(1), } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) @@ -3654,8 +3650,8 @@ func Test{{appShortName}}Client(t *testing.T) { } options := BatchCheckOptions{ - MaxParallelRequests: openfga.PtrInt32(2), // 2 parallel requests - MaxBatchSize: openfga.PtrInt32(50), // 50 items per batch + MaxParallelRequests: {{packageName}}.PtrInt32(2), // 2 parallel requests + MaxBatchSize: {{packageName}}.PtrInt32(50), // 50 items per batch } // Total capacity: 2*50 = 100, but we have 101 items @@ -3684,7 +3680,666 @@ func Test{{appShortName}}Client(t *testing.T) { }) } -func getStoreId(t *testing.T, fgaClient *OpenFgaClient) string { +func Test{{appShortName}}ClientWriteClientWriteConflictOptions(t *testing.T) { + fgaClient, err := NewSdkClient(&ClientConfiguration{ + ApiUrl: "https://api.fga.example", + StoreId: "01GXSB9YR785C4FYS3C0RTG7B2", + }) + if err != nil { + t.Fatalf("%v", err) + } + + t.Run("Client Write with OnDuplicateWrites ignore option", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + } }, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnDuplicate field set to "ignore" + var requestBody {{packageName}}.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore', got %v", requestBody.Writes.OnDuplicate) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with OnDuplicateWrites error option", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + } }, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnDuplicate field set to "error" + var requestBody {{packageName}}.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "error" { + t.Errorf("Expected OnDuplicate to be 'error', got %v", requestBody.Writes.OnDuplicate) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_ERROR, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with OnMissingDeletes ignore option", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Deletes: []ClientTupleKeyWithoutCondition{ { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + } }, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnMissing field set to "ignore" + var requestBody {{packageName}}.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Deletes == nil || requestBody.Deletes.OnMissing == nil || *requestBody.Deletes.OnMissing != "ignore" { + t.Errorf("Expected OnMissing to be 'ignore', got %v", requestBody.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with OnMissingDeletes error option", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Deletes: []ClientTupleKeyWithoutCondition{ { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + } }, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains the OnMissing field set to "error" + var requestBody {{packageName}}.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Deletes == nil || requestBody.Deletes.OnMissing == nil || *requestBody.Deletes.OnMissing != "error" { + t.Errorf("Expected OnMissing to be 'error', got %v", requestBody.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_ERROR, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with both OnDuplicateWrites and OnMissingDeletes options", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + } }, + Deletes: []ClientTupleKeyWithoutCondition{ { + User: "user:another-user", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + } }, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request body contains both OnDuplicate and OnMissing fields + var requestBody {{packageName}}.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore', got %v", requestBody.Writes.OnDuplicate) + } + if requestBody.Deletes == nil || requestBody.Deletes.OnMissing == nil || *requestBody.Deletes.OnMissing != "ignore" { + t.Errorf("Expected OnMissing to be 'ignore', got %v", requestBody.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with conflict options and transaction disabled", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + { + User: "user:another-user", + Relation: "viewer", + Object: "document:another-doc", + }, + }, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify each chunked request contains the OnDuplicate field + var requestBody {{packageName}}.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore' in chunked request, got %v", requestBody.Writes.OnDuplicate) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + }, + Transaction: &TransactionOptions{ + Disable: true, + MaxPerChunk: 1, + MaxParallelRequests: 2, + }, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with conflict options and authorization model", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + } }, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + modelId := "01GAHCE4YVKPQEKZQHT2R89MQV" + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request contains both conflict options and authorization model + var requestBody {{packageName}}.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore', got %v", requestBody.Writes.OnDuplicate) + } + if requestBody.AuthorizationModelId == nil || *requestBody.AuthorizationModelId != modelId { + t.Errorf("Expected AuthorizationModelId to be %s, got %v", modelId, requestBody.AuthorizationModelId) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + }, + AuthorizationModelId: &modelId, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write with store override and conflict options", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + } }, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + overrideStoreId := "01GXSB9YR785C4FYS3C0RTG7B3" + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, overrideStoreId, test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify the request contains conflict options + var requestBody {{packageName}}.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore', got %v", requestBody.Writes.OnDuplicate) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + }, + StoreId: &overrideStoreId, + } + _, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + }) + + t.Run("Client Write conflict options precedence test", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + { + User: "user:another-user", + Relation: "editor", + Object: "document:another-doc", + }, + }, + Deletes: []ClientTupleKeyWithoutCondition{ + { + User: "user:old-user", + Relation: "viewer", + Object: "document:old-doc", + }, + }, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + // Verify that client options override any default values + var requestBody {{packageName}}.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + + if requestBody.Writes == nil || requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected client OnDuplicateWrites option to override defaults, got %v", requestBody.Writes.OnDuplicate) + } + + if requestBody.Deletes == nil || requestBody.Deletes.OnMissing == nil || *requestBody.Deletes.OnMissing != "ignore" { + t.Errorf("Expected client OnMissingDeletes option to override defaults, got %v", requestBody.Deletes.OnMissing) + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE, + }, + } + + result, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if result == nil { + t.Fatalf("Expected non-nil result") + } + + if len(result.Writes) != 2 { + t.Fatalf("Expected 2 write results, got %d", len(result.Writes)) + } + + if len(result.Deletes) != 1 { + t.Fatalf("Expected 1 delete result, got %d", len(result.Deletes)) + } + }) + + t.Run("Client Write mixed conflict options with chunked requests", func(t *testing.T) { + test := TestDefinition{ + Name: "Write", + JsonResponse: `{}`, + ResponseStatus: 200, + Method: "POST", + RequestPath: "write", + } + + body := ClientWriteRequest{ + Writes: []ClientTupleKey{ + { + User: "user:writer1", + Relation: "viewer", + Object: "document:doc1", + }, + { + User: "user:writer2", + Relation: "viewer", + Object: "document:doc2", + }, + }, + Deletes: []ClientTupleKeyWithoutCondition{ + { + User: "user:deleter1", + Relation: "viewer", + Object: "document:doc3", + }, + { + User: "user:deleter2", + Relation: "viewer", + Object: "document:doc4", + }, + }, + } + + var expectedResponse map[string]interface{} + if err := json.Unmarshal([]byte(test.JsonResponse), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder(test.Method, fmt.Sprintf("%s/stores/%s/%s", fgaClient.GetConfig().ApiUrl, getStoreId(t, fgaClient), test.RequestPath), + func(req *http.Request) (*http.Response, error) { + var requestBody {{packageName}}.WriteRequest + if err := json.NewDecoder(req.Body).Decode(&requestBody); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + + if requestBody.Writes != nil && len(requestBody.Writes.TupleKeys) > 0 { + if requestBody.Writes.OnDuplicate == nil || *requestBody.Writes.OnDuplicate != "ignore" { + t.Errorf("Expected OnDuplicate to be 'ignore' in write chunk, got %v", requestBody.Writes.OnDuplicate) + } + } + + if requestBody.Deletes != nil && len(requestBody.Deletes.TupleKeys) > 0 { + if requestBody.Deletes.OnMissing == nil || *requestBody.Deletes.OnMissing != "ignore" { + t.Errorf("Expected OnMissing to be 'ignore' in delete chunk, got %v", requestBody.Deletes.OnMissing) + } + } + + resp, err := httpmock.NewJsonResponse(test.ResponseStatus, expectedResponse) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }, + ) + + options := ClientWriteOptions{ + Conflict: ClientWriteConflictOptions{ + OnDuplicateWrites: CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + OnMissingDeletes: CLIENT_WRITE_REQUEST_ON_MISSING_DELETES_IGNORE, + }, + Transaction: &TransactionOptions{ + Disable: true, + MaxPerChunk: 1, + MaxParallelRequests: 4, + }, + } + + result, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if result == nil { + t.Fatalf("Expected non-nil result") + } + + if len(result.Writes) != 2 { + t.Fatalf("Expected 2 write results, got %d", len(result.Writes)) + } + + if len(result.Deletes) != 2 { + t.Fatalf("Expected 2 delete results, got %d", len(result.Deletes)) + } + }) +} + +func getStoreId(t *testing.T, fgaClient *{{appShortName}}Client) string { storeId, err := fgaClient.GetStoreId() if err != nil { t.Fatalf("%v", err) diff --git a/config/clients/go/template/example/example1/example1.go b/config/clients/go/template/example/example1/example1.go index 3658664f4..d7853c73f 100644 --- a/config/clients/go/template/example/example1/example1.go +++ b/config/clients/go/template/example/example1/example1.go @@ -200,6 +200,10 @@ func mainInner() error { }, }).Options(client.ClientWriteOptions{ AuthorizationModelId: &authorizationModel.AuthorizationModelId, + Conflict: client.ClientWriteConflictOptions{ + // We can choose to ignore conflicts during writes + OnDuplicateWrites: client.CLIENT_WRITE_REQUEST_ON_DUPLICATE_WRITES_IGNORE, + }, }).Execute() if err != nil { return err diff --git a/config/clients/go/template/models_test.mustache b/config/clients/go/template/models_test.mustache new file mode 100644 index 000000000..2105fd56d --- /dev/null +++ b/config/clients/go/template/models_test.mustache @@ -0,0 +1,759 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "encoding/json" + "testing" +) + +func TestWriteRequestDeletes(t *testing.T) { + t.Run("NewWriteRequestDeletes sets default OnMissing", func(t *testing.T) { + tupleKeys := []TupleKeyWithoutCondition{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + deletes := NewWriteRequestDeletes(tupleKeys) + + if deletes.OnMissing == nil { + t.Fatalf("Expected OnMissing to be non-nil with default value") + } + + if *deletes.OnMissing != "error" { + t.Fatalf("Expected OnMissing default to be 'error', got %v", *deletes.OnMissing) + } + + if len(deletes.TupleKeys) != 1 { + t.Fatalf("Expected 1 tuple key, got %d", len(deletes.TupleKeys)) + } + }) + + t.Run("NewWriteRequestDeletesWithDefaults sets default OnMissing", func(t *testing.T) { + deletes := NewWriteRequestDeletesWithDefaults() + + if deletes.OnMissing == nil { + t.Fatalf("Expected OnMissing to be non-nil with default value") + } + + if *deletes.OnMissing != "error" { + t.Fatalf("Expected OnMissing default to be 'error', got %v", *deletes.OnMissing) + } + }) + + t.Run("GetOnMissing returns correct value", func(t *testing.T) { + deletes := WriteRequestDeletes{} + + // Test when OnMissing is nil + if deletes.GetOnMissing() != "" { + t.Fatalf("Expected empty string when OnMissing is nil, got %v", deletes.GetOnMissing()) + } + + // Test when OnMissing is set + onMiss := "ignore" + deletes.OnMissing = &onMiss + + if deletes.GetOnMissing() != "ignore" { + t.Fatalf("Expected 'ignore', got %v", deletes.GetOnMissing()) + } + }) + + t.Run("GetOnMissingOk returns correct value and status", func(t *testing.T) { + deletes := WriteRequestDeletes{} + + // Test when OnMissing is nil + value, ok := deletes.GetOnMissingOk() + if value != nil || ok { + t.Fatalf("Expected nil value and false status when OnMissing is nil, got %v, %v", value, ok) + } + + // Test when OnMissing is set + onMiss := "ignore" + deletes.OnMissing = &onMiss + + value, ok = deletes.GetOnMissingOk() + if value == nil || !ok || *value != "ignore" { + t.Fatalf("Expected 'ignore' value and true status, got %v, %v", value, ok) + } + }) + + t.Run("HasOnMissing returns correct status", func(t *testing.T) { + deletes := WriteRequestDeletes{} + + // Test when OnMissing is nil + if deletes.HasOnMissing() { + t.Fatalf("Expected false when OnMissing is nil") + } + + // Test when OnMissing is set + onMiss := "ignore" + deletes.OnMissing = &onMiss + + if !deletes.HasOnMissing() { + t.Fatalf("Expected true when OnMissing is set") + } + }) + + t.Run("SetOnMissing sets value correctly", func(t *testing.T) { + deletes := WriteRequestDeletes{} + + deletes.SetOnMissing("ignore") + + if deletes.OnMissing == nil || *deletes.OnMissing != "ignore" { + t.Fatalf("Expected OnMissing to be 'ignore', got %v", deletes.OnMissing) + } + }) + + t.Run("MarshalJSON includes OnMissing when set", func(t *testing.T) { + tupleKeys := []TupleKeyWithoutCondition{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + deletes := WriteRequestDeletes{ + TupleKeys: tupleKeys, + } + + // Test without OnMissing + jsonData, err := deletes.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling JSON: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if _, exists := result["on_missing"]; exists { + t.Fatalf("Expected on_missing to not be present when nil") + } + + // Test with OnMissing + onMiss := "ignore" + deletes.OnMissing = &onMiss + + jsonData, err = deletes.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling JSON: %v", err) + } + + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if result["on_missing"] != "ignore" { + t.Fatalf("Expected on_missing to be 'ignore', got %v", result["on_missing"]) + } + }) + + t.Run("OnMissing validation - valid values", func(t *testing.T) { + validValues := []string{"error", "ignore"} + + for _, value := range validValues { + deletes := WriteRequestDeletes{} + deletes.SetOnMissing(value) + + if deletes.GetOnMissing() != value { + t.Fatalf("Expected OnMissing to be '%s', got %v", value, deletes.GetOnMissing()) + } + } + }) +} + +func TestWriteRequestWrites(t *testing.T) { + t.Run("NewWriteRequestWrites sets default OnDuplicate", func(t *testing.T) { + tupleKeys := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + writes := NewWriteRequestWrites(tupleKeys) + + if writes.OnDuplicate == nil { + t.Fatalf("Expected OnDuplicate to be non-nil with default value") + } + + if *writes.OnDuplicate != "error" { + t.Fatalf("Expected OnDuplicate default to be 'error', got %v", *writes.OnDuplicate) + } + + if len(writes.TupleKeys) != 1 { + t.Fatalf("Expected 1 tuple key, got %d", len(writes.TupleKeys)) + } + }) + + t.Run("NewWriteRequestWritesWithDefaults sets default OnDuplicate", func(t *testing.T) { + writes := NewWriteRequestWritesWithDefaults() + + if writes.OnDuplicate == nil { + t.Fatalf("Expected OnDuplicate to be non-nil with default value") + } + + if *writes.OnDuplicate != "error" { + t.Fatalf("Expected OnDuplicate default to be 'error', got %v", *writes.OnDuplicate) + } + }) + + t.Run("GetOnDuplicate returns correct value", func(t *testing.T) { + writes := WriteRequestWrites{} + + // Test when OnDuplicate is nil + if writes.GetOnDuplicate() != "" { + t.Fatalf("Expected empty string when OnDuplicate is nil, got %v", writes.GetOnDuplicate()) + } + + // Test when OnDuplicate is set + onDup := "ignore" + writes.OnDuplicate = &onDup + + if writes.GetOnDuplicate() != "ignore" { + t.Fatalf("Expected 'ignore', got %v", writes.GetOnDuplicate()) + } + }) + + t.Run("GetOnDuplicateOk returns correct value and status", func(t *testing.T) { + writes := WriteRequestWrites{} + + // Test when OnDuplicate is nil + value, ok := writes.GetOnDuplicateOk() + if value != nil || ok { + t.Fatalf("Expected nil value and false status when OnDuplicate is nil, got %v, %v", value, ok) + } + + // Test when OnDuplicate is set + onDup := "ignore" + writes.OnDuplicate = &onDup + + value, ok = writes.GetOnDuplicateOk() + if value == nil || !ok || *value != "ignore" { + t.Fatalf("Expected 'ignore' value and true status, got %v, %v", value, ok) + } + }) + + t.Run("HasOnDuplicate returns correct status", func(t *testing.T) { + writes := WriteRequestWrites{} + + // Test when OnDuplicate is nil + if writes.HasOnDuplicate() { + t.Fatalf("Expected false when OnDuplicate is nil") + } + + // Test when OnDuplicate is set + onDup := "ignore" + writes.OnDuplicate = &onDup + + if !writes.HasOnDuplicate() { + t.Fatalf("Expected true when OnDuplicate is set") + } + }) + + t.Run("SetOnDuplicate sets value correctly", func(t *testing.T) { + writes := WriteRequestWrites{} + + writes.SetOnDuplicate("ignore") + + if writes.OnDuplicate == nil || *writes.OnDuplicate != "ignore" { + t.Fatalf("Expected OnDuplicate to be 'ignore', got %v", writes.OnDuplicate) + } + }) + + t.Run("MarshalJSON includes OnDuplicate when set", func(t *testing.T) { + tupleKeys := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + writes := WriteRequestWrites{ + TupleKeys: tupleKeys, + } + + // Test without OnDuplicate + jsonData, err := writes.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling JSON: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if _, exists := result["on_duplicate"]; exists { + t.Fatalf("Expected on_duplicate to not be present when nil") + } + + // Test with OnDuplicate + onDup := "ignore" + writes.OnDuplicate = &onDup + + jsonData, err = writes.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling JSON: %v", err) + } + + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if result["on_duplicate"] != "ignore" { + t.Fatalf("Expected on_duplicate to be 'ignore', got %v", result["on_duplicate"]) + } + }) + + t.Run("OnDuplicate validation - valid values", func(t *testing.T) { + validValues := []string{"error", "ignore"} + + for _, value := range validValues { + writes := WriteRequestWrites{} + writes.SetOnDuplicate(value) + + if writes.GetOnDuplicate() != value { + t.Fatalf("Expected OnDuplicate to be '%s', got %v", value, writes.GetOnDuplicate()) + } + } + }) +} + +func TestWriteRequest(t *testing.T) { + t.Run("NewWriteRequest creates empty WriteRequest", func(t *testing.T) { + writeRequest := NewWriteRequest() + + if writeRequest == nil { + t.Fatalf("Expected non-nil WriteRequest") + } + + if writeRequest.Writes != nil { + t.Fatalf("Expected Writes to be nil by default") + } + + if writeRequest.Deletes != nil { + t.Fatalf("Expected Deletes to be nil by default") + } + + if writeRequest.AuthorizationModelId != nil { + t.Fatalf("Expected AuthorizationModelId to be nil by default") + } + }) + + t.Run("NewWriteRequestWithDefaults creates empty WriteRequest", func(t *testing.T) { + writeRequest := NewWriteRequestWithDefaults() + + if writeRequest == nil { + t.Fatalf("Expected non-nil WriteRequest") + } + + if writeRequest.Writes != nil { + t.Fatalf("Expected Writes to be nil by default") + } + + if writeRequest.Deletes != nil { + t.Fatalf("Expected Deletes to be nil by default") + } + + if writeRequest.AuthorizationModelId != nil { + t.Fatalf("Expected AuthorizationModelId to be nil by default") + } + }) + + t.Run("Writes field operations", func(t *testing.T) { + writeRequest := WriteRequest{} + + // Test GetWrites when nil + writes := writeRequest.GetWrites() + if writes.TupleKeys != nil { + t.Fatalf("Expected empty WriteRequestWrites when Writes is nil") + } + + // Test GetWritesOk when nil + writesPtr, ok := writeRequest.GetWritesOk() + if writesPtr != nil || ok { + t.Fatalf("Expected nil pointer and false when Writes is nil, got %v, %v", writesPtr, ok) + } + + // Test HasWrites when nil + if writeRequest.HasWrites() { + t.Fatalf("Expected false when Writes is nil") + } + + // Test SetWrites + tupleKeys := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + writesValue := WriteRequestWrites{TupleKeys: tupleKeys} + writeRequest.SetWrites(writesValue) + + // Test GetWrites after setting + retrievedWrites := writeRequest.GetWrites() + if len(retrievedWrites.TupleKeys) != 1 { + t.Fatalf("Expected 1 tuple key, got %d", len(retrievedWrites.TupleKeys)) + } + + // Test GetWritesOk after setting + writesPtr, ok = writeRequest.GetWritesOk() + if writesPtr == nil || !ok || len(writesPtr.TupleKeys) != 1 { + t.Fatalf("Expected valid pointer and true after setting Writes") + } + + // Test HasWrites after setting + if !writeRequest.HasWrites() { + t.Fatalf("Expected true after setting Writes") + } + }) + + t.Run("Deletes field operations", func(t *testing.T) { + writeRequest := WriteRequest{} + + // Test GetDeletes when nil + deletes := writeRequest.GetDeletes() + if deletes.TupleKeys != nil { + t.Fatalf("Expected empty WriteRequestDeletes when Deletes is nil") + } + + // Test GetDeletesOk when nil + deletesPtr, ok := writeRequest.GetDeletesOk() + if deletesPtr != nil || ok { + t.Fatalf("Expected nil pointer and false when Deletes is nil, got %v, %v", deletesPtr, ok) + } + + // Test HasDeletes when nil + if writeRequest.HasDeletes() { + t.Fatalf("Expected false when Deletes is nil") + } + + // Test SetDeletes + tupleKeys := []TupleKeyWithoutCondition{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + deletesValue := WriteRequestDeletes{TupleKeys: tupleKeys} + writeRequest.SetDeletes(deletesValue) + + // Test GetDeletes after setting + retrievedDeletes := writeRequest.GetDeletes() + if len(retrievedDeletes.TupleKeys) != 1 { + t.Fatalf("Expected 1 tuple key, got %d", len(retrievedDeletes.TupleKeys)) + } + + // Test GetDeletesOk after setting + deletesPtr, ok = writeRequest.GetDeletesOk() + if deletesPtr == nil || !ok || len(deletesPtr.TupleKeys) != 1 { + t.Fatalf("Expected valid pointer and true after setting Deletes") + } + + // Test HasDeletes after setting + if !writeRequest.HasDeletes() { + t.Fatalf("Expected true after setting Deletes") + } + }) + + t.Run("AuthorizationModelId field operations", func(t *testing.T) { + writeRequest := WriteRequest{} + + // Test GetAuthorizationModelId when nil + modelId := writeRequest.GetAuthorizationModelId() + if modelId != "" { + t.Fatalf("Expected empty string when AuthorizationModelId is nil, got %v", modelId) + } + + // Test GetAuthorizationModelIdOk when nil + modelIdPtr, ok := writeRequest.GetAuthorizationModelIdOk() + if modelIdPtr != nil || ok { + t.Fatalf("Expected nil pointer and false when AuthorizationModelId is nil, got %v, %v", modelIdPtr, ok) + } + + // Test HasAuthorizationModelId when nil + if writeRequest.HasAuthorizationModelId() { + t.Fatalf("Expected false when AuthorizationModelId is nil") + } + + // Test SetAuthorizationModelId + expectedModelId := "01GAHCE4YVKPQEKZQHT2R89MQV" + writeRequest.SetAuthorizationModelId(expectedModelId) + + // Test GetAuthorizationModelId after setting + retrievedModelId := writeRequest.GetAuthorizationModelId() + if retrievedModelId != expectedModelId { + t.Fatalf("Expected %s, got %s", expectedModelId, retrievedModelId) + } + + // Test GetAuthorizationModelIdOk after setting + modelIdPtr, ok = writeRequest.GetAuthorizationModelIdOk() + if modelIdPtr == nil || !ok || *modelIdPtr != expectedModelId { + t.Fatalf("Expected valid pointer and true after setting AuthorizationModelId") + } + + // Test HasAuthorizationModelId after setting + if !writeRequest.HasAuthorizationModelId() { + t.Fatalf("Expected true after setting AuthorizationModelId") + } + }) + + t.Run("MarshalJSON includes all fields when set", func(t *testing.T) { + writeRequest := WriteRequest{} + + // Test marshaling empty request + jsonData, err := writeRequest.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling empty WriteRequest: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if len(result) != 0 { + t.Fatalf("Expected empty object for empty WriteRequest, got %v", result) + } + + // Test marshaling with all fields set + tupleKeysWrite := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + tupleKeysDelete := []TupleKeyWithoutCondition{ + { + User: "user:another-user", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + onDuplicate := "ignore" + onMissing := "ignore" + writes := WriteRequestWrites{TupleKeys: tupleKeysWrite, OnDuplicate: &onDuplicate} + deletes := WriteRequestDeletes{TupleKeys: tupleKeysDelete, OnMissing: &onMissing} + modelId := "01GAHCE4YVKPQEKZQHT2R89MQV" + + writeRequest.SetWrites(writes) + writeRequest.SetDeletes(deletes) + writeRequest.SetAuthorizationModelId(modelId) + + jsonData, err = writeRequest.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling complete WriteRequest: %v", err) + } + + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + // Verify all fields are present + if _, exists := result["writes"]; !exists { + t.Fatalf("Expected 'writes' field in JSON") + } + + if _, exists := result["deletes"]; !exists { + t.Fatalf("Expected 'deletes' field in JSON") + } + + if _, exists := result["authorization_model_id"]; !exists { + t.Fatalf("Expected 'authorization_model_id' field in JSON") + } + + if result["authorization_model_id"] != modelId { + t.Fatalf("Expected authorization_model_id to be %s, got %v", modelId, result["authorization_model_id"]) + } + + // Verify nested conflict options are preserved + writesMap := result["writes"].(map[string]interface{}) + if writesMap["on_duplicate"] != "ignore" { + t.Fatalf("Expected writes.on_duplicate to be 'ignore', got %v", writesMap["on_duplicate"]) + } + + deletesMap := result["deletes"].(map[string]interface{}) + if deletesMap["on_missing"] != "ignore" { + t.Fatalf("Expected deletes.on_missing to be 'ignore', got %v", deletesMap["on_missing"]) + } + }) + + t.Run("WriteRequest with only writes", func(t *testing.T) { + writeRequest := WriteRequest{} + + tupleKeys := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + onDuplicate := "error" + writes := WriteRequestWrites{TupleKeys: tupleKeys, OnDuplicate: &onDuplicate} + writeRequest.SetWrites(writes) + + // Verify only writes is set + if !writeRequest.HasWrites() { + t.Fatalf("Expected HasWrites to be true") + } + + if writeRequest.HasDeletes() { + t.Fatalf("Expected HasDeletes to be false") + } + + if writeRequest.HasAuthorizationModelId() { + t.Fatalf("Expected HasAuthorizationModelId to be false") + } + + // Test JSON marshaling + jsonData, err := writeRequest.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling WriteRequest: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if _, exists := result["writes"]; !exists { + t.Fatalf("Expected 'writes' field in JSON") + } + + if _, exists := result["deletes"]; exists { + t.Fatalf("Expected 'deletes' field to not be in JSON") + } + + if _, exists := result["authorization_model_id"]; exists { + t.Fatalf("Expected 'authorization_model_id' field to not be in JSON") + } + }) + + t.Run("WriteRequest with only deletes", func(t *testing.T) { + writeRequest := WriteRequest{} + + tupleKeys := []TupleKeyWithoutCondition{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + onMissing := "error" + deletes := WriteRequestDeletes{TupleKeys: tupleKeys, OnMissing: &onMissing} + writeRequest.SetDeletes(deletes) + + // Verify only deletes is set + if writeRequest.HasWrites() { + t.Fatalf("Expected HasWrites to be false") + } + + if !writeRequest.HasDeletes() { + t.Fatalf("Expected HasDeletes to be true") + } + + if writeRequest.HasAuthorizationModelId() { + t.Fatalf("Expected HasAuthorizationModelId to be false") + } + + // Test JSON marshaling + jsonData, err := writeRequest.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling WriteRequest: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + if _, exists := result["writes"]; exists { + t.Fatalf("Expected 'writes' field to not be in JSON") + } + + if _, exists := result["deletes"]; !exists { + t.Fatalf("Expected 'deletes' field in JSON") + } + + if _, exists := result["authorization_model_id"]; exists { + t.Fatalf("Expected 'authorization_model_id' field to not be in JSON") + } + }) + + t.Run("WriteRequest with conflict options integration", func(t *testing.T) { + writeRequest := WriteRequest{} + + // Test with writes having ignore duplicate policy + tupleKeysWrite := []TupleKey{ + { + User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + // Test with deletes having ignore missing policy + tupleKeysDelete := []TupleKeyWithoutCondition{ + { + User: "user:another-user", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + }, + } + + onDuplicate := "ignore" + onMissing := "ignore" + + writes := WriteRequestWrites{TupleKeys: tupleKeysWrite, OnDuplicate: &onDuplicate} + deletes := WriteRequestDeletes{TupleKeys: tupleKeysDelete, OnMissing: &onMissing} + + writeRequest.SetWrites(writes) + writeRequest.SetDeletes(deletes) + writeRequest.SetAuthorizationModelId("01GAHCE4YVKPQEKZQHT2R89MQV") + + // Verify conflict options are preserved through getter methods + retrievedWrites := writeRequest.GetWrites() + if retrievedWrites.GetOnDuplicate() != "ignore" { + t.Fatalf("Expected OnDuplicate to be 'ignore', got %v", retrievedWrites.GetOnDuplicate()) + } + + retrievedDeletes := writeRequest.GetDeletes() + if retrievedDeletes.GetOnMissing() != "ignore" { + t.Fatalf("Expected OnMissing to be 'ignore', got %v", retrievedDeletes.GetOnMissing()) + } + + // Verify JSON serialization preserves conflict options + jsonData, err := writeRequest.MarshalJSON() + if err != nil { + t.Fatalf("Error marshaling WriteRequest: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + t.Fatalf("Error unmarshaling JSON: %v", err) + } + + writesMap := result["writes"].(map[string]interface{}) + if writesMap["on_duplicate"] != "ignore" { + t.Fatalf("Expected writes.on_duplicate to be 'ignore' in JSON, got %v", writesMap["on_duplicate"]) + } + + deletesMap := result["deletes"].(map[string]interface{}) + if deletesMap["on_missing"] != "ignore" { + t.Fatalf("Expected deletes.on_missing to be 'ignore' in JSON, got %v", deletesMap["on_missing"]) + } + }) +}