From c4b416fe2064b0f71b5162622c9448d03bd1dbcd Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 6 Oct 2025 11:21:26 +0100 Subject: [PATCH 01/10] fix(ClientConfiguration): override defaultHeaders to return correct type --- .../openfga/sdk/api/configuration/ClientConfiguration.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java index b04b5be0..9e533a2a 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java @@ -140,4 +140,10 @@ public ClientConfiguration telemetryConfiguration(TelemetryConfiguration telemet super.telemetryConfiguration(telemetryConfiguration); return this; } + + @Override + public ClientConfiguration defaultHeaders(java.util.Map defaultHeaders) { + super.defaultHeaders(defaultHeaders); + return this; + } } From 705472f56239ff8ab0ce8d2c7aa4342612c30c42 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 6 Oct 2025 11:22:05 +0100 Subject: [PATCH 02/10] fix(readAuthorizationModel): correctly handle options with no modelID set --- src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java index f3d28b4d..7f495d13 100644 --- a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -248,7 +248,11 @@ public CompletableFuture readAuthorization ClientReadAuthorizationModelOptions options) throws FgaInvalidParameterException { configuration.assertValid(); String storeId = configuration.getStoreIdChecked(); - String authorizationModelId = options.getAuthorizationModelIdChecked(); + // Set authorizationModelId from options if available; otherwise, use the default from configuration + String authorizationModelId = !isNullOrWhitespace(options.getAuthorizationModelId()) + ? options.getAuthorizationModelIdChecked() + : configuration.getAuthorizationModelId(); + var overrides = new ConfigurationOverride().addHeaders(options); return call(() -> api.readAuthorizationModel(storeId, authorizationModelId, overrides)) .thenApply(ClientReadAuthorizationModelResponse::new); From 67a625e7f3bcc8caddbc835ce71875ba70492aff Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 6 Oct 2025 11:43:46 +0100 Subject: [PATCH 03/10] fix(ClientListRelationsOptions): include headers when converting to ClientBatchCheckOptions --- .../sdk/api/configuration/ClientListRelationsOptions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java index 73afd8c2..f06b3c18 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java @@ -61,6 +61,7 @@ public ConsistencyPreference getConsistency() { public ClientBatchCheckClientOptions asClientBatchCheckClientOptions() { return new ClientBatchCheckClientOptions() .authorizationModelId(authorizationModelId) + .additionalHeaders(additionalHeaders) .maxParallelRequests(maxParallelRequests) .consistency(consistency); } From 7086be51cbe1a2837bd4e443f9a8aa648fe20788 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 6 Oct 2025 11:44:26 +0100 Subject: [PATCH 04/10] test: add tests to cover headers --- .../api/client/OpenFgaClientHeadersTest.java | 839 ++++++++++++++++++ 1 file changed, 839 insertions(+) create mode 100644 src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java new file mode 100644 index 00000000..3479252c --- /dev/null +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java @@ -0,0 +1,839 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 1.x + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.client; + +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pgssoft.httpclient.HttpClientMock; +import dev.openfga.sdk.api.client.model.*; +import dev.openfga.sdk.api.configuration.*; +import dev.openfga.sdk.api.model.*; +import java.net.http.HttpClient; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for OpenFgaClient header functionality. + */ +public class OpenFgaClientHeadersTest { + private static final String DEFAULT_STORE_ID = "01YCP46JKYM8FJCQ37NMBYHE5X"; + private static final String DEFAULT_STORE_NAME = "test_store"; + private static final String DEFAULT_AUTH_MODEL_ID = "01G5JAVJ41T49E9TT3SKVS7X1J"; + private static final String DEFAULT_USER = "user:81684243-9356-4421-8fbf-a4f8d36aa31b"; + private static final String DEFAULT_RELATION = "reader"; + private static final String DEFAULT_TYPE = "document"; + private static final String DEFAULT_ID = "budget"; + private static final String DEFAULT_OBJECT = DEFAULT_TYPE + ":" + DEFAULT_ID; + private static final String DEFAULT_SCHEMA_VERSION = "1.1"; + private static final String EMPTY_RESPONSE_BODY = "{}"; + private OpenFgaClient fga; + private ClientConfiguration clientConfiguration; + private HttpClientMock mockHttpClient; + + @BeforeEach + public void beforeEachTest() throws Exception { + mockHttpClient = new HttpClientMock(); + mockHttpClient.debugOn(); + + var mockHttpClientBuilder = mock(HttpClient.Builder.class); + when(mockHttpClientBuilder.executor(any())).thenReturn(mockHttpClientBuilder); + when(mockHttpClientBuilder.build()).thenReturn(mockHttpClient); + + clientConfiguration = new ClientConfiguration() + .storeId(DEFAULT_STORE_ID) + .authorizationModelId(DEFAULT_AUTH_MODEL_ID) + .apiUrl("https://api.fga.example") + .defaultHeaders(Map.of( + "test-header", "test-value", + "another-header", "another-value")); + + var mockApiClient = mock(ApiClient.class); + when(mockApiClient.getHttpClient()).thenReturn(mockHttpClient); + when(mockApiClient.getObjectMapper()).thenReturn(new ObjectMapper()); + when(mockApiClient.getHttpClientBuilder()).thenReturn(mockHttpClientBuilder); + + fga = new OpenFgaClient(clientConfiguration, mockApiClient); + } + + @Test + public void createStore_withHeaders() throws Exception { + // Given + String expectedBody = String.format("{\"name\":\"%s\"}", DEFAULT_STORE_NAME); + String requestBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onPost("https://api.fga.example/stores") + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(201, requestBody); + CreateStoreRequest request = new CreateStoreRequest().name(DEFAULT_STORE_NAME); + ClientCreateStoreOptions options = + new ClientCreateStoreOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + CreateStoreResponse response = fga.createStore(request, options).get(); + + // Then + mockHttpClient + .verify() + .post("https://api.fga.example/stores") + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + assertEquals(DEFAULT_STORE_NAME, response.getName()); + } + + @Test + public void createStore_withEmptyHeaders() throws Exception { + // Given + String expectedBody = String.format("{\"name\":\"%s\"}", DEFAULT_STORE_NAME); + String requestBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onPost("https://api.fga.example/stores") + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value") + .doReturn(201, requestBody); + CreateStoreRequest request = new CreateStoreRequest().name(DEFAULT_STORE_NAME); + ClientCreateStoreOptions options = new ClientCreateStoreOptions().additionalHeaders(Collections.emptyMap()); + + // When + CreateStoreResponse response = fga.createStore(request, options).get(); + + // Then - should use default headers only + mockHttpClient + .verify() + .post("https://api.fga.example/stores") + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value") + .called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + } + + @Test + public void createStore_withNullHeaders() throws Exception { + // Given + String expectedBody = String.format("{\"name\":\"%s\"}", DEFAULT_STORE_NAME); + String requestBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onPost("https://api.fga.example/stores") + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value") + .doReturn(201, requestBody); + CreateStoreRequest request = new CreateStoreRequest().name(DEFAULT_STORE_NAME); + ClientCreateStoreOptions options = new ClientCreateStoreOptions().additionalHeaders(null); + + // When + CreateStoreResponse response = fga.createStore(request, options).get(); + + // Then - should use default headers only + mockHttpClient + .verify() + .post("https://api.fga.example/stores") + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value") + .called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + } + + @Test + public void createStore_withNewHeaders() throws Exception { + // Given + String expectedBody = String.format("{\"name\":\"%s\"}", DEFAULT_STORE_NAME); + String requestBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onPost("https://api.fga.example/stores") + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value") + .withHeader("new-header", "new-value") + .doReturn(201, requestBody); + CreateStoreRequest request = new CreateStoreRequest().name(DEFAULT_STORE_NAME); + ClientCreateStoreOptions options = + new ClientCreateStoreOptions().additionalHeaders(Map.of("new-header", "new-value")); + + // When + CreateStoreResponse response = fga.createStore(request, options).get(); + + // Then - should include both default and new headers + mockHttpClient + .verify() + .post("https://api.fga.example/stores") + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value") + .withHeader("new-header", "new-value") + .called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + } + + @Test + public void listStores_withHeaders() throws Exception { + // Given + String responseBody = + String.format("{\"stores\":[{\"id\":\"%s\",\"name\":\"%s\"}]}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onGet("https://api.fga.example/stores") + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, responseBody); + ClientListStoresOptions options = + new ClientListStoresOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientListStoresResponse response = fga.listStores(options).get(); + + // Then + mockHttpClient + .verify() + .get("https://api.fga.example/stores") + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertNotNull(response.getStores()); + assertEquals(1, response.getStores().size()); + } + + @Test + public void getStore_withHeaders() throws Exception { + // Given + String getUrl = String.format("https://api.fga.example/stores/%s", DEFAULT_STORE_ID); + String responseBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onGet(getUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, responseBody); + ClientGetStoreOptions options = + new ClientGetStoreOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientGetStoreResponse response = fga.getStore(options).get(); + + // Then + mockHttpClient + .verify() + .get(getUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + } + + @Test + public void deleteStore_withHeaders() throws Exception { + // Given + String deleteUrl = String.format("https://api.fga.example/stores/%s", DEFAULT_STORE_ID); + mockHttpClient + .onDelete(deleteUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(204, EMPTY_RESPONSE_BODY); + ClientDeleteStoreOptions options = + new ClientDeleteStoreOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientDeleteStoreResponse response = fga.deleteStore(options).get(); + + // Then + mockHttpClient + .verify() + .delete(deleteUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertEquals(204, response.getStatusCode()); + } + + @Test + public void readAuthorizationModels_withHeaders() throws Exception { + // Given + String getUrl = String.format("https://api.fga.example/stores/%s/authorization-models", DEFAULT_STORE_ID); + String responseBody = String.format( + "{\"authorization_models\":[{\"id\":\"%s\",\"schema_version\":\"%s\"}]}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_SCHEMA_VERSION); + mockHttpClient + .onGet(getUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, responseBody); + ClientReadAuthorizationModelsOptions options = new ClientReadAuthorizationModelsOptions() + .additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientReadAuthorizationModelsResponse response = + fga.readAuthorizationModels(options).get(); + + // Then + mockHttpClient + .verify() + .get(getUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertNotNull(response.getAuthorizationModels()); + assertEquals(1, response.getAuthorizationModels().size()); + } + + @Test + public void writeAuthorizationModel_withHeaders() throws Exception { + // Given + String postUrl = String.format("https://api.fga.example/stores/%s/authorization-models", DEFAULT_STORE_ID); + String expectedBody = + "{\"type_definitions\":[{\"type\":\"document\",\"relations\":{},\"metadata\":null}],\"schema_version\":\"1.1\",\"conditions\":{}}"; + String responseBody = String.format("{\"authorization_model_id\":\"%s\"}", DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPost(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(201, responseBody); + WriteAuthorizationModelRequest request = new WriteAuthorizationModelRequest() + .schemaVersion(DEFAULT_SCHEMA_VERSION) + .typeDefinitions(List.of(new TypeDefinition().type(DEFAULT_TYPE))); + ClientWriteAuthorizationModelOptions options = new ClientWriteAuthorizationModelOptions() + .additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientWriteAuthorizationModelResponse response = + fga.writeAuthorizationModel(request, options).get(); + + // Then + mockHttpClient + .verify() + .post(postUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertEquals(DEFAULT_AUTH_MODEL_ID, response.getAuthorizationModelId()); + } + + @Test + public void readAuthorizationModel_withHeaders() throws Exception { + // Given + String getUrl = String.format( + "https://api.fga.example/stores/%s/authorization-models/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + String getResponse = String.format( + "{\"authorization_model\":{\"id\":\"%s\",\"schema_version\":\"%s\"}}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_SCHEMA_VERSION); + mockHttpClient + .onGet(getUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, getResponse); + ClientReadAuthorizationModelOptions options = new ClientReadAuthorizationModelOptions() + .additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientReadAuthorizationModelResponse response = + fga.readAuthorizationModel(options).get(); + + // Then + mockHttpClient + .verify() + .get(getUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertNotNull(response.getAuthorizationModel()); + assertEquals(DEFAULT_AUTH_MODEL_ID, response.getAuthorizationModel().getId()); + } + + @Test + public void read_withHeaders() throws Exception { + // Given + String postUrl = String.format("https://api.fga.example/stores/%s/read", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"page_size\":null,\"continuation_token\":null,\"consistency\":\"UNSPECIFIED\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); + String responseBody = String.format( + "{\"tuples\":[{\"key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}}]}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); + mockHttpClient + .onPost(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, responseBody); + ClientReadRequest request = new ClientReadRequest() + .user(DEFAULT_USER) + .relation(DEFAULT_RELATION) + ._object(DEFAULT_OBJECT); + ClientReadOptions options = + new ClientReadOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientReadResponse response = fga.read(request, options).get(); + + // Then + mockHttpClient + .verify() + .post(postUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertNotNull(response.getTuples()); + assertEquals(1, response.getTuples().size()); + } + + @Test + public void write_withHeaders() throws Exception { + // Given + String postPath = String.format("https://api.fga.example/stores/%s/write", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":null}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPost(postPath) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, EMPTY_RESPONSE_BODY); + ClientWriteRequest request = new ClientWriteRequest() + .writes(List.of(new ClientTupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER))); + ClientWriteOptions options = + new ClientWriteOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientWriteResponse response = fga.write(request, options).get(); + + // Then + mockHttpClient + .verify() + .post(postPath) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertEquals(200, response.getStatusCode()); + } + + @Test + public void check_withHeaders() throws Exception { + // Given + String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"%s\",\"trace\":null,\"context\":null,\"consistency\":\"UNSPECIFIED\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPost(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, "{\"allowed\":true}"); + ClientCheckRequest request = new ClientCheckRequest() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER); + ClientCheckOptions options = + new ClientCheckOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientCheckResponse response = fga.check(request, options).get(); + + // Then + mockHttpClient + .verify() + .post(postUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertEquals(Boolean.TRUE, response.getAllowed()); + } + + @Test + public void batchCheck_withHeaders() throws Exception { + // Given + String postUrl = String.format("https://api.fga.example/stores/%s/batch-check", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"checks\":[{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"context\":null,\"correlation_id\":\"cor-1\"}],\"authorization_model_id\":\"%s\",\"consistency\":\"UNSPECIFIED\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPost(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, "{\"result\":{}}"); + ClientBatchCheckItem item = new ClientBatchCheckItem() + .user(DEFAULT_USER) + .relation(DEFAULT_RELATION) + ._object(DEFAULT_OBJECT) + .correlationId("cor-1"); + ClientBatchCheckRequest request = new ClientBatchCheckRequest().checks(List.of(item)); + // Use HashMap instead of Map.of() to create a mutable map + Map headers = new java.util.HashMap<>(); + headers.put("test-header", "test-value-per-call"); + ClientBatchCheckOptions options = new ClientBatchCheckOptions().additionalHeaders(headers); + + // When + ClientBatchCheckResponse response = fga.batchCheck(request, options).join(); + + // Then + mockHttpClient + .verify() + .post(postUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertNotNull(response); + assertTrue(response.getResult().isEmpty()); + } + + @Test + public void expand_withHeaders() throws Exception { + // Given + String postPath = String.format("https://api.fga.example/stores/%s/expand", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"tuple_key\":{\"relation\":\"%s\",\"object\":\"%s\"},\"authorization_model_id\":\"%s\",\"consistency\":\"UNSPECIFIED\",\"contextual_tuples\":null}", + DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + String responseBody = String.format( + "{\"tree\":{\"root\":{\"union\":{\"nodes\":[{\"leaf\":{\"users\":{\"users\":[\"%s\"]}}}]}}}}", + DEFAULT_USER); + mockHttpClient + .onPost(postPath) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, responseBody); + ClientExpandRequest request = + new ClientExpandRequest().relation(DEFAULT_RELATION)._object(DEFAULT_OBJECT); + ClientExpandOptions options = + new ClientExpandOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientExpandResponse response = fga.expand(request, options).get(); + + // Then + mockHttpClient + .verify() + .post(postPath) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertNotNull(response.getTree()); + } + + @Test + public void listObjects_withHeaders() throws Exception { + // Given + String postPath = String.format("https://api.fga.example/stores/%s/list-objects", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"authorization_model_id\":\"%s\",\"type\":null,\"relation\":\"%s\",\"user\":\"%s\",\"contextual_tuples\":null,\"context\":null,\"consistency\":\"UNSPECIFIED\"}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_RELATION, DEFAULT_USER); + mockHttpClient + .onPost(postPath) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, String.format("{\"objects\":[\"%s\"]}", DEFAULT_OBJECT)); + ClientListObjectsRequest request = + new ClientListObjectsRequest().relation(DEFAULT_RELATION).user(DEFAULT_USER); + ClientListObjectsOptions options = + new ClientListObjectsOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientListObjectsResponse response = fga.listObjects(request, options).get(); + + // Then + mockHttpClient + .verify() + .post(postPath) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertEquals(List.of(DEFAULT_OBJECT), response.getObjects()); + } + + @Test + public void listUsers_withHeaders() throws Exception { + // Given + String postPath = String.format("https://api.fga.example/stores/%s/list-users", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"authorization_model_id\":\"%s\",\"object\":{\"type\":\"%s\",\"id\":\"%s\"},\"relation\":\"%s\",\"user_filters\":null,\"contextual_tuples\":[],\"context\":null,\"consistency\":\"UNSPECIFIED\"}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_TYPE, DEFAULT_ID, DEFAULT_RELATION); + mockHttpClient + .onPost(postPath) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, "{\"users\":[]}"); + ClientListUsersRequest request = new ClientListUsersRequest() + ._object(new FgaObject().type(DEFAULT_TYPE).id(DEFAULT_ID)) + .relation(DEFAULT_RELATION); + ClientListUsersOptions options = + new ClientListUsersOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientListUsersResponse response = fga.listUsers(request, options).get(); + + // Then + mockHttpClient + .verify() + .post(postPath) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertNotNull(response.getUsers()); + } + + @Test + public void readAssertions_withHeaders() throws Exception { + // Given + String getUrl = String.format( + "https://api.fga.example/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + String responseBody = String.format( + "{\"assertions\":[{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"expectation\":true}]}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); + mockHttpClient + .onGet(getUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, responseBody); + ClientReadAssertionsOptions options = + new ClientReadAssertionsOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientReadAssertionsResponse response = fga.readAssertions(options).get(); + + // Then + mockHttpClient + .verify() + .get(getUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertNotNull(response.getAssertions()); + assertEquals(1, response.getAssertions().size()); + } + + @Test + public void writeAssertions_withHeaders() throws Exception { + // Given + String putUrl = String.format( + "https://api.fga.example/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); + String expectedBody = String.format( + "{\"assertions\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"expectation\":true,\"contextual_tuples\":[],\"context\":null}]}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + mockHttpClient + .onPut(putUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, EMPTY_RESPONSE_BODY); + List assertions = List.of(new ClientAssertion() + .user(DEFAULT_USER) + .relation(DEFAULT_RELATION) + ._object(DEFAULT_OBJECT) + .expectation(true)); + ClientWriteAssertionsOptions options = + new ClientWriteAssertionsOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientWriteAssertionsResponse response = + fga.writeAssertions(assertions, options).get(); + + // Then + mockHttpClient + .verify() + .put(putUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertEquals(200, response.getStatusCode()); + } + + @Test + public void readLatestAuthorizationModel_withHeaders() throws Exception { + // Given + String getUrl = + String.format("https://api.fga.example/stores/%s/authorization-models?page_size=1", DEFAULT_STORE_ID); + String responseBody = String.format( + "{\"authorization_models\":[{\"id\":\"%s\",\"schema_version\":\"%s\"}]}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_SCHEMA_VERSION); + mockHttpClient + .onGet(getUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, responseBody); + ClientReadLatestAuthorizationModelOptions options = new ClientReadLatestAuthorizationModelOptions() + .additionalHeaders(Map.of("test-header", "test-value-per-call")); + + // When + ClientReadAuthorizationModelResponse response = + fga.readLatestAuthorizationModel(options).get(); + + // Then + mockHttpClient + .verify() + .get(getUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertNotNull(response.getAuthorizationModel()); + assertEquals(DEFAULT_AUTH_MODEL_ID, response.getAuthorizationModel().getId()); + } + + @Test + public void listRelations_withHeaders() throws Exception { + // Given + String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"%s\",\"trace\":null,\"context\":null,\"consistency\":\"UNSPECIFIED\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + + mockHttpClient + .onPost(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, "{\"allowed\":true}"); + + ClientListRelationsRequest request = new ClientListRelationsRequest() + .relations(List.of(DEFAULT_RELATION)) + .user(DEFAULT_USER) + ._object(DEFAULT_OBJECT); + // Use HashMap instead of Map.of() to create a mutable map + Map headers = new java.util.HashMap<>(); + headers.put("test-header", "test-value-per-call"); + ClientListRelationsOptions options = new ClientListRelationsOptions().additionalHeaders(headers); + + // When + ClientListRelationsResponse response = + fga.listRelations(request, options).get(); + + mockHttpClient + .verify() + .post(postUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + } + + /** + * Edge case: No default headers configured on client. + */ + @Test + public void createStore_withNoDefaultHeaders() throws Exception { + // Given - reconfigure without default headers + clientConfiguration.defaultHeaders(null); + fga.setConfiguration(clientConfiguration); + + String expectedBody = String.format("{\"name\":\"%s\"}", DEFAULT_STORE_NAME); + String requestBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onPost("https://api.fga.example/stores") + .withBody(is(expectedBody)) + .withHeader("per-call-header", "per-call-value") + .doReturn(201, requestBody); + CreateStoreRequest request = new CreateStoreRequest().name(DEFAULT_STORE_NAME); + ClientCreateStoreOptions options = + new ClientCreateStoreOptions().additionalHeaders(Map.of("per-call-header", "per-call-value")); + + // When + CreateStoreResponse response = fga.createStore(request, options).get(); + + // Then - should only have per-call headers + mockHttpClient + .verify() + .post("https://api.fga.example/stores") + .withHeader("per-call-header", "per-call-value") + .called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + } + + /** + * Edge case: Multiple headers with same key override correctly. + */ + @Test + public void write_multipleOverrides() throws Exception { + // Given + String postPath = String.format("https://api.fga.example/stores/%s/write", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":null}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPost(postPath) + .withBody(is(expectedBody)) + .withHeader("test-header", "override-3") + .withHeader("another-header", "override-2") + .doReturn(200, EMPTY_RESPONSE_BODY); + ClientWriteRequest request = new ClientWriteRequest() + .writes(List.of(new ClientTupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER))); + ClientWriteOptions options = new ClientWriteOptions() + .additionalHeaders(Map.of("test-header", "override-3", "another-header", "override-2")); + + // When + ClientWriteResponse response = fga.write(request, options).get(); + + // Then - all headers should be overridden + mockHttpClient + .verify() + .post(postPath) + .withHeader("test-header", "override-3") + .withHeader("another-header", "override-2") + .called(1); + assertEquals(200, response.getStatusCode()); + } + + /** + * Edge case: Special characters in header values. + */ + @Test + public void check_withSpecialCharactersInHeaders() throws Exception { + // Given + String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"%s\",\"trace\":null,\"context\":null,\"consistency\":\"UNSPECIFIED\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPost(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "value-with-dashes_and_underscores") + .withHeader("x-custom", "UTF-8,gzip,deflate") + .doReturn(200, "{\"allowed\":true}"); + ClientCheckRequest request = new ClientCheckRequest() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER); + ClientCheckOptions options = new ClientCheckOptions() + .additionalHeaders(Map.of( + "test-header", "value-with-dashes_and_underscores", + "x-custom", "UTF-8,gzip,deflate")); + + // When + ClientCheckResponse response = fga.check(request, options).get(); + + // Then + mockHttpClient + .verify() + .post(postUrl) + .withHeader("test-header", "value-with-dashes_and_underscores") + .withHeader("x-custom", "UTF-8,gzip,deflate") + .called(1); + assertEquals(Boolean.TRUE, response.getAllowed()); + } +} From cba8f6a8f368925eae7b91ed7e1731c1f914a8c4 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 6 Oct 2025 11:44:43 +0100 Subject: [PATCH 05/10] docs: add docs to readme for header usage --- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/README.md b/README.md index 4896a394..1c6e1058 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,67 @@ public class Example { } ``` +### Custom Headers + +#### Default Headers + +You can set default headers to be sent with every request by using the `defaultHeaders` property of the `ClientConfiguration` class. + +```java +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.configuration.ClientConfiguration; + +import java.net.http.HttpClient; +import java.util.Map; + +public class Example { + public static void main(String[] args) throws Exception { + var config = new ClientConfiguration() + .apiUrl(System.getenv("FGA_API_URL")) + .storeId(System.getenv("FGA_STORE_ID")) + .authorizationModelId(System.getenv("FGA_MODEL_ID")) + .defaultHeaders(Map.of( + "X-Custom-Header", "default-value", + "X-Request-Source", "my-app" + )); + + var fgaClient = new OpenFgaClient(config); + } +} +``` + +#### Per-request Headers + +You can set custom headers to be sent with a specific request by using the `additionalHeaders` property of the options classes (e.g. `ClientReadOptions`, `ClientWriteOptions`, etc.). + +```java +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.configuration.ClientConfiguration; +import java.net.http.HttpClient; + +public class Example { + public static void main(String[] args) throws Exception { + var config = new ClientConfiguration() + .apiUrl(System.getenv("FGA_API_URL")) + .storeId(System.getenv("FGA_STORE_ID")) + .authorizationModelId(System.getenv("FGA_MODEL_ID")) + .defaultHeaders(Map.of( + "X-Custom-Header", "default-value", + "X-Request-Source", "my-app" + )); + + var fgaClient = new OpenFgaClient(config); + var options = new ClientReadOptions() + .additionalHeaders(Map.of( + "X-Request-Id", "123e4567-e89b-12d3-a456-426614174000", + "X-Custom-Header", "overridden-value" // this will override the default value for this request only + ) + ); + } +} +``` ### Get your Store ID From 2f3bcf9421cfbf758d99a7b69e4f229600dce0e4 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 6 Oct 2025 12:15:23 +0100 Subject: [PATCH 06/10] test: update listRelations tests to have correct header value --- .../openfga/sdk/api/client/OpenFgaClientTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java index fff5bf27..2f1d064d 100644 --- a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java @@ -2389,7 +2389,7 @@ public void listRelations() throws Exception { mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ListRelations") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .doReturn(200, "{\"allowed\":true}"); ClientListRelationsRequest request = new ClientListRelationsRequest() @@ -2409,7 +2409,7 @@ public void listRelations() throws Exception { .verify() .post(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ListRelations") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .called(1); assertNotNull(response); @@ -2428,7 +2428,7 @@ public void listRelations_deny() throws Exception { mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ListRelations") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .doReturn(200, "{\"allowed\":false}"); ClientListRelationsRequest request = new ClientListRelationsRequest() @@ -2447,7 +2447,7 @@ public void listRelations_deny() throws Exception { .verify() .post(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ListRelations") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .called(1); assertNotNull(response); @@ -2616,7 +2616,7 @@ public void listRelations_contextAndContextualTuples() throws Exception { mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ListRelations") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .doReturn(200, "{\"allowed\":false}"); ClientListRelationsRequest request = new ClientListRelationsRequest() @@ -2640,7 +2640,7 @@ public void listRelations_contextAndContextualTuples() throws Exception { .verify() .post(postUrl) .withBody(is(expectedBody)) - .withHeader(CLIENT_METHOD_HEADER, "ClientBatchCheck") + .withHeader(CLIENT_METHOD_HEADER, "ListRelations") .withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID()) .called(1); assertNotNull(response); From 45669df365b088292fdf9c65a415e4d5f09bcd1c Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 6 Oct 2025 12:18:05 +0100 Subject: [PATCH 07/10] refactor: address review comments regarding npe, readme update and copying headers --- README.md | 2 ++ .../dev/openfga/sdk/api/client/OpenFgaClient.java | 12 +++++++----- .../configuration/ClientBatchCheckClientOptions.java | 3 ++- .../configuration/ClientListRelationsOptions.java | 3 ++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1c6e1058..0eaa2f09 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This is an autogenerated Java SDK for OpenFGA. It provides a wrapper around the - [Installation](#installation) - [Getting Started](#getting-started) - [Initializing the API Client](#initializing-the-api-client) + - [Custom Headers](#custom-headers) - [Get your Store ID](#get-your-store-id) - [Calling the API](#calling-the-api) - [Stores](#stores) @@ -294,6 +295,7 @@ public class Example { "X-Custom-Header", "overridden-value" // this will override the default value for this request only ) ); + var response = fgaClient.read(request, options).get(); } } ``` diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java index 7f495d13..4d48a98d 100644 --- a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -248,11 +248,13 @@ public CompletableFuture readAuthorization ClientReadAuthorizationModelOptions options) throws FgaInvalidParameterException { configuration.assertValid(); String storeId = configuration.getStoreIdChecked(); - // Set authorizationModelId from options if available; otherwise, use the default from configuration - String authorizationModelId = !isNullOrWhitespace(options.getAuthorizationModelId()) - ? options.getAuthorizationModelIdChecked() - : configuration.getAuthorizationModelId(); - + // Set authorizationModelId from options if available; otherwise, require a valid configuration value + String authorizationModelId; + if (options != null && !isNullOrWhitespace(options.getAuthorizationModelId())) { + authorizationModelId = options.getAuthorizationModelIdChecked(); + } else { + authorizationModelId = configuration.getAuthorizationModelIdChecked(); + } var overrides = new ConfigurationOverride().addHeaders(options); return call(() -> api.readAuthorizationModel(storeId, authorizationModelId, overrides)) .thenApply(ClientReadAuthorizationModelResponse::new); diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java index 0230bb15..a5f80788 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java @@ -13,6 +13,7 @@ package dev.openfga.sdk.api.configuration; import dev.openfga.sdk.api.model.ConsistencyPreference; +import java.util.HashMap; import java.util.Map; public class ClientBatchCheckClientOptions implements AdditionalHeadersSupplier { @@ -60,7 +61,7 @@ public ConsistencyPreference getConsistency() { public ClientCheckOptions asClientCheckOptions() { return new ClientCheckOptions() - .additionalHeaders(additionalHeaders) + .additionalHeaders(additionalHeaders != null ? new HashMap<>(additionalHeaders) : null) .authorizationModelId(authorizationModelId) .consistency(consistency); } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java index f06b3c18..2c0c5410 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java @@ -13,6 +13,7 @@ package dev.openfga.sdk.api.configuration; import dev.openfga.sdk.api.model.ConsistencyPreference; +import java.util.HashMap; import java.util.Map; public class ClientListRelationsOptions implements AdditionalHeadersSupplier { @@ -61,7 +62,7 @@ public ConsistencyPreference getConsistency() { public ClientBatchCheckClientOptions asClientBatchCheckClientOptions() { return new ClientBatchCheckClientOptions() .authorizationModelId(authorizationModelId) - .additionalHeaders(additionalHeaders) + .additionalHeaders(additionalHeaders != null ? new HashMap<>(additionalHeaders) : null) .maxParallelRequests(maxParallelRequests) .consistency(consistency); } From 90294e37ce4746faec03e1c81057d41976b30e09 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 6 Oct 2025 14:24:08 +0100 Subject: [PATCH 08/10] test: add test for null headers --- .../api/client/OpenFgaClientHeadersTest.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java index 3479252c..e8f31910 100644 --- a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java @@ -499,6 +499,42 @@ public void batchCheck_withHeaders() throws Exception { assertTrue(response.getResult().isEmpty()); } + @Test + public void clientBatchCheck_withHeaders() throws Exception { + // Given + String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"%s\",\"trace\":null,\"context\":null,\"consistency\":\"UNSPECIFIED\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPost(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, "{\"allowed\":true}"); + ClientCheckRequest request = new ClientCheckRequest() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER); + Map headers = new java.util.HashMap<>(); + headers.put("test-header", "test-value-per-call"); + ClientBatchCheckClientOptions options = new ClientBatchCheckClientOptions().additionalHeaders(headers); + + // When + List response = + fga.clientBatchCheck(List.of(request), options).get(); + + // Then + mockHttpClient + .verify() + .post(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertEquals(Boolean.TRUE, response.get(0).getAllowed()); + } + @Test public void expand_withHeaders() throws Exception { // Given @@ -721,6 +757,7 @@ public void listRelations_withHeaders() throws Exception { ClientListRelationsResponse response = fga.listRelations(request, options).get(); + // Then mockHttpClient .verify() .post(postUrl) @@ -729,6 +766,75 @@ public void listRelations_withHeaders() throws Exception { .called(1); } + @Test + public void listRelations_withNullHeaders() throws Exception { + // Given + String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"%s\",\"trace\":null,\"context\":null,\"consistency\":\"UNSPECIFIED\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + + mockHttpClient + .onPost(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value") + .doReturn(200, "{\"allowed\":true}"); + + ClientListRelationsRequest request = new ClientListRelationsRequest() + .relations(List.of(DEFAULT_RELATION)) + .user(DEFAULT_USER) + ._object(DEFAULT_OBJECT); + ClientListRelationsOptions options = new ClientListRelationsOptions().additionalHeaders(null); + + // When - this should not throw even though additionalHeaders is null + ClientListRelationsResponse response = + fga.listRelations(request, options).get(); + + // Then + mockHttpClient + .verify() + .post(postUrl) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value") + .called(1); + assertNotNull(response); + } + + @Test + public void clientBatchCheck_withNullHeaders() throws Exception { + // Given + String postUrl = String.format("https://api.fga.example/stores/%s/check", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"%s\",\"trace\":null,\"context\":null,\"consistency\":\"UNSPECIFIED\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPost(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value") + .doReturn(200, "{\"allowed\":true}"); + ClientCheckRequest request = new ClientCheckRequest() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER); + ClientBatchCheckClientOptions options = new ClientBatchCheckClientOptions().additionalHeaders(null); + + // When - this should not throw even though additionalHeaders is null + List response = + fga.clientBatchCheck(List.of(request), options).get(); + + // Then + mockHttpClient + .verify() + .post(postUrl) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value") + .called(1); + assertEquals(Boolean.TRUE, response.get(0).getAllowed()); + } + /** * Edge case: No default headers configured on client. */ From d478606d8c3a692f5799605cb3a5d22c65687b2d Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 6 Oct 2025 15:48:52 +0100 Subject: [PATCH 09/10] refactor: change header handling to support immutable maps --- .../openfga/sdk/api/client/OpenFgaClient.java | 26 ++++++++++--------- .../api/client/OpenFgaClientHeadersTest.java | 11 +++----- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java index 4d48a98d..87d7b3b5 100644 --- a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -838,12 +838,13 @@ public CompletableFuture> clientBatchCheck( var options = batchCheckOptions != null ? batchCheckOptions : new ClientBatchCheckClientOptions().maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS); - if (options.getAdditionalHeaders() == null) { - options.additionalHeaders(new HashMap<>()); - } - options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "ClientBatchCheck"); - options.getAdditionalHeaders() - .putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); + + HashMap headers = options.getAdditionalHeaders() != null + ? new HashMap<>(options.getAdditionalHeaders()) + : new HashMap<>(); + headers.putIfAbsent(CLIENT_METHOD_HEADER, "ClientBatchCheck"); + headers.putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); + options.additionalHeaders(headers); int maxParallelRequests = options.getMaxParallelRequests() != null ? options.getMaxParallelRequests() @@ -1130,12 +1131,13 @@ public CompletableFuture listRelations( var options = listRelationsOptions != null ? listRelationsOptions : new ClientListRelationsOptions().maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS); - if (options.getAdditionalHeaders() == null) { - options.additionalHeaders(new HashMap<>()); - } - options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "ListRelations"); - options.getAdditionalHeaders() - .putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); + + HashMap headers = options.getAdditionalHeaders() != null + ? new HashMap<>(options.getAdditionalHeaders()) + : new HashMap<>(); + headers.put(CLIENT_METHOD_HEADER, "ListRelations"); + headers.put(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); + options.additionalHeaders(headers); var batchCheckRequests = request.getRelations().stream() .map(relation -> new ClientCheckRequest() diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java index e8f31910..9bff66fe 100644 --- a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java @@ -516,9 +516,8 @@ public void clientBatchCheck_withHeaders() throws Exception { ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER); - Map headers = new java.util.HashMap<>(); - headers.put("test-header", "test-value-per-call"); - ClientBatchCheckClientOptions options = new ClientBatchCheckClientOptions().additionalHeaders(headers); + ClientBatchCheckClientOptions options = + new ClientBatchCheckClientOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); // When List response = @@ -748,10 +747,8 @@ public void listRelations_withHeaders() throws Exception { .relations(List.of(DEFAULT_RELATION)) .user(DEFAULT_USER) ._object(DEFAULT_OBJECT); - // Use HashMap instead of Map.of() to create a mutable map - Map headers = new java.util.HashMap<>(); - headers.put("test-header", "test-value-per-call"); - ClientListRelationsOptions options = new ClientListRelationsOptions().additionalHeaders(headers); + ClientListRelationsOptions options = + new ClientListRelationsOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); // When ClientListRelationsResponse response = From 33fa182b256b4a7184bcc4c32a452c533088f1ed Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 6 Oct 2025 17:21:12 +0100 Subject: [PATCH 10/10] refactor: update writeNonTransaction and batchCheck to handle immutable map for headers --- .../openfga/sdk/api/client/OpenFgaClient.java | 29 ++++++------- .../api/client/OpenFgaClientHeadersTest.java | 41 +++++++++++++++++-- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java index 87d7b3b5..bd1816bf 100644 --- a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -558,12 +558,12 @@ private CompletableFuture writeNonTransaction( ? writeOptions : new ClientWriteOptions().transactionChunkSize(DEFAULT_MAX_METHOD_PARALLEL_REQS); - if (options.getAdditionalHeaders() == null) { - options.additionalHeaders(new HashMap<>()); - } - options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "Write"); - options.getAdditionalHeaders() - .putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); + HashMap headers = options.getAdditionalHeaders() != null + ? new HashMap<>(options.getAdditionalHeaders()) + : new HashMap<>(); + headers.putIfAbsent(CLIENT_METHOD_HEADER, "Write"); + headers.putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); + options.additionalHeaders(headers); int chunkSize = options.getTransactionChunkSize(); @@ -899,12 +899,13 @@ public CompletableFuture batchCheck( : new ClientBatchCheckOptions() .maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS) .maxBatchSize(DEFAULT_MAX_BATCH_SIZE); - if (options.getAdditionalHeaders() == null) { - options.additionalHeaders(new HashMap<>()); - } - options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "BatchCheck"); - options.getAdditionalHeaders() - .putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); + + HashMap headers = options.getAdditionalHeaders() != null + ? new HashMap<>(options.getAdditionalHeaders()) + : new HashMap<>(); + headers.putIfAbsent(CLIENT_METHOD_HEADER, "BatchCheck"); + headers.putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); + options.additionalHeaders(headers); Map correlationIdToCheck = new HashMap<>(); @@ -1135,8 +1136,8 @@ public CompletableFuture listRelations( HashMap headers = options.getAdditionalHeaders() != null ? new HashMap<>(options.getAdditionalHeaders()) : new HashMap<>(); - headers.put(CLIENT_METHOD_HEADER, "ListRelations"); - headers.put(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); + headers.putIfAbsent(CLIENT_METHOD_HEADER, "ListRelations"); + headers.putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString()); options.additionalHeaders(headers); var batchCheckRequests = request.getRelations().stream() diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java index 9bff66fe..5b9804a7 100644 --- a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientHeadersTest.java @@ -428,6 +428,41 @@ public void write_withHeaders() throws Exception { assertEquals(200, response.getStatusCode()); } + @Test + public void writeNonTransaction_withHeaders() throws Exception { + // Given + String postPath = String.format("https://api.fga.example/stores/%s/write", DEFAULT_STORE_ID); + String expectedBody = String.format( + "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":null}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + mockHttpClient + .onPost(postPath) + .withBody(is(expectedBody)) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .doReturn(200, EMPTY_RESPONSE_BODY); + ClientWriteRequest request = new ClientWriteRequest() + .writes(List.of(new ClientTupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER))); + ClientWriteOptions options = new ClientWriteOptions() + .additionalHeaders(Map.of("test-header", "test-value-per-call")) + .disableTransactions(true); + + // When + ClientWriteResponse response = fga.write(request, options).get(); + + // Then + mockHttpClient + .verify() + .post(postPath) + .withHeader("another-header", "another-value") + .withHeader("test-header", "test-value-per-call") + .called(1); + assertEquals(200, response.getStatusCode()); + } + @Test public void check_withHeaders() throws Exception { // Given @@ -480,10 +515,8 @@ public void batchCheck_withHeaders() throws Exception { ._object(DEFAULT_OBJECT) .correlationId("cor-1"); ClientBatchCheckRequest request = new ClientBatchCheckRequest().checks(List.of(item)); - // Use HashMap instead of Map.of() to create a mutable map - Map headers = new java.util.HashMap<>(); - headers.put("test-header", "test-value-per-call"); - ClientBatchCheckOptions options = new ClientBatchCheckOptions().additionalHeaders(headers); + ClientBatchCheckOptions options = + new ClientBatchCheckOptions().additionalHeaders(Map.of("test-header", "test-value-per-call")); // When ClientBatchCheckResponse response = fga.batchCheck(request, options).join();