From 74e8d38e175ae9d6da43fc2f184401af11dfd533 Mon Sep 17 00:00:00 2001 From: Raghd Hamzeh Date: Tue, 7 Oct 2025 10:05:40 -0400 Subject: [PATCH] feat: support per-request headers and connection options (#233) --- .openapi-generator/FILES | 3 + CHANGELOG.md | 5 + README.md | 76 +++ api_client.go | 4 +- api_headers_test.go | 824 ++++++++++++++++++++++++++++++ api_open_fga.go | 213 +++++++- api_open_fga_test.go | 292 +++++++++++ client/client.go | 289 +++++++++-- client/client_headers_test.go | 881 +++++++++++++++++++++++++++++++++ client/client_test.go | 659 ++++++++++++++++++++++++ docs/WriteRequestDeletes.md | 26 + docs/WriteRequestWrites.md | 26 + example/example1/example1.go | 22 + example/example1/go.mod | 2 +- example/opentelemetry/go.mod | 2 +- model_write_request_deletes.go | 41 ++ model_write_request_writes.go | 41 ++ models_test.go | 770 ++++++++++++++++++++++++++++ 18 files changed, 4135 insertions(+), 41 deletions(-) create mode 100644 api_headers_test.go create mode 100644 client/client_headers_test.go create mode 100644 models_test.go diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index ce3d770..32e6240 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -13,9 +13,11 @@ README.md VERSION.txt api_client.go api_client_test.go +api_headers_test.go api_open_fga.go api_open_fga_test.go client/client.go +client/client_headers_test.go client/client_test.go client/errors.go configuration.go @@ -210,6 +212,7 @@ model_write_authorization_model_response.go model_write_request.go model_write_request_deletes.go model_write_request_writes.go +models_test.go oauth2/LICENSE oauth2/ORIGINAL_AUTHORS oauth2/ORIGINAL_CONTRIBUTORS diff --git a/CHANGELOG.md b/CHANGELOG.md index e4b1327..c17ad3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased](https://github.com/openfga/go-sdk/compare/v0.7.2...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. + ## 0.7.2 ### [0.7.2](https://github.com/openfga/go-sdk/compare/v0.7.1...0.7.2) (2025-09-15) diff --git a/README.md b/README.md index 1b99a78..c2cf1f5 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,46 @@ func main() { } ``` +### Custom Headers + +#### Default Headers +You can set default headers that will be sent with every request during client initialization: + +```golang +fgaClient, err := client.NewSdkClient(&client.ClientConfiguration{ + ApiUrl: os.Getenv("FGA_API_URL"), + StoreId: os.Getenv("FGA_STORE_ID"), + AuthorizationModelId: os.Getenv("FGA_MODEL_ID"), + DefaultHeaders: map[string]string{ + "X-Custom-Header": "default-value", + "X-Request-Source": "my-app", + }, +}) +``` + +#### Per-Request Headers + +You can also send custom headers on a per-request basis by using the `Options` parameter. Custom headers will override any default headers set in the client configuration. + +```golang +// Add custom headers to a specific request +checkResponse, err := fgaClient.Check(context.Background()). + Body(client.ClientCheckRequest{ + User: "user:anne", + Relation: "viewer", + Object: "document:roadmap", + }). + Options(client.ClientCheckOptions{ + RequestOptions: client.RequestOptions{ + Headers: map[string]string{ + "X-Request-ID": "123e4567-e89b-12d3-a456-426614174000", + "X-Custom-Header": "custom-value", // these override any default headers set + }, + }, + }). + Execute() +``` + ### Get your Store ID @@ -591,6 +631,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/api_client.go b/api_client.go index 9e5fe6d..0da31bd 100644 --- a/api_client.go +++ b/api_client.go @@ -284,7 +284,9 @@ func (c *APIClient) prepareRequest( localVarRequest.Header.Set("User-Agent", c.cfg.UserAgent) for header, value := range c.cfg.DefaultHeaders { - localVarRequest.Header.Set(header, value) + if localVarRequest.Header.Get(header) == "" { + localVarRequest.Header.Set(header, value) + } } if ctx != nil { diff --git a/api_headers_test.go b/api_headers_test.go new file mode 100644 index 0000000..c5dc756 --- /dev/null +++ b/api_headers_test.go @@ -0,0 +1,824 @@ +package openfga_test + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + openfga "github.com/openfga/go-sdk" +) + +// Test helpers and setup + +// Constants to avoid duplication +const ( + apiDefaultHeaderName = "Default-Header" + apiDefaultHeaderValue = "default-value" + apiOverriddenValue = "overridden-value" + apiCustomHeaderName = "X-Custom-Header" + apiCustomHeaderValue = "custom-value" + apiTestUser = "user:anne" + apiTestRelation = "viewer" + apiTestObject = "document:roadmap" + apiTestStoreId = "01H0H015178Y2V4CX10C2KGHF4" + apiRequestFailedMsg = "API request failed: %v" + apiExpectedCustomMsg = "Expected X-Custom-Header to be 'custom-value', got '%s'" + apiExpectedOverriddenMsg = "Expected Default-Header to be overridden to 'overridden-value', got '%s'" + apiExpectedDefaultMsg = "Expected Default-Header to be 'default-value', got '%s'" +) + +// createAPITestServer creates a test server that captures headers and returns appropriate responses +func createAPITestServer(t *testing.T, capturedHeaders *map[string]string, responseBody string) *httptest.Server { + t.Helper() + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + *capturedHeaders = make(map[string]string) + for name, values := range r.Header { + if len(values) > 0 { + (*capturedHeaders)[name] = values[0] + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(responseBody)) + })) +} + +// createAPITestClient creates a test API client with the given server URL and default headers +func createAPITestClient(t *testing.T, serverURL string, defaultHeaders map[string]string) *openfga.APIClient { + t.Helper() + + config, err := openfga.NewConfiguration(openfga.Configuration{ + ApiUrl: serverURL, + DefaultHeaders: defaultHeaders, + }) + if err != nil { + t.Fatalf("Failed to create API configuration: %v", err) + } + + return openfga.NewAPIClient(config) +} + +// Misc tests + +// Test RequestOptions structure directly at API level +func TestAPIRequestOptionsStructure(t *testing.T) { + t.Run("RequestOptionsWithAllFields", func(t *testing.T) { + options := openfga.RequestOptions{ + Headers: map[string]string{ + "Test-Header": "test-value", + }, + } + + if options.Headers["Test-Header"] != "test-value" { + t.Errorf("Expected Test-Header to be 'test-value', got '%s'", options.Headers["Test-Header"]) + } + }) + + t.Run("RequestOptionsWithNilHeaders", func(t *testing.T) { + options := openfga.RequestOptions{ + Headers: nil, + } + + if options.Headers != nil { + t.Errorf("Expected Headers to be nil, got %v", options.Headers) + } + }) + + t.Run("RequestOptionsWithEmptyHeaders", func(t *testing.T) { + options := openfga.RequestOptions{ + Headers: map[string]string{}, + } + + if len(options.Headers) != 0 { + t.Errorf("Expected Headers to be empty, got %v", options.Headers) + } + }) +} + +// Test header precedence and merging behavior +func TestAPIHeaderPrecedenceHandling(t *testing.T) { + t.Run("CustomHeadersOverrideDefaults", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"allowed": true}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + "Header-1": "default-1", + "Header-2": "default-2", + "Header-3": "default-3", + }) + + _, _, err := client.OpenFgaApi.Check(context.Background(), apiTestStoreId). + Body(openfga.CheckRequest{ + TupleKey: openfga.CheckRequestTupleKey{ + User: apiTestUser, + Relation: apiTestRelation, + Object: apiTestObject, + }, + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + "Header-1": "overridden-1", // Override the default + "Header-4": "custom-4", + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders["Header-1"] != "overridden-1" { + t.Errorf("Expected Header-1 to be 'overridden-1', got '%s'", capturedHeaders["Header-1"]) + } + + if capturedHeaders["Header-2"] != "default-2" { + t.Errorf("Expected Header-2 to be 'default-2', got '%s'", capturedHeaders["Header-2"]) + } + + if capturedHeaders["Header-3"] != "default-3" { + t.Errorf("Expected Header-3 to be 'default-3', got '%s'", capturedHeaders["Header-3"]) + } + + if capturedHeaders["Header-4"] != "custom-4" { + t.Errorf("Expected Header-4 to be 'custom-4', got '%s'", capturedHeaders["Header-4"]) + } + }) +} + +// Test the header handling for the methods + +func TestCheckAPIMethodHeaderHandling(t *testing.T) { + t.Run("CheckAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"allowed": true}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.Check(context.Background(), apiTestStoreId). + Body(openfga.CheckRequest{ + TupleKey: openfga.CheckRequestTupleKey{ + User: apiTestUser, + Relation: apiTestRelation, + Object: apiTestObject, + }, + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) + + t.Run("CheckAPIWithoutCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"allowed": true}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.Check(context.Background(), apiTestStoreId). + Body(openfga.CheckRequest{ + TupleKey: openfga.CheckRequestTupleKey{ + User: apiTestUser, + Relation: apiTestRelation, + Object: apiTestObject, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiDefaultHeaderName] != apiDefaultHeaderValue { + t.Errorf(apiExpectedDefaultMsg, capturedHeaders[apiDefaultHeaderName]) + } + + if _, exists := capturedHeaders[apiCustomHeaderName]; exists { + t.Error("Did not expect X-Custom-Header to be present") + } + }) + + t.Run("CheckAPIWithEmptyHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"allowed": true}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, nil) + + _, _, err := client.OpenFgaApi.Check(context.Background(), apiTestStoreId). + Body(openfga.CheckRequest{ + TupleKey: openfga.CheckRequestTupleKey{ + User: apiTestUser, + Relation: apiTestRelation, + Object: apiTestObject, + }, + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{}, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + for header := range capturedHeaders { + if strings.HasPrefix(header, "X-") || header == apiDefaultHeaderName { + t.Errorf("Unexpected custom header found: %s", header) + } + } + }) +} + +func TestBatchCheckAPIMethodHeaderHandling(t *testing.T) { + t.Run("BatchCheckAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"test-correlation-id": {"allowed": true}}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.BatchCheck(context.Background(), apiTestStoreId). + Body(openfga.BatchCheckRequest{ + Checks: []openfga.BatchCheckItem{ + { + TupleKey: openfga.CheckRequestTupleKey{ + User: apiTestUser, + Relation: apiTestRelation, + Object: apiTestObject, + }, + CorrelationId: "test-correlation-id", + }, + }, + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} + +func TestWriteAPIMethodHeaderHandling(t *testing.T) { + t.Run("WriteAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.Write(context.Background(), apiTestStoreId). + Body(openfga.WriteRequest{ + Writes: &openfga.WriteRequestWrites{ + TupleKeys: []openfga.TupleKey{ + { + User: apiTestUser, + Relation: apiTestRelation, + Object: apiTestObject, + }, + }, + }, + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} + +func TestReadAPIMethodHeaderHandling(t *testing.T) { + t.Run("ReadAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"tuples": []}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.Read(context.Background(), apiTestStoreId). + Body(openfga.ReadRequest{ + TupleKey: &openfga.ReadRequestTupleKey{ + User: openfga.PtrString(apiTestUser), + Relation: openfga.PtrString(apiTestRelation), + Object: openfga.PtrString(apiTestObject), + }, + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} + +func TestExpandAPIMethodHeaderHandling(t *testing.T) { + t.Run("ExpandAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"tree": {"root": {"name": "document:roadmap#viewer"}}}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.Expand(context.Background(), apiTestStoreId). + Body(openfga.ExpandRequest{ + TupleKey: openfga.ExpandRequestTupleKey{ + Relation: apiTestRelation, + Object: apiTestObject, + }, + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} + +func TestListObjectsAPIMethodHeaderHandling(t *testing.T) { + t.Run("ListObjectsAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"objects": ["document:roadmap"]}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.ListObjects(context.Background(), apiTestStoreId). + Body(openfga.ListObjectsRequest{ + User: apiTestUser, + Relation: apiTestRelation, + Type: "document", + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} + +func TestListUsersAPIMethodHeaderHandling(t *testing.T) { + t.Run("ListUsersAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"users": [{"object": {"type": "user", "id": "anne"}}]}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.ListUsers(context.Background(), apiTestStoreId). + Body(openfga.ListUsersRequest{ + Object: openfga.FgaObject{ + Type: "document", + Id: "roadmap", + }, + Relation: apiTestRelation, + UserFilters: []openfga.UserTypeFilter{ + {Type: "user"}, + }, + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} + +func TestStoreAPIMethodHeaderHandling(t *testing.T) { + t.Run("ListStoresAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"stores": [{"id": "01H0H015178Y2V4CX10C2KGHF4", "name": "test"}]}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.ListStores(context.Background()). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) + + t.Run("CreateStoreAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"id": "01H0H015178Y2V4CX10C2KGHF4", "name": "test"}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.CreateStore(context.Background()). + Body(openfga.CreateStoreRequest{ + Name: "test", + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) + + t.Run("GetStoreAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"id": "01H0H015178Y2V4CX10C2KGHF4", "name": "test"}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.GetStore(context.Background(), apiTestStoreId). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) + + t.Run("DeleteStoreAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, err := client.OpenFgaApi.DeleteStore(context.Background(), apiTestStoreId). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} + +func TestReadAuthorizationModelAPIMethodHeaderHandling(t *testing.T) { + t.Run("ReadAuthorizationModelAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"authorization_model": {"id": "01H0H015178Y2V4CX10C2KGHF4", "schema_version": "1.1"}}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.ReadAuthorizationModel(context.Background(), apiTestStoreId, apiTestStoreId). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} + +func TestWriteAuthorizationModelAPIMethodHeaderHandling(t *testing.T) { + t.Run("WriteAuthorizationModelAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"authorization_model_id": "01H0H015178Y2V4CX10C2KGHF4"}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.WriteAuthorizationModel(context.Background(), apiTestStoreId). + Body(openfga.WriteAuthorizationModelRequest{ + SchemaVersion: "1.1", + TypeDefinitions: []openfga.TypeDefinition{ + { + Type: "user", + }, + { + Type: "document", + Relations: &map[string]openfga.Userset{ + "viewer": {}, + }, + }, + }, + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} + +func TestReadChangesAPIMethodHeaderHandling(t *testing.T) { + t.Run("ReadChangesAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"changes": [], "continuation_token": ""}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.ReadChanges(context.Background(), apiTestStoreId). + Type_("document"). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} + +func TestAssertionsAPIMethodHeaderHandling(t *testing.T) { + t.Run("ReadAssertionsAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{"assertions": []}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, _, err := client.OpenFgaApi.ReadAssertions(context.Background(), apiTestStoreId, apiTestStoreId). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) + + t.Run("WriteAssertionsAPIWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createAPITestServer(t, &capturedHeaders, `{}`) + defer server.Close() + + client := createAPITestClient(t, server.URL, map[string]string{ + apiDefaultHeaderName: apiDefaultHeaderValue, + }) + + _, err := client.OpenFgaApi.WriteAssertions(context.Background(), apiTestStoreId, apiTestStoreId). + Body(openfga.WriteAssertionsRequest{ + Assertions: []openfga.Assertion{ + { + TupleKey: openfga.AssertionTupleKey{ + User: apiTestUser, + Relation: apiTestRelation, + Object: apiTestObject, + }, + Expectation: true, + }, + }, + }). + Options(openfga.RequestOptions{ + Headers: map[string]string{ + apiCustomHeaderName: apiCustomHeaderValue, + apiDefaultHeaderName: apiOverriddenValue, + }, + }). + Execute() + + if err != nil { + t.Fatalf(apiRequestFailedMsg, err) + } + + if capturedHeaders[apiCustomHeaderName] != apiCustomHeaderValue { + t.Errorf(apiExpectedCustomMsg, capturedHeaders[apiCustomHeaderName]) + } + + if capturedHeaders[apiDefaultHeaderName] != apiOverriddenValue { + t.Errorf(apiExpectedOverriddenMsg, capturedHeaders[apiDefaultHeaderName]) + } + }) +} diff --git a/api_open_fga.go b/api_open_fga.go index eef1df1..309b182 100644 --- a/api_open_fga.go +++ b/api_open_fga.go @@ -32,6 +32,10 @@ var ( _ context.Context ) +type RequestOptions struct { + Headers map[string]string `json:"headers,omitempty"` +} + type OpenFgaApi interface { /* @@ -771,7 +775,10 @@ type OpenFgaApi interface { * Write Add or delete tuples from the store * The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. - The API is not idempotent: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. + The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. + To allow writes when an identical tuple already exists in the database, set `"on_duplicate": "ignore"` on the `writes` object. + To allow deletes when a tuple was already removed from the database, set `"on_missing": "ignore"` on the `deletes` object. + If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. ## Example @@ -786,7 +793,8 @@ type OpenFgaApi interface { "relation": "writer", "object": "document:2021-budget" } - ] + ], + "on_duplicate": "ignore" }, "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" } @@ -802,7 +810,8 @@ type OpenFgaApi interface { "relation": "reader", "object": "document:2021-budget" } - ] + ], + "on_missing": "ignore" } } ``` @@ -900,6 +909,7 @@ type ApiBatchCheckRequest struct { ApiService OpenFgaApi storeId string body *BatchCheckRequest + options RequestOptions } func (r ApiBatchCheckRequest) Body(body BatchCheckRequest) ApiBatchCheckRequest { @@ -907,6 +917,11 @@ func (r ApiBatchCheckRequest) Body(body BatchCheckRequest) ApiBatchCheckRequest return r } +func (r ApiBatchCheckRequest) Options(options RequestOptions) ApiBatchCheckRequest { + r.options = options + return r +} + func (r ApiBatchCheckRequest) Execute() (BatchCheckResponse, *http.Response, error) { return r.ApiService.BatchCheckExecute(r) } @@ -1029,6 +1044,11 @@ func (a *OpenFgaApiService) BatchCheckExecute(r ApiBatchCheckRequest) (BatchChec // body params requestBody = r.body + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -1136,6 +1156,7 @@ type ApiCheckRequest struct { ApiService OpenFgaApi storeId string body *CheckRequest + options RequestOptions } func (r ApiCheckRequest) Body(body CheckRequest) ApiCheckRequest { @@ -1143,6 +1164,11 @@ func (r ApiCheckRequest) Body(body CheckRequest) ApiCheckRequest { return r } +func (r ApiCheckRequest) Options(options RequestOptions) ApiCheckRequest { + r.options = options + return r +} + func (r ApiCheckRequest) Execute() (CheckResponse, *http.Response, error) { return r.ApiService.CheckExecute(r) } @@ -1349,6 +1375,11 @@ func (a *OpenFgaApiService) CheckExecute(r ApiCheckRequest) (CheckResponse, *htt // body params requestBody = r.body + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -1455,6 +1486,7 @@ type ApiCreateStoreRequest struct { ctx context.Context ApiService OpenFgaApi body *CreateStoreRequest + options RequestOptions } func (r ApiCreateStoreRequest) Body(body CreateStoreRequest) ApiCreateStoreRequest { @@ -1462,6 +1494,11 @@ func (r ApiCreateStoreRequest) Body(body CreateStoreRequest) ApiCreateStoreReque return r } +func (r ApiCreateStoreRequest) Options(options RequestOptions) ApiCreateStoreRequest { + r.options = options + return r +} + func (r ApiCreateStoreRequest) Execute() (CreateStoreResponse, *http.Response, error) { return r.ApiService.CreateStoreExecute(r) } @@ -1522,6 +1559,11 @@ func (a *OpenFgaApiService) CreateStoreExecute(r ApiCreateStoreRequest) (CreateS // body params requestBody = r.body + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -1627,6 +1669,12 @@ type ApiDeleteStoreRequest struct { ctx context.Context ApiService OpenFgaApi storeId string + options RequestOptions +} + +func (r ApiDeleteStoreRequest) Options(options RequestOptions) ApiDeleteStoreRequest { + r.options = options + return r } func (r ApiDeleteStoreRequest) Execute() (*http.Response, error) { @@ -1689,6 +1737,11 @@ func (a *OpenFgaApiService) DeleteStoreExecute(r ApiDeleteStoreRequest) (*http.R localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -1787,6 +1840,7 @@ type ApiExpandRequest struct { ApiService OpenFgaApi storeId string body *ExpandRequest + options RequestOptions } func (r ApiExpandRequest) Body(body ExpandRequest) ApiExpandRequest { @@ -1794,6 +1848,11 @@ func (r ApiExpandRequest) Body(body ExpandRequest) ApiExpandRequest { return r } +func (r ApiExpandRequest) Options(options RequestOptions) ApiExpandRequest { + r.options = options + return r +} + func (r ApiExpandRequest) Execute() (ExpandResponse, *http.Response, error) { return r.ApiService.ExpandExecute(r) } @@ -2038,6 +2097,11 @@ func (a *OpenFgaApiService) ExpandExecute(r ApiExpandRequest) (ExpandResponse, * // body params requestBody = r.body + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -2144,6 +2208,12 @@ type ApiGetStoreRequest struct { ctx context.Context ApiService OpenFgaApi storeId string + options RequestOptions +} + +func (r ApiGetStoreRequest) Options(options RequestOptions) ApiGetStoreRequest { + r.options = options + return r } func (r ApiGetStoreRequest) Execute() (GetStoreResponse, *http.Response, error) { @@ -2208,6 +2278,11 @@ func (a *OpenFgaApiService) GetStoreExecute(r ApiGetStoreRequest) (GetStoreRespo localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -2315,6 +2390,7 @@ type ApiListObjectsRequest struct { ApiService OpenFgaApi storeId string body *ListObjectsRequest + options RequestOptions } func (r ApiListObjectsRequest) Body(body ListObjectsRequest) ApiListObjectsRequest { @@ -2322,6 +2398,11 @@ func (r ApiListObjectsRequest) Body(body ListObjectsRequest) ApiListObjectsReque return r } +func (r ApiListObjectsRequest) Options(options RequestOptions) ApiListObjectsRequest { + r.options = options + return r +} + func (r ApiListObjectsRequest) Execute() (ListObjectsResponse, *http.Response, error) { return r.ApiService.ListObjectsExecute(r) } @@ -2398,6 +2479,11 @@ func (a *OpenFgaApiService) ListObjectsExecute(r ApiListObjectsRequest) (ListObj // body params requestBody = r.body + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -2506,6 +2592,7 @@ type ApiListStoresRequest struct { pageSize *int32 continuationToken *string name *string + options RequestOptions } func (r ApiListStoresRequest) PageSize(pageSize int32) ApiListStoresRequest { @@ -2521,6 +2608,11 @@ func (r ApiListStoresRequest) Name(name string) ApiListStoresRequest { return r } +func (r ApiListStoresRequest) Options(options RequestOptions) ApiListStoresRequest { + r.options = options + return r +} + func (r ApiListStoresRequest) Execute() (ListStoresResponse, *http.Response, error) { return r.ApiService.ListStoresExecute(r) } @@ -2588,6 +2680,11 @@ func (a *OpenFgaApiService) ListStoresExecute(r ApiListStoresRequest) (ListStore localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -2694,6 +2791,7 @@ type ApiListUsersRequest struct { ApiService OpenFgaApi storeId string body *ListUsersRequest + options RequestOptions } func (r ApiListUsersRequest) Body(body ListUsersRequest) ApiListUsersRequest { @@ -2701,6 +2799,11 @@ func (r ApiListUsersRequest) Body(body ListUsersRequest) ApiListUsersRequest { return r } +func (r ApiListUsersRequest) Options(options RequestOptions) ApiListUsersRequest { + r.options = options + return r +} + func (r ApiListUsersRequest) Execute() (ListUsersResponse, *http.Response, error) { return r.ApiService.ListUsersExecute(r) } @@ -2778,6 +2881,11 @@ func (a *OpenFgaApiService) ListUsersExecute(r ApiListUsersRequest) (ListUsersRe // body params requestBody = r.body + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -2885,6 +2993,7 @@ type ApiReadRequest struct { ApiService OpenFgaApi storeId string body *ReadRequest + options RequestOptions } func (r ApiReadRequest) Body(body ReadRequest) ApiReadRequest { @@ -2892,6 +3001,11 @@ func (r ApiReadRequest) Body(body ReadRequest) ApiReadRequest { return r } +func (r ApiReadRequest) Options(options RequestOptions) ApiReadRequest { + r.options = options + return r +} + func (r ApiReadRequest) Execute() (ReadResponse, *http.Response, error) { return r.ApiService.ReadExecute(r) } @@ -3070,6 +3184,11 @@ func (a *OpenFgaApiService) ReadExecute(r ApiReadRequest) (ReadResponse, *http.R // body params requestBody = r.body + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -3177,6 +3296,12 @@ type ApiReadAssertionsRequest struct { ApiService OpenFgaApi storeId string authorizationModelId string + options RequestOptions +} + +func (r ApiReadAssertionsRequest) Options(options RequestOptions) ApiReadAssertionsRequest { + r.options = options + return r } func (r ApiReadAssertionsRequest) Execute() (ReadAssertionsResponse, *http.Response, error) { @@ -3248,6 +3373,11 @@ func (a *OpenFgaApiService) ReadAssertionsExecute(r ApiReadAssertionsRequest) (R localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -3355,6 +3485,12 @@ type ApiReadAuthorizationModelRequest struct { ApiService OpenFgaApi storeId string id string + options RequestOptions +} + +func (r ApiReadAuthorizationModelRequest) Options(options RequestOptions) ApiReadAuthorizationModelRequest { + r.options = options + return r } func (r ApiReadAuthorizationModelRequest) Execute() (ReadAuthorizationModelResponse, *http.Response, error) { @@ -3469,6 +3605,11 @@ func (a *OpenFgaApiService) ReadAuthorizationModelExecute(r ApiReadAuthorization localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -3577,6 +3718,7 @@ type ApiReadAuthorizationModelsRequest struct { storeId string pageSize *int32 continuationToken *string + options RequestOptions } func (r ApiReadAuthorizationModelsRequest) PageSize(pageSize int32) ApiReadAuthorizationModelsRequest { @@ -3588,6 +3730,11 @@ func (r ApiReadAuthorizationModelsRequest) ContinuationToken(continuationToken s return r } +func (r ApiReadAuthorizationModelsRequest) Options(options RequestOptions) ApiReadAuthorizationModelsRequest { + r.options = options + return r +} + func (r ApiReadAuthorizationModelsRequest) Execute() (ReadAuthorizationModelsResponse, *http.Response, error) { return r.ApiService.ReadAuthorizationModelsExecute(r) } @@ -3697,6 +3844,11 @@ func (a *OpenFgaApiService) ReadAuthorizationModelsExecute(r ApiReadAuthorizatio localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -3807,6 +3959,7 @@ type ApiReadChangesRequest struct { pageSize *int32 continuationToken *string startTime *time.Time + options RequestOptions } func (r ApiReadChangesRequest) Type_(type_ string) ApiReadChangesRequest { @@ -3826,6 +3979,11 @@ func (r ApiReadChangesRequest) StartTime(startTime time.Time) ApiReadChangesRequ return r } +func (r ApiReadChangesRequest) Options(options RequestOptions) ApiReadChangesRequest { + r.options = options + return r +} + func (r ApiReadChangesRequest) Execute() (ReadChangesResponse, *http.Response, error) { return r.ApiService.ReadChangesExecute(r) } @@ -3905,6 +4063,11 @@ func (a *OpenFgaApiService) ReadChangesExecute(r ApiReadChangesRequest) (ReadCha localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept } + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -4012,6 +4175,7 @@ type ApiWriteRequest struct { ApiService OpenFgaApi storeId string body *WriteRequest + options RequestOptions } func (r ApiWriteRequest) Body(body WriteRequest) ApiWriteRequest { @@ -4019,6 +4183,11 @@ func (r ApiWriteRequest) Body(body WriteRequest) ApiWriteRequest { return r } +func (r ApiWriteRequest) Options(options RequestOptions) ApiWriteRequest { + r.options = options + return r +} + func (r ApiWriteRequest) Execute() (map[string]interface{}, *http.Response, error) { return r.ApiService.WriteExecute(r) } @@ -4028,7 +4197,10 @@ func (r ApiWriteRequest) Execute() (map[string]interface{}, *http.Response, erro - The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. -The API is not idempotent: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. +The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. +To allow writes when an identical tuple already exists in the database, set `"on_duplicate": "ignore"` on the `writes` object. +To allow deletes when a tuple was already removed from the database, set `"on_missing": "ignore"` on the `deletes` object. +If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. ## Example @@ -4044,7 +4216,8 @@ To add `user:anne` as a `writer` for `document:2021-budget`, call write API with "relation": "writer", "object": "document:2021-budget" } - ] + ], + "on_duplicate": "ignore" }, "authorization_model_id": "01G50QVV17PECNVAHX1GG4Y5NC" } @@ -4062,7 +4235,8 @@ To remove `user:bob` as a `reader` for `document:2021-budget`, call write API wi "relation": "reader", "object": "document:2021-budget" } - ] + ], + "on_missing": "ignore" } } @@ -4128,6 +4302,11 @@ func (a *OpenFgaApiService) WriteExecute(r ApiWriteRequest) (map[string]interfac // body params requestBody = r.body + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -4236,6 +4415,7 @@ type ApiWriteAssertionsRequest struct { storeId string authorizationModelId string body *WriteAssertionsRequest + options RequestOptions } func (r ApiWriteAssertionsRequest) Body(body WriteAssertionsRequest) ApiWriteAssertionsRequest { @@ -4243,6 +4423,11 @@ func (r ApiWriteAssertionsRequest) Body(body WriteAssertionsRequest) ApiWriteAss return r } +func (r ApiWriteAssertionsRequest) Options(options RequestOptions) ApiWriteAssertionsRequest { + r.options = options + return r +} + func (r ApiWriteAssertionsRequest) Execute() (*http.Response, error) { return r.ApiService.WriteAssertionsExecute(r) } @@ -4315,6 +4500,11 @@ func (a *OpenFgaApiService) WriteAssertionsExecute(r ApiWriteAssertionsRequest) // body params requestBody = r.body + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) @@ -4413,6 +4603,7 @@ type ApiWriteAuthorizationModelRequest struct { ApiService OpenFgaApi storeId string body *WriteAuthorizationModelRequest + options RequestOptions } func (r ApiWriteAuthorizationModelRequest) Body(body WriteAuthorizationModelRequest) ApiWriteAuthorizationModelRequest { @@ -4420,6 +4611,11 @@ func (r ApiWriteAuthorizationModelRequest) Body(body WriteAuthorizationModelRequ return r } +func (r ApiWriteAuthorizationModelRequest) Options(options RequestOptions) ApiWriteAuthorizationModelRequest { + r.options = options + return r +} + func (r ApiWriteAuthorizationModelRequest) Execute() (WriteAuthorizationModelResponse, *http.Response, error) { return r.ApiService.WriteAuthorizationModelExecute(r) } @@ -4532,6 +4728,11 @@ func (a *OpenFgaApiService) WriteAuthorizationModelExecute(r ApiWriteAuthorizati // body params requestBody = r.body + // if any override headers were in the options, set them now + for header, val := range r.options.Headers { + localVarHeaderParams[header] = val + } + retryParams := a.client.cfg.RetryParams for i := 0; i < retryParams.MaxRetry+1; i++ { req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) diff --git a/api_open_fga_test.go b/api_open_fga_test.go index fcff26d..b83b815 100644 --- a/api_open_fga_test.go +++ b/api_open_fga_test.go @@ -794,6 +794,298 @@ func TestOpenFgaApi(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.OpenFgaApi.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("OpenFga%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.OpenFgaApi.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("OpenFga%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.OpenFgaApi.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("OpenFga%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.OpenFgaApi.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("OpenFga%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.OpenFgaApi.Write(context.Background(), "01GXSB9YR785C4FYS3C0RTG7B2").Body(requestBody).Execute() + if err != nil { + t.Fatalf("%v", err) + } + + if response.StatusCode != test.ResponseStatus { + t.Fatalf("OpenFga%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/client/client.go b/client/client.go index 00095b3..bafa2ba 100644 --- a/client/client.go +++ b/client/client.go @@ -118,17 +118,14 @@ func NewSdkClient(cfg *ClientConfiguration) (*OpenFgaClient, error) { }, nil } -type ClientRequestOptions struct { - MaxRetry *int `json:"max_retry,omitempty"` - MinWaitInMs *int `json:"min_wait_in_ms,omitempty"` -} +type RequestOptions = fgaSdk.RequestOptions type AuthorizationModelIdOptions struct { AuthorizationModelId *string `json:"authorization_model_id,omitempty"` } type ClientRequestOptionsWithAuthZModelId struct { - ClientRequestOptions + RequestOptions AuthorizationModelIdOptions } @@ -156,6 +153,8 @@ type ClientBatchCheckRequest struct { // BatchCheckOptions represents options for server-side batch check operations type BatchCheckOptions struct { + RequestOptions + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` MaxParallelRequests *int32 `json:"max_parallel_requests,omitempty"` @@ -578,6 +577,8 @@ type SdkClientListStoresRequestInterface interface { } type ClientListStoresOptions struct { + RequestOptions + PageSize *int32 `json:"page_size,omitempty"` ContinuationToken *string `json:"continuation_token,omitempty"` Name *string `json:"name,omitempty"` @@ -606,6 +607,7 @@ func (client *OpenFgaClient) ListStoresExecute(request SdkClientListStoresReques req := client.OpenFgaApi.ListStores(request.GetContext()) options := request.GetOptions() if options != nil { + req = req.Options(options.RequestOptions) if options.PageSize != nil { req = req.PageSize(*options.PageSize) } @@ -616,6 +618,7 @@ func (client *OpenFgaClient) ListStoresExecute(request SdkClientListStoresReques req = req.Name(*options.Name) } } + data, _, err := req.Execute() if err != nil { return nil, err @@ -654,6 +657,7 @@ type ClientCreateStoreRequest struct { } type ClientCreateStoreOptions struct { + RequestOptions } type ClientCreateStoreResponse = fgaSdk.CreateStoreResponse @@ -685,9 +689,20 @@ func (request *SdkClientCreateStoreRequest) GetBody() *ClientCreateStoreRequest } func (client *OpenFgaClient) CreateStoreExecute(request SdkClientCreateStoreRequestInterface) (*ClientCreateStoreResponse, error) { - data, _, err := client.OpenFgaApi.CreateStore(request.GetContext()).Body(fgaSdk.CreateStoreRequest{ + requestOptions := RequestOptions{} + if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions + } + + requestBody := fgaSdk.CreateStoreRequest{ Name: request.GetBody().Name, - }).Execute() + } + + data, _, err := client.OpenFgaApi. + CreateStore(request.GetContext()). + Body(requestBody). + Options(requestOptions). + Execute() if err != nil { return nil, err } @@ -719,6 +734,8 @@ type SdkClientGetStoreRequestInterface interface { } type ClientGetStoreOptions struct { + RequestOptions + StoreId *string `json:"store_id,omitempty"` } @@ -753,7 +770,16 @@ func (client *OpenFgaClient) GetStoreExecute(request SdkClientGetStoreRequestInt if err != nil { return nil, err } - data, _, err := client.OpenFgaApi.GetStore(request.GetContext(), *storeId).Execute() + + requestOptions := RequestOptions{} + if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions + } + + data, _, err := client.OpenFgaApi. + GetStore(request.GetContext(), *storeId). + Options(requestOptions). + Execute() if err != nil { return nil, err } @@ -785,6 +811,8 @@ type SdkClientDeleteStoreRequestInterface interface { } type ClientDeleteStoreOptions struct { + RequestOptions + StoreId *string `json:"store_id,omitempty"` } @@ -819,7 +847,16 @@ func (client *OpenFgaClient) DeleteStoreExecute(request SdkClientDeleteStoreRequ if err != nil { return nil, err } - _, err = client.OpenFgaApi.DeleteStore(request.GetContext(), *storeId).Execute() + + requestOptions := RequestOptions{} + if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions + } + + _, err = client.OpenFgaApi. + DeleteStore(request.GetContext(), *storeId). + Options(requestOptions). + Execute() if err != nil { return nil, err } @@ -853,6 +890,8 @@ type SdkClientReadAuthorizationModelsRequestInterface interface { } type ClientReadAuthorizationModelsOptions struct { + RequestOptions + PageSize *int32 `json:"page_size,omitempty"` ContinuationToken *string `json:"continuation_token,omitempty"` StoreId *string `json:"store_id,omitempty"` @@ -896,7 +935,15 @@ func (client *OpenFgaClient) ReadAuthorizationModelsExecute(request SdkClientRea return nil, err } - req := client.OpenFgaApi.ReadAuthorizationModels(request.GetContext(), *storeId) + requestOptions := RequestOptions{} + if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions + } + + req := client.OpenFgaApi. + ReadAuthorizationModels(request.GetContext(), *storeId). + Options(requestOptions) + pageSize := getPageSizeFromRequest(&pagingOpts) if pageSize != nil { req = req.PageSize(*pageSize) @@ -942,6 +989,8 @@ type SdkClientWriteAuthorizationModelRequestInterface interface { type ClientWriteAuthorizationModelRequest = fgaSdk.WriteAuthorizationModelRequest type ClientWriteAuthorizationModelOptions struct { + RequestOptions + StoreId *string `json:"store_id,omitempty"` } @@ -985,7 +1034,17 @@ func (client *OpenFgaClient) WriteAuthorizationModelExecute(request SdkClientWri if err != nil { return nil, err } - data, _, err := client.OpenFgaApi.WriteAuthorizationModel(request.GetContext(), *storeId).Body(*request.GetBody()).Execute() + + requestOptions := RequestOptions{} + if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions + } + + data, _, err := client.OpenFgaApi. + WriteAuthorizationModel(request.GetContext(), *storeId). + Body(*request.GetBody()). + Options(requestOptions). + Execute() if err != nil { return nil, err } @@ -1024,6 +1083,8 @@ type ClientReadAuthorizationModelRequest struct { } type ClientReadAuthorizationModelOptions struct { + RequestOptions + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` } @@ -1082,8 +1143,16 @@ func (client *OpenFgaClient) ReadAuthorizationModelExecute(request SdkClientRead if err != nil { return nil, err } - data, _, err := client.OpenFgaApi.ReadAuthorizationModel(request.GetContext(), *storeId, *authorizationModelId).Execute() + requestOptions := RequestOptions{} + if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions + } + + data, _, err := client.OpenFgaApi. + ReadAuthorizationModel(request.GetContext(), *storeId, *authorizationModelId). + Options(requestOptions). + Execute() if err != nil { return nil, err } @@ -1115,6 +1184,8 @@ type SdkClientReadLatestAuthorizationModelRequestInterface interface { } type ClientReadLatestAuthorizationModelOptions struct { + RequestOptions + StoreId *string `json:"store_id,omitempty"` } @@ -1155,6 +1226,7 @@ func (client *OpenFgaClient) ReadLatestAuthorizationModelExecute(request SdkClie } if request.GetOptions() != nil { opts.StoreId = request.GetOptions().StoreId + opts.RequestOptions = request.GetOptions().RequestOptions } req := client.ReadAuthorizationModels(request.GetContext()).Options(opts) @@ -1203,6 +1275,8 @@ type ClientReadChangesRequest struct { } type ClientReadChangesOptions struct { + RequestOptions + PageSize *int32 `json:"page_size,omitempty"` ContinuationToken *string `json:"continuation_token,omitempty"` StoreId *string `json:"store_id"` @@ -1252,7 +1326,9 @@ func (request *SdkClientReadChangesRequest) GetOptions() *ClientReadChangesOptio func (client *OpenFgaClient) ReadChangesExecute(request SdkClientReadChangesRequestInterface) (*ClientReadChangesResponse, error) { pagingOpts := ClientPaginationOptions{} + requestOptions := RequestOptions{} if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions pagingOpts.PageSize = request.GetOptions().PageSize pagingOpts.ContinuationToken = request.GetOptions().ContinuationToken } @@ -1262,7 +1338,9 @@ func (client *OpenFgaClient) ReadChangesExecute(request SdkClientReadChangesRequ return nil, err } - req := client.OpenFgaApi.ReadChanges(request.GetContext(), *storeId) + req := client.OpenFgaApi. + ReadChanges(request.GetContext(), *storeId). + Options(requestOptions) pageSize := getPageSizeFromRequest(&pagingOpts) if pageSize != nil { req = req.PageSize(*pageSize) @@ -1313,6 +1391,8 @@ type ClientReadRequest struct { } type ClientReadOptions struct { + RequestOptions + PageSize *int32 `json:"page_size,omitempty"` ContinuationToken *string `json:"continuation_token,omitempty"` StoreId *string `json:"store_id,omitempty"` @@ -1363,8 +1443,10 @@ func (request *SdkClientReadRequest) GetOptions() *ClientReadOptions { func (client *OpenFgaClient) ReadExecute(request SdkClientReadRequestInterface) (*ClientReadResponse, error) { pagingOpts := ClientPaginationOptions{} + requestOptions := RequestOptions{} var consistency *fgaSdk.ConsistencyPreference if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions pagingOpts.PageSize = request.GetOptions().PageSize pagingOpts.ContinuationToken = request.GetOptions().ContinuationToken consistency = request.GetOptions().Consistency @@ -1386,7 +1468,12 @@ func (client *OpenFgaClient) ReadExecute(request SdkClientReadRequestInterface) if err != nil { return nil, err } - data, _, err := client.OpenFgaApi.Read(request.GetContext(), *storeId).Body(body).Execute() + + data, _, err := client.OpenFgaApi. + Read(request.GetContext(), *storeId). + Body(body). + Options(requestOptions). + Execute() if err != nil { return nil, err } @@ -1428,10 +1515,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 + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` Transaction *TransactionOptions `json:"transaction_options,omitempty"` + Conflict ClientWriteConflictOptions } type ClientWriteStatus string @@ -1546,6 +1685,10 @@ func (client *OpenFgaClient) WriteExecute(request SdkClientWriteRequestInterface Writes: []ClientWriteRequestWriteResponse{}, Deletes: []ClientWriteRequestDeleteResponse{}, } + requestOptions := RequestOptions{} + if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions + } authorizationModelId, err := client.getAuthorizationModelId(request.GetAuthorizationModelIdOverride()) if err != nil { @@ -1565,6 +1708,9 @@ func (client *OpenFgaClient) WriteExecute(request SdkClientWriteRequestInterface } 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]) } @@ -1572,12 +1718,20 @@ func (client *OpenFgaClient) WriteExecute(request SdkClientWriteRequestInterface } 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]) } writeRequest.Deletes = &deletes } - _, httpResponse, err := client.OpenFgaApi.Write(request.GetContext(), *storeId).Body(writeRequest).Execute() + + _, httpResponse, err := client.OpenFgaApi. + Write(request.GetContext(), *storeId). + Body(writeRequest). + Options(requestOptions). + Execute() clientWriteStatus := SUCCESS if err != nil { @@ -1649,8 +1803,10 @@ func (client *OpenFgaClient) WriteExecute(request SdkClientWriteRequestInterface Writes: writeBody, }, options: &ClientWriteOptions{ + RequestOptions: options.RequestOptions, AuthorizationModelId: authorizationModelId, StoreId: request.GetStoreIdOverride(), + Conflict: options.Conflict, }, }) @@ -1693,8 +1849,10 @@ func (client *OpenFgaClient) WriteExecute(request SdkClientWriteRequestInterface Deletes: deleteBody, }, options: &ClientWriteOptions{ + RequestOptions: options.RequestOptions, AuthorizationModelId: authorizationModelId, StoreId: request.GetStoreIdOverride(), + Conflict: options.Conflict, }, }) @@ -1886,6 +2044,8 @@ type ClientCheckRequest struct { } type ClientCheckOptions struct { + RequestOptions + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` Consistency *fgaSdk.ConsistencyPreference `json:"consistency,omitempty"` @@ -1944,10 +2104,11 @@ func (request *SdkClientCheckRequest) GetOptions() *ClientCheckOptions { } func (client *OpenFgaClient) CheckExecute(request SdkClientCheckRequestInterface) (*ClientCheckResponse, error) { + if request.GetBody() == nil { + return nil, FgaRequiredParamError{param: "body"} + } + var contextualTuples []ClientContextualTupleKey - if request.GetBody() == nil { - return nil, FgaRequiredParamError{param: "body"} - } if request.GetBody().ContextualTuples != nil { for index := 0; index < len(request.GetBody().ContextualTuples); index++ { contextualTuples = append(contextualTuples, (request.GetBody().ContextualTuples)[index]) @@ -1972,11 +2133,17 @@ func (client *OpenFgaClient) CheckExecute(request SdkClientCheckRequestInterface AuthorizationModelId: authorizationModelId, } + requestOptions := RequestOptions{} if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions requestBody.Consistency = request.GetOptions().Consistency } - data, httpResponse, err := client.OpenFgaApi.Check(request.GetContext(), *storeId).Body(requestBody).Execute() + data, httpResponse, err := client.OpenFgaApi. + Check(request.GetContext(), *storeId). + Body(requestBody). + Options(requestOptions). + Execute() return &ClientCheckResponse{CheckResponse: data, HttpResponse: httpResponse}, err } @@ -2005,6 +2172,8 @@ type SdkClientBatchCheckClientRequestInterface interface { type ClientBatchCheckClientBody = []ClientCheckRequest type ClientBatchCheckClientOptions struct { + RequestOptions + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` MaxParallelRequests *int32 `json:"max_parallel_requests,omitempty"` @@ -2068,12 +2237,15 @@ func (request *SdkClientBatchCheckClientRequest) GetOptions() *ClientBatchCheckC func (client *OpenFgaClient) ClientBatchCheckExecute(request SdkClientBatchCheckClientRequestInterface) (*ClientBatchCheckClientResponse, error) { group, ctx := errgroup.WithContext(request.GetContext()) - var maxParallelReqs int - if request.GetOptions() == nil || request.GetOptions().MaxParallelRequests == nil { - maxParallelReqs = int(DEFAULT_MAX_METHOD_PARALLEL_REQS) - } else { - maxParallelReqs = int(*request.GetOptions().MaxParallelRequests) + requestOptions := RequestOptions{} + maxParallelReqs := int(DEFAULT_MAX_METHOD_PARALLEL_REQS) + if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions + if request.GetOptions().MaxParallelRequests != nil { + maxParallelReqs = int(*request.GetOptions().MaxParallelRequests) + } } + group.SetLimit(maxParallelReqs) var numOfChecks = len(*request.GetBody()) response := make(ClientBatchCheckClientResponse, numOfChecks) @@ -2088,6 +2260,8 @@ func (client *OpenFgaClient) ClientBatchCheckExecute(request SdkClientBatchCheck } checkOptions := &ClientCheckOptions{ + RequestOptions: requestOptions, + AuthorizationModelId: authorizationModelId, StoreId: storeId, } @@ -2264,9 +2438,10 @@ func (client *OpenFgaClient) singleBatchCheck(ctx _context.Context, body fgaSdk. return nil, err } - req := client.OpenFgaApi.BatchCheck(ctx, *storeId) - req = req.Body(body) - + req := client.OpenFgaApi. + BatchCheck(ctx, *storeId). + Body(body). + Options(options.RequestOptions) response, _, err := req.Execute() if err != nil { return nil, err @@ -2381,6 +2556,8 @@ type ClientExpandRequest struct { } type ClientExpandOptions struct { + RequestOptions + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` Consistency *fgaSdk.ConsistencyPreference `json:"consistency,omitempty"` @@ -2461,11 +2638,17 @@ func (client *OpenFgaClient) ExpandExecute(request SdkClientExpandRequestInterfa AuthorizationModelId: authorizationModelId, } + requestOptions := RequestOptions{} if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions body.Consistency = request.GetOptions().Consistency } - data, _, err := client.OpenFgaApi.Expand(request.GetContext(), *storeId).Body(body).Execute() + data, _, err := client.OpenFgaApi. + Expand(request.GetContext(), *storeId). + Body(body). + Options(requestOptions). + Execute() if err != nil { return nil, err } @@ -2502,6 +2685,8 @@ type ClientListObjectsRequest struct { } type ClientListObjectsOptions struct { + RequestOptions + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` Consistency *fgaSdk.ConsistencyPreference `json:"consistency,omitempty"` @@ -2579,10 +2764,16 @@ func (client *OpenFgaClient) ListObjectsExecute(request SdkClientListObjectsRequ Context: request.GetBody().Context, AuthorizationModelId: authorizationModelId, } + requestOptions := RequestOptions{} if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions body.Consistency = request.GetOptions().Consistency } - data, _, err := client.OpenFgaApi.ListObjects(request.GetContext(), *storeId).Body(body).Execute() + data, _, err := client.OpenFgaApi. + ListObjects(request.GetContext(), *storeId). + Body(body). + Options(requestOptions). + Execute() if err != nil { return nil, err } @@ -2620,6 +2811,8 @@ type ClientListRelationsRequest struct { } type ClientListRelationsOptions struct { + RequestOptions + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` MaxParallelRequests *int32 `json:"max_parallel_requests,omitempty"` StoreId *string `json:"store_id,omitempty"` @@ -2712,6 +2905,7 @@ func (client *OpenFgaClient) ListRelationsExecute(request SdkClientListRelations StoreId: storeId, } if request.GetOptions() != nil { + options.RequestOptions = request.GetOptions().RequestOptions options.Consistency = request.GetOptions().Consistency options.MaxParallelRequests = request.GetOptions().MaxParallelRequests } @@ -2768,6 +2962,8 @@ type ClientListUsersRequest struct { } type ClientListUsersOptions struct { + RequestOptions + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` Consistency *fgaSdk.ConsistencyPreference `json:"consistency,omitempty"` @@ -2846,11 +3042,17 @@ func (client *OpenFgaClient) ListUsersExecute(request SdkClientListUsersRequestI AuthorizationModelId: authorizationModelId, } + requestOptions := RequestOptions{} if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions body.Consistency = request.GetOptions().Consistency } - data, _, err := client.OpenFgaApi.ListUsers(request.GetContext(), *storeId).Body(body).Execute() + data, _, err := client.OpenFgaApi. + ListUsers(request.GetContext(), *storeId). + Body(body). + Options(requestOptions). + Execute() if err != nil { return nil, err } @@ -2876,6 +3078,8 @@ type SdkClientReadAssertionsRequestInterface interface { } type ClientReadAssertionsOptions struct { + RequestOptions + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` } @@ -2932,7 +3136,16 @@ func (client *OpenFgaClient) ReadAssertionsExecute(request SdkClientReadAssertio if err != nil { return nil, err } - data, _, err := client.OpenFgaApi.ReadAssertions(request.GetContext(), *storeId, *authorizationModelId).Execute() + + requestOptions := RequestOptions{} + if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions + } + + data, _, err := client.OpenFgaApi. + ReadAssertions(request.GetContext(), *storeId, *authorizationModelId). + Options(requestOptions). + Execute() if err != nil { return nil, err } @@ -2990,6 +3203,8 @@ func (clientAssertion ClientAssertion) ToAssertion() fgaSdk.Assertion { } type ClientWriteAssertionsOptions struct { + RequestOptions + AuthorizationModelId *string `json:"authorization_model_id,omitempty"` StoreId *string `json:"store_id,omitempty"` } @@ -3061,7 +3276,17 @@ func (client *OpenFgaClient) WriteAssertionsExecute(request SdkClientWriteAssert clientAssertion := (*request.GetBody())[index] writeAssertionsRequest.Assertions = append(writeAssertionsRequest.Assertions, clientAssertion.ToAssertion()) } - _, err = client.OpenFgaApi.WriteAssertions(request.GetContext(), *storeId, *authorizationModelId).Body(writeAssertionsRequest).Execute() + + requestOptions := RequestOptions{} + if request.GetOptions() != nil { + requestOptions = request.GetOptions().RequestOptions + } + + _, err = client.OpenFgaApi. + WriteAssertions(request.GetContext(), *storeId, *authorizationModelId). + Body(writeAssertionsRequest). + Options(requestOptions). + Execute() if err != nil { return nil, err diff --git a/client/client_headers_test.go b/client/client_headers_test.go new file mode 100644 index 0000000..8e9c506 --- /dev/null +++ b/client/client_headers_test.go @@ -0,0 +1,881 @@ +package client_test + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + fgaSdk "github.com/openfga/go-sdk" + fgaSdkClient "github.com/openfga/go-sdk/client" +) + +// Test helpers and setup + +// Constants to avoid duplication +const ( + defaultHeaderName = "Default-Header" + defaultHeaderValue = "default-value" + overriddenValue = "overridden-value" + customHeaderName = "X-Custom-Header" + customHeaderValue = "custom-value" + testUser = "user:anne" + testRelation = "viewer" + testObject = "document:roadmap" + testHeaderName = "Test-Header" + testHeaderValue = "test-value" + checkRequestFailedMsg = "Check request failed: %v" + expectedCustomHeaderMsg = "Expected X-Custom-Header to be 'custom-value', got '%s'" + expectedOverriddenHeaderMsg = "Expected Default-Header to be overridden to 'overridden-value', got '%s'" + expectedDefaultHeaderMsg = "Expected Default-Header to be 'default-value', got '%s'" +) + +func createTestServer(t *testing.T, capturedHeaders *map[string]string, responseBody string) *httptest.Server { + t.Helper() + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + *capturedHeaders = make(map[string]string) + for name, values := range r.Header { + if len(values) > 0 { + (*capturedHeaders)[name] = values[0] + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(responseBody)) + })) +} + +func createTestClient(t *testing.T, serverURL string, defaultHeaders map[string]string) *fgaSdkClient.OpenFgaClient { + t.Helper() + + config := fgaSdkClient.ClientConfiguration{ + ApiUrl: serverURL, + DefaultHeaders: defaultHeaders, + StoreId: "01H0H015178Y2V4CX10C2KGHF4", + HTTPClient: &http.Client{}, + } + + client, err := fgaSdkClient.NewSdkClient(&config) + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + + return client +} + +// Test RequestOptions +func TestRequestOptionsStructure(t *testing.T) { + t.Run("RequestOptionsEmbedding", func(t *testing.T) { + options := fgaSdkClient.ClientCheckOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + testHeaderName: testHeaderValue, + }, + }, + AuthorizationModelId: fgaSdk.PtrString("01H0H015178Y2V4CX10C2KGHF4"), + Consistency: nil, + } + + if options.Headers[testHeaderName] != testHeaderValue { + t.Errorf("Expected %s to be '%s', got '%s'", testHeaderName, testHeaderValue, options.Headers[testHeaderName]) + } + + if options.AuthorizationModelId == nil || *options.AuthorizationModelId != "01H0H015178Y2V4CX10C2KGHF4" { + t.Errorf("Expected AuthorizationModelId to be set correctly") + } + }) + + t.Run("RequestWithNilHeaders", func(t *testing.T) { + options := fgaSdkClient.ClientCheckOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: nil, + }, + } + + if options.Headers != nil { + t.Errorf("Expected Headers to be nil, got %v", options.Headers) + } + }) +} + +// Test the header handling for the methods + +func TestCheckMethodHeaderHandling(t *testing.T) { + t.Run("CheckWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, `{"allowed": true}`) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.Check(context.Background()). + Body(fgaSdkClient.ClientCheckRequest{ + User: testUser, + Relation: testRelation, + Object: testObject, + }). + Options(fgaSdkClient.ClientCheckOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) + + t.Run("CheckWithoutCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, `{"allowed": true}`) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.Check(context.Background()). + Body(fgaSdkClient.ClientCheckRequest{ + User: testUser, + Relation: testRelation, + Object: testObject, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[defaultHeaderName] != defaultHeaderValue { + t.Errorf(expectedDefaultHeaderMsg, capturedHeaders[defaultHeaderName]) + } + + if _, exists := capturedHeaders[customHeaderName]; exists { + t.Error("Did not expect X-Custom-Header to be present") + } + }) + + t.Run("CheckWithEmptyHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, `{"allowed": true}`) + defer server.Close() + + client := createTestClient(t, server.URL, nil) + + _, err := client.Check(context.Background()). + Body(fgaSdkClient.ClientCheckRequest{ + User: testUser, + Relation: testRelation, + Object: testObject, + }). + Options(fgaSdkClient.ClientCheckOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{}, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + // Only standard headers should be present + for header := range capturedHeaders { + if strings.HasPrefix(header, "X-") || header == defaultHeaderName { + t.Errorf("Unexpected custom header found: %s", header) + } + } + }) + + t.Run("CheckWithNilOptions", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, `{"allowed": true}`) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + request := client.Check(context.Background()). + Body(fgaSdkClient.ClientCheckRequest{ + User: testUser, + Relation: testRelation, + Object: testObject, + }) + + // Options not set + _, err := request.Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[defaultHeaderName] != defaultHeaderValue { + t.Errorf(expectedDefaultHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestWriteMethodHeaderHandling(t *testing.T) { + const writeResponse = `{}` + + t.Run("WriteWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, writeResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.Write(context.Background()). + Body(fgaSdkClient.ClientWriteRequest{ + Writes: []fgaSdkClient.ClientTupleKey{ + { + User: testUser, + Relation: testRelation, + Object: testObject, + }, + }, + }). + Options(fgaSdkClient.ClientWriteOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) + + t.Run("WriteWithoutCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, writeResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.Write(context.Background()). + Body(fgaSdkClient.ClientWriteRequest{ + Writes: []fgaSdkClient.ClientTupleKey{ + { + User: testUser, + Relation: testRelation, + Object: testObject, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[defaultHeaderName] != defaultHeaderValue { + t.Errorf(expectedDefaultHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestReadMethodHeaderHandling(t *testing.T) { + const readResponse = `{"tuples": []}` + + t.Run("ReadWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, readResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.Read(context.Background()). + Body(fgaSdkClient.ClientReadRequest{ + User: fgaSdk.PtrString(testUser), + Relation: fgaSdk.PtrString(testRelation), + Object: fgaSdk.PtrString(testObject), + }). + Options(fgaSdkClient.ClientReadOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestExpandMethodHeaderHandling(t *testing.T) { + const expandResponse = `{"tree": {"root": {"name": "document:roadmap#viewer"}}}` + + t.Run("ExpandWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, expandResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.Expand(context.Background()). + Body(fgaSdkClient.ClientExpandRequest{ + Relation: testRelation, + Object: testObject, + }). + Options(fgaSdkClient.ClientExpandOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestListObjectsMethodHeaderHandling(t *testing.T) { + const listObjectsResponse = `{"objects": ["document:roadmap"]}` + + t.Run("ListObjectsWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, listObjectsResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.ListObjects(context.Background()). + Body(fgaSdkClient.ClientListObjectsRequest{ + User: testUser, + Relation: testRelation, + Type: "document", + }). + Options(fgaSdkClient.ClientListObjectsOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestListUsersMethodHeaderHandling(t *testing.T) { + const listUsersResponse = `{"users": [{"object": {"type": "user", "id": "anne"}}]}` + + t.Run("ListUsersWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, listUsersResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.ListUsers(context.Background()). + Body(fgaSdkClient.ClientListUsersRequest{ + Object: fgaSdk.FgaObject{ + Type: "document", + Id: "roadmap", + }, + Relation: testRelation, + UserFilters: []fgaSdk.UserTypeFilter{ + {Type: "user"}, + }, + }). + Options(fgaSdkClient.ClientListUsersOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestBatchCheckMethodHeaderHandling(t *testing.T) { + const batchCheckResponse = `{"result":{"corr-id-123":{"allowed": true}}}` + + t.Run("BatchCheckWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, batchCheckResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + checks := fgaSdkClient.ClientBatchCheckRequest{ + Checks: []fgaSdkClient.ClientBatchCheckItem{{ + CorrelationId: "corr-id-123", + User: testUser, + Relation: testRelation, + Object: testObject, + }}, + } + + _, err := client.BatchCheck(context.Background()). + Body(checks). + Options(fgaSdkClient.BatchCheckOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestReadAuthorizationModelMethodHeaderHandling(t *testing.T) { + const authModelResponse = `{"authorization_model": {"id": "01H0H015178Y2V4CX10C2KGHF4", "schema_version": "1.1"}}` + + t.Run("ReadAuthorizationModelWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, authModelResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.ReadAuthorizationModel(context.Background()). + Options(fgaSdkClient.ClientReadAuthorizationModelOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + AuthorizationModelId: fgaSdk.PtrString("01H0H015178Y2V4CX10C2KGHF4"), + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestWriteAuthorizationModelMethodHeaderHandling(t *testing.T) { + const writeAuthModelResponse = `{"authorization_model_id": "01H0H015178Y2V4CX10C2KGHF4"}` + + t.Run("WriteAuthorizationModelWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, writeAuthModelResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.WriteAuthorizationModel(context.Background()). + Body(fgaSdkClient.ClientWriteAuthorizationModelRequest{ + SchemaVersion: "1.1", + TypeDefinitions: []fgaSdk.TypeDefinition{ + { + Type: "user", + }, + { + Type: "document", + Relations: &map[string]fgaSdk.Userset{ + "viewer": {}, + }, + }, + }, + }). + Options(fgaSdkClient.ClientWriteAuthorizationModelOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestStoreMethodHeaderHandling(t *testing.T) { + t.Run("ListStoresWithCustomHeaders", func(t *testing.T) { + const listStoresResponse = `{"stores": [{"id": "01H0H015178Y2V4CX10C2KGHF4", "name": "test"}]}` + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, listStoresResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.ListStores(context.Background()). + Options(fgaSdkClient.ClientListStoresOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) + + t.Run("CreateStoreWithCustomHeaders", func(t *testing.T) { + const createStoreResponse = `{"id": "01H0H015178Y2V4CX10C2KGHF4", "name": "test"}` + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, createStoreResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.CreateStore(context.Background()). + Body(fgaSdkClient.ClientCreateStoreRequest{ + Name: "test", + }). + Options(fgaSdkClient.ClientCreateStoreOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) + + t.Run("GetStoreWithCustomHeaders", func(t *testing.T) { + const getStoreResponse = `{"id": "01H0H015178Y2V4CX10C2KGHF4", "name": "test"}` + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, getStoreResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.GetStore(context.Background()). + Options(fgaSdkClient.ClientGetStoreOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) + + t.Run("DeleteStoreWithCustomHeaders", func(t *testing.T) { + const deleteStoreResponse = `{}` + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, deleteStoreResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.DeleteStore(context.Background()). + Options(fgaSdkClient.ClientDeleteStoreOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestReadChangesMethodHeaderHandling(t *testing.T) { + const readChangesResponse = `{"changes": [], "continuation_token": ""}` + + t.Run("ReadChangesWithCustomHeaders", func(t *testing.T) { + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, readChangesResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.ReadChanges(context.Background()). + Body(fgaSdkClient.ClientReadChangesRequest{ + Type: "document", + }). + Options(fgaSdkClient.ClientReadChangesOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} + +func TestAssertionsMethodHeaderHandling(t *testing.T) { + t.Run("ReadAssertionsWithCustomHeaders", func(t *testing.T) { + const readAssertionsResponse = `{"assertions": []}` + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, readAssertionsResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + _, err := client.ReadAssertions(context.Background()). + Options(fgaSdkClient.ClientReadAssertionsOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + AuthorizationModelId: fgaSdk.PtrString("01H0H015178Y2V4CX10C2KGHF4"), + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) + + t.Run("WriteAssertionsWithCustomHeaders", func(t *testing.T) { + const writeAssertionsResponse = `{}` + var capturedHeaders map[string]string + server := createTestServer(t, &capturedHeaders, writeAssertionsResponse) + defer server.Close() + + client := createTestClient(t, server.URL, map[string]string{ + defaultHeaderName: defaultHeaderValue, + }) + + assertions := []fgaSdkClient.ClientAssertion{ + { + User: testUser, + Relation: testRelation, + Object: testObject, + Expectation: true, + }, + } + + _, err := client.WriteAssertions(context.Background()). + Body(assertions). + Options(fgaSdkClient.ClientWriteAssertionsOptions{ + RequestOptions: fgaSdkClient.RequestOptions{ + Headers: map[string]string{ + customHeaderName: customHeaderValue, + defaultHeaderName: overriddenValue, + }, + }, + AuthorizationModelId: fgaSdk.PtrString("01H0H015178Y2V4CX10C2KGHF4"), + }). + Execute() + + if err != nil { + t.Fatalf(checkRequestFailedMsg, err) + } + + if capturedHeaders[customHeaderName] != customHeaderValue { + t.Errorf(expectedCustomHeaderMsg, capturedHeaders[customHeaderName]) + } + + if capturedHeaders[defaultHeaderName] != overriddenValue { + t.Errorf(expectedOverriddenHeaderMsg, capturedHeaders[defaultHeaderName]) + } + }) +} diff --git a/client/client_test.go b/client/client_test.go index 683206e..f051e50 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -3688,6 +3688,665 @@ func TestOpenFgaClient(t *testing.T) { }) } +func TestOpenFgaClientWriteClientWriteConflictOptions(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 openfga.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 openfga.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 openfga.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 openfga.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 openfga.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 openfga.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 openfga.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 openfga.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 openfga.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 openfga.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 *OpenFgaClient) string { storeId, err := fgaClient.GetStoreId() if err != nil { diff --git a/docs/WriteRequestDeletes.md b/docs/WriteRequestDeletes.md index 9ef9418..03ef213 100644 --- a/docs/WriteRequestDeletes.md +++ b/docs/WriteRequestDeletes.md @@ -5,6 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **TupleKeys** | [**[]TupleKeyWithoutCondition**](TupleKeyWithoutCondition.md) | | +**OnMissing** | Pointer to **string** | On 'error', the API returns an error when deleting a tuple that does not exist. On 'ignore', deletes of non-existent tuples are treated as no-ops. | [optional] [default to "error"] ## Methods @@ -45,6 +46,31 @@ and a boolean to check if the value has been set. SetTupleKeys sets TupleKeys field to given value. +### GetOnMissing + +`func (o *WriteRequestDeletes) GetOnMissing() string` + +GetOnMissing returns the OnMissing field if non-nil, zero value otherwise. + +### GetOnMissingOk + +`func (o *WriteRequestDeletes) GetOnMissingOk() (*string, bool)` + +GetOnMissingOk returns a tuple with the OnMissing field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetOnMissing + +`func (o *WriteRequestDeletes) SetOnMissing(v string)` + +SetOnMissing sets OnMissing field to given value. + +### HasOnMissing + +`func (o *WriteRequestDeletes) HasOnMissing() bool` + +HasOnMissing returns a boolean if a field has been set. + [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/docs/WriteRequestWrites.md b/docs/WriteRequestWrites.md index 0f8aa86..04d1483 100644 --- a/docs/WriteRequestWrites.md +++ b/docs/WriteRequestWrites.md @@ -5,6 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **TupleKeys** | [**[]TupleKey**](TupleKey.md) | | +**OnDuplicate** | Pointer to **string** | On 'error' ( or unspecified ), the API returns an error if an identical tuple already exists. On 'ignore', identical writes are treated as no-ops (matching on user, relation, object, and RelationshipCondition). | [optional] [default to "error"] ## Methods @@ -45,6 +46,31 @@ and a boolean to check if the value has been set. SetTupleKeys sets TupleKeys field to given value. +### GetOnDuplicate + +`func (o *WriteRequestWrites) GetOnDuplicate() string` + +GetOnDuplicate returns the OnDuplicate field if non-nil, zero value otherwise. + +### GetOnDuplicateOk + +`func (o *WriteRequestWrites) GetOnDuplicateOk() (*string, bool)` + +GetOnDuplicateOk returns a tuple with the OnDuplicate field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetOnDuplicate + +`func (o *WriteRequestWrites) SetOnDuplicate(v string)` + +SetOnDuplicate sets OnDuplicate field to given value. + +### HasOnDuplicate + +`func (o *WriteRequestWrites) HasOnDuplicate() bool` + +HasOnDuplicate returns a boolean if a field has been set. + [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/example/example1/example1.go b/example/example1/example1.go index 914b069..d7853c7 100644 --- a/example/example1/example1.go +++ b/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 @@ -254,6 +258,24 @@ func mainInner() error { } fmt.Printf("Allowed: %v\n", checkResponse.Allowed) + fmt.Println("Checking for access with custom headers") + checkWithHeadersResponse, err := fgaClient.Check(ctx).Body(client.ClientCheckRequest{ + User: "user:anne", + Relation: "viewer", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + Context: &map[string]interface{}{"ViewCount": 100}, + }).Options(client.ClientCheckOptions{ + RequestOptions: client.RequestOptions{ + Headers: map[string]string{ + "X-Request-ID": "example-request-123", + }, + }, + }).Execute() + if err != nil { + return err + } + fmt.Printf("Allowed (with custom headers): %v\n", checkWithHeadersResponse.Allowed) + // BatchCheck fmt.Println("Batch checking for access") batchCheckResponse, err := fgaClient.BatchCheck(ctx).Body(client.ClientBatchCheckRequest{ diff --git a/example/example1/go.mod b/example/example1/go.mod index 3c471af..1fab570 100644 --- a/example/example1/go.mod +++ b/example/example1/go.mod @@ -19,5 +19,5 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/sync v0.16.0 // indirect + golang.org/x/sync v0.17.0 // indirect ) diff --git a/example/opentelemetry/go.mod b/example/opentelemetry/go.mod index fa62938..8cae8ab 100644 --- a/example/opentelemetry/go.mod +++ b/example/opentelemetry/go.mod @@ -31,7 +31,7 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.16.0 // indirect + golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect diff --git a/model_write_request_deletes.go b/model_write_request_deletes.go index 0667968..a1aac13 100644 --- a/model_write_request_deletes.go +++ b/model_write_request_deletes.go @@ -21,6 +21,8 @@ import ( // WriteRequestDeletes struct for WriteRequestDeletes type WriteRequestDeletes struct { TupleKeys []TupleKeyWithoutCondition `json:"tuple_keys" yaml:"tuple_keys"` + // On 'error', the API returns an error when deleting a tuple that does not exist. On 'ignore', deletes of non-existent tuples are treated as no-ops. + OnMissing *string `json:"on_missing,omitempty" yaml:"on_missing,omitempty"` } // NewWriteRequestDeletes instantiates a new WriteRequestDeletes object @@ -30,6 +32,8 @@ type WriteRequestDeletes struct { func NewWriteRequestDeletes(tupleKeys []TupleKeyWithoutCondition) *WriteRequestDeletes { this := WriteRequestDeletes{} this.TupleKeys = tupleKeys + var onMissing = "error" + this.OnMissing = &onMissing return &this } @@ -38,6 +42,8 @@ func NewWriteRequestDeletes(tupleKeys []TupleKeyWithoutCondition) *WriteRequestD // but it doesn't guarantee that properties required by API are set func NewWriteRequestDeletesWithDefaults() *WriteRequestDeletes { this := WriteRequestDeletes{} + var onMissing = "error" + this.OnMissing = &onMissing return &this } @@ -65,9 +71,44 @@ func (o *WriteRequestDeletes) SetTupleKeys(v []TupleKeyWithoutCondition) { o.TupleKeys = v } +// GetOnMissing returns the OnMissing field value if set, zero value otherwise. +func (o *WriteRequestDeletes) GetOnMissing() string { + if o == nil || o.OnMissing == nil { + var ret string + return ret + } + return *o.OnMissing +} + +// GetOnMissingOk returns a tuple with the OnMissing field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *WriteRequestDeletes) GetOnMissingOk() (*string, bool) { + if o == nil || o.OnMissing == nil { + return nil, false + } + return o.OnMissing, true +} + +// HasOnMissing returns a boolean if a field has been set. +func (o *WriteRequestDeletes) HasOnMissing() bool { + if o != nil && o.OnMissing != nil { + return true + } + + return false +} + +// SetOnMissing gets a reference to the given string and assigns it to the OnMissing field. +func (o *WriteRequestDeletes) SetOnMissing(v string) { + o.OnMissing = &v +} + func (o WriteRequestDeletes) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} toSerialize["tuple_keys"] = o.TupleKeys + if o.OnMissing != nil { + toSerialize["on_missing"] = o.OnMissing + } var b bytes.Buffer enc := json.NewEncoder(&b) enc.SetEscapeHTML(false) diff --git a/model_write_request_writes.go b/model_write_request_writes.go index 18b7d5b..af3b002 100644 --- a/model_write_request_writes.go +++ b/model_write_request_writes.go @@ -21,6 +21,8 @@ import ( // WriteRequestWrites struct for WriteRequestWrites type WriteRequestWrites struct { TupleKeys []TupleKey `json:"tuple_keys" yaml:"tuple_keys"` + // On 'error' ( or unspecified ), the API returns an error if an identical tuple already exists. On 'ignore', identical writes are treated as no-ops (matching on user, relation, object, and RelationshipCondition). + OnDuplicate *string `json:"on_duplicate,omitempty" yaml:"on_duplicate,omitempty"` } // NewWriteRequestWrites instantiates a new WriteRequestWrites object @@ -30,6 +32,8 @@ type WriteRequestWrites struct { func NewWriteRequestWrites(tupleKeys []TupleKey) *WriteRequestWrites { this := WriteRequestWrites{} this.TupleKeys = tupleKeys + var onDuplicate = "error" + this.OnDuplicate = &onDuplicate return &this } @@ -38,6 +42,8 @@ func NewWriteRequestWrites(tupleKeys []TupleKey) *WriteRequestWrites { // but it doesn't guarantee that properties required by API are set func NewWriteRequestWritesWithDefaults() *WriteRequestWrites { this := WriteRequestWrites{} + var onDuplicate = "error" + this.OnDuplicate = &onDuplicate return &this } @@ -65,9 +71,44 @@ func (o *WriteRequestWrites) SetTupleKeys(v []TupleKey) { o.TupleKeys = v } +// GetOnDuplicate returns the OnDuplicate field value if set, zero value otherwise. +func (o *WriteRequestWrites) GetOnDuplicate() string { + if o == nil || o.OnDuplicate == nil { + var ret string + return ret + } + return *o.OnDuplicate +} + +// GetOnDuplicateOk returns a tuple with the OnDuplicate field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *WriteRequestWrites) GetOnDuplicateOk() (*string, bool) { + if o == nil || o.OnDuplicate == nil { + return nil, false + } + return o.OnDuplicate, true +} + +// HasOnDuplicate returns a boolean if a field has been set. +func (o *WriteRequestWrites) HasOnDuplicate() bool { + if o != nil && o.OnDuplicate != nil { + return true + } + + return false +} + +// SetOnDuplicate gets a reference to the given string and assigns it to the OnDuplicate field. +func (o *WriteRequestWrites) SetOnDuplicate(v string) { + o.OnDuplicate = &v +} + func (o WriteRequestWrites) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} toSerialize["tuple_keys"] = o.TupleKeys + if o.OnDuplicate != nil { + toSerialize["on_duplicate"] = o.OnDuplicate + } var b bytes.Buffer enc := json.NewEncoder(&b) enc.SetEscapeHTML(false) diff --git a/models_test.go b/models_test.go new file mode 100644 index 0000000..db89f99 --- /dev/null +++ b/models_test.go @@ -0,0 +1,770 @@ +/** + * Go SDK for OpenFGA + * + * API version: 1.x + * Website: https://openfga.dev + * Documentation: https://openfga.dev/docs + * Support: https://openfga.dev/community + * License: [Apache-2.0](https://github.com/openfga/go-sdk/blob/main/LICENSE) + * + * NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. + */ + +package openfga + +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"]) + } + }) +}