From c5a9b11024e1be6b66cd8b8da026584611c71d1e Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Fri, 3 Oct 2025 15:29:58 -0500 Subject: [PATCH 1/5] fix(python-sdk): custom header precedence --- .../template/src/api_client.py.mustache | 3 +- .../template/src/client/client.py.mustache | 2 +- .../template/src/sync/api_client.py.mustache | 3 +- .../src/sync/client/client.py.mustache | 2 +- .../python/template/test/api_test.py.mustache | 116 ++++++++++++++++++ .../test/client/client_test.py.mustache | 99 ++++++++++++++- .../template/test/sync/api_test.py.mustache | 114 +++++++++++++++++ .../test/sync/client/client_test.py.mustache | 99 ++++++++++++++- 8 files changed, 430 insertions(+), 8 deletions(-) diff --git a/config/clients/python/template/src/api_client.py.mustache b/config/clients/python/template/src/api_client.py.mustache index fcdabf11d..ed1c81924 100644 --- a/config/clients/python/template/src/api_client.py.mustache +++ b/config/clients/python/template/src/api_client.py.mustache @@ -184,8 +184,7 @@ class ApiClient: start = float(time.time()) # header parameters - header_params = header_params or {} - header_params.update(self.default_headers) + header_params = {**self.default_headers, **(header_params or {})} if self.cookie: header_params['Cookie'] = self.cookie if header_params: diff --git a/config/clients/python/template/src/client/client.py.mustache b/config/clients/python/template/src/client/client.py.mustache index 0032c7464..446724247 100644 --- a/config/clients/python/template/src/client/client.py.mustache +++ b/config/clients/python/template/src/client/client.py.mustache @@ -78,7 +78,7 @@ def set_heading_if_not_set( _options["headers"] = {} if type(_options["headers"]) is dict: - if type(_options["headers"].get(name)) not in [int, str]: + if _options["headers"].get(name) is None: _options["headers"][name] = value return _options diff --git a/config/clients/python/template/src/sync/api_client.py.mustache b/config/clients/python/template/src/sync/api_client.py.mustache index 60e861279..a087d0070 100644 --- a/config/clients/python/template/src/sync/api_client.py.mustache +++ b/config/clients/python/template/src/sync/api_client.py.mustache @@ -170,8 +170,7 @@ class ApiClient: start = float(time.time()) # header parameters - header_params = header_params or {} - header_params.update(self.default_headers) + header_params = {**self.default_headers, **(header_params or {})} if self.cookie: header_params['Cookie'] = self.cookie if header_params: diff --git a/config/clients/python/template/src/sync/client/client.py.mustache b/config/clients/python/template/src/sync/client/client.py.mustache index f68e5ccfe..2aa9c4f28 100644 --- a/config/clients/python/template/src/sync/client/client.py.mustache +++ b/config/clients/python/template/src/sync/client/client.py.mustache @@ -78,7 +78,7 @@ def set_heading_if_not_set( _options["headers"] = {} if type(_options["headers"]) is dict: - if type(_options["headers"].get(name)) not in [int, str]: + if _options["headers"].get(name) is None: _options["headers"][name] = value return _options diff --git a/config/clients/python/template/test/api_test.py.mustache b/config/clients/python/template/test/api_test.py.mustache index f7206367d..c8d71bad6 100644 --- a/config/clients/python/template/test/api_test.py.mustache +++ b/config/clients/python/template/test/api_test.py.mustache @@ -2036,6 +2036,122 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): ) await api_client.close() + @patch.object(rest.RESTClientObject, "request") + async def test_check_custom_header_override_default_header(self, mock_request): + """Test case for per-request custom header overriding default header + + Per-request custom headers should override default headers with the same name + """ + + # First, mock the response + response_body = '{"allowed": true}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + async with openfga_sdk.ApiClient(configuration) as api_client: + # Set a default header + api_client.set_default_header("X-Custom-Header", "default-value") + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + # Make request with per-request custom header that should override the default + api_response = await api_instance.check( + body=body, + _headers={"X-Custom-Header": "per-request-value"}, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure the API was called with the per-request header value, not the default + expected_headers = urllib3.response.HTTPHeaderDict( + { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": "openfga-sdk python/0.9.6", + "X-Custom-Header": "per-request-value", # Should be the per-request value + } + ) + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/check", + headers=expected_headers, + query_params=[], + post_params=[], + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + } + }, + _preload_content=ANY, + _request_timeout=None, + ) + + @patch.object(rest.RESTClientObject, "request") + async def test_check_per_request_header_and_default_header_coexist( + self, mock_request + ): + """Test case for per-request custom header and default header coexisting + + Per-request custom headers should be merged with default headers + """ + + # First, mock the response + response_body = '{"allowed": true}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + async with openfga_sdk.ApiClient(configuration) as api_client: + # Set a default header + api_client.set_default_header("X-Default-Header", "default-value") + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + # Make request with per-request custom header (different from default) + api_response = await api_instance.check( + body=body, + _headers={"X-Per-Request-Header": "per-request-value"}, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure both headers are present in the request + expected_headers = urllib3.response.HTTPHeaderDict( + { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": "openfga-sdk python/0.9.6", + "X-Default-Header": "default-value", # Default header preserved + "X-Per-Request-Header": "per-request-value", # Per-request header added + } + ) + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/check", + headers=expected_headers, + query_params=[], + post_params=[], + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + } + }, + _preload_content=ANY, + _request_timeout=None, + ) if __name__ == "__main__": unittest.main() diff --git a/config/clients/python/template/test/client/client_test.py.mustache b/config/clients/python/template/test/client/client_test.py.mustache index 81107947c..ddc84713d 100644 --- a/config/clients/python/template/test/client/client_test.py.mustache +++ b/config/clients/python/template/test/client/client_test.py.mustache @@ -11,7 +11,7 @@ import urllib3 from {{packageName}} import rest from {{packageName}}.client import ClientConfiguration -from {{packageName}}.client.client import OpenFgaClient +from {{packageName}}.client.client import OpenFgaClient, set_heading_if_not_set from {{packageName}}.client.models.assertion import ClientAssertion from {{packageName}}.client.models.batch_check_item import ClientBatchCheckItem from {{packageName}}.client.models.batch_check_request import ClientBatchCheckRequest @@ -3263,3 +3263,100 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): authorization_model_id="abcd", ) self.assertRaises(FgaValidationException, configuration.is_valid) + + def test_set_heading_if_not_set_when_none_provided(self): + """Should set header when no options provided""" + result = set_heading_if_not_set(None, "X-Test-Header", "default-value") + + self.assertIsNotNone(result) + self.assertIn("headers", result) + self.assertEqual(result["headers"]["X-Test-Header"], "default-value") + + def test_set_heading_if_not_set_when_empty_options_provided(self): + """Should set header when empty options dict provided""" + result = set_heading_if_not_set({}, "X-Test-Header", "default-value") + + self.assertIn("headers", result) + self.assertEqual(result["headers"]["X-Test-Header"], "default-value") + + def test_set_heading_if_not_set_when_no_headers_in_options(self): + """Should set header when options dict has no headers key""" + options = {"page_size": 10} + result = set_heading_if_not_set(options, "X-Test-Header", "default-value") + + self.assertIn("headers", result) + self.assertEqual(result["headers"]["X-Test-Header"], "default-value") + self.assertEqual(result["page_size"], 10) + + def test_set_heading_if_not_set_when_headers_empty(self): + """Should set header when headers dict is empty""" + options = {"headers": {}} + result = set_heading_if_not_set(options, "X-Test-Header", "default-value") + + self.assertEqual(result["headers"]["X-Test-Header"], "default-value") + + def test_set_heading_if_not_set_does_not_override_existing_custom_header(self): + """Should NOT override when custom header already exists - this is the critical test for the bug fix""" + options = {"headers": {"X-Test-Header": "custom-value"}} + result = set_heading_if_not_set(options, "X-Test-Header", "default-value") + + # Custom header should be preserved, NOT overridden by default + self.assertEqual(result["headers"]["X-Test-Header"], "custom-value") + + def test_set_heading_if_not_set_preserves_other_headers_when_setting_new_header( + self, + ): + """Should preserve existing headers when setting a new one""" + options = {"headers": {"X-Existing-Header": "existing-value"}} + result = set_heading_if_not_set(options, "X-New-Header", "new-value") + + self.assertEqual(result["headers"]["X-Existing-Header"], "existing-value") + self.assertEqual(result["headers"]["X-New-Header"], "new-value") + + def test_set_heading_if_not_set_handles_integer_header_values(self): + """Should not override existing integer header values""" + options = {"headers": {"X-Retry-Count": 5}} + result = set_heading_if_not_set(options, "X-Retry-Count", 1) + + # Existing integer value should be preserved + self.assertEqual(result["headers"]["X-Retry-Count"], 5) + + def test_set_heading_if_not_set_handles_non_dict_headers_value(self): + """Should convert non-dict headers value to dict""" + options = {"headers": "invalid"} + result = set_heading_if_not_set(options, "X-Test-Header", "default-value") + + self.assertIsInstance(result["headers"], dict) + self.assertEqual(result["headers"]["X-Test-Header"], "default-value") + + def test_set_heading_if_not_set_does_not_mutate_when_header_exists(self): + """Should return same dict when header already exists""" + options = {"headers": {"X-Test-Header": "custom-value"}} + original_value = options["headers"]["X-Test-Header"] + + result = set_heading_if_not_set(options, "X-Test-Header", "default-value") + + # Should return the same modified dict + self.assertIs(result, options) + # Value should not have changed + self.assertEqual(result["headers"]["X-Test-Header"], original_value) + + def test_set_heading_if_not_set_multiple_headers_with_mixed_states(self): + """Should handle multiple headers, some existing and some new""" + options = { + "headers": { + "X-Custom-Header": "custom-value", + "X-Another-Header": "another-value", + } + } + + # Try to set a custom header (should not override) + result = set_heading_if_not_set(options, "X-Custom-Header", "default-value") + self.assertEqual(result["headers"]["X-Custom-Header"], "custom-value") + + # Try to set a new header (should be added) + result = set_heading_if_not_set(result, "X-New-Header", "new-value") + self.assertEqual(result["headers"]["X-New-Header"], "new-value") + + # Original headers should still exist + self.assertEqual(result["headers"]["X-Another-Header"], "another-value") diff --git a/config/clients/python/template/test/sync/api_test.py.mustache b/config/clients/python/template/test/sync/api_test.py.mustache index 4b90c56e6..41b7a1f9b 100644 --- a/config/clients/python/template/test/sync/api_test.py.mustache +++ b/config/clients/python/template/test/sync/api_test.py.mustache @@ -1910,6 +1910,120 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): _request_timeout=None, ) + @patch.object(rest.RESTClientObject, "request") + def test_check_custom_header_override_default_header(self, mock_request): + """Test case for per-request custom header overriding default header + + Per-request custom headers should override default headers with the same name + """ + + # First, mock the response + response_body = '{"allowed": true}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + # Set a default header + api_client.set_default_header("X-Custom-Header", "default-value") + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + # Make request with per-request custom header that should override the default + api_response = api_instance.check( + body=body, + _headers={"X-Custom-Header": "per-request-value"}, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure the API was called with the per-request header value, not the default + expected_headers = urllib3.response.HTTPHeaderDict( + { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": "openfga-sdk python/0.9.6", + "X-Custom-Header": "per-request-value", # Should be the per-request value + } + ) + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/check", + headers=expected_headers, + query_params=[], + post_params=[], + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + } + }, + _preload_content=ANY, + _request_timeout=None, + ) + + @patch.object(rest.RESTClientObject, "request") + def test_check_per_request_header_and_default_header_coexist(self, mock_request): + """Test case for per-request custom header and default header coexisting + + Per-request custom headers should be merged with default headers + """ + + # First, mock the response + response_body = '{"allowed": true}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + # Set a default header + api_client.set_default_header("X-Default-Header", "default-value") + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + # Make request with per-request custom header (different from default) + api_response = api_instance.check( + body=body, + _headers={"X-Per-Request-Header": "per-request-value"}, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure both headers are present in the request + expected_headers = urllib3.response.HTTPHeaderDict( + { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": "openfga-sdk python/0.9.6", + "X-Default-Header": "default-value", # Default header preserved + "X-Per-Request-Header": "per-request-value", # Per-request header added + } + ) + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/check", + headers=expected_headers, + query_params=[], + post_params=[], + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + } + }, + _preload_content=ANY, + _request_timeout=None, + ) if __name__ == "__main__": unittest.main() diff --git a/config/clients/python/template/test/sync/client/client_test.py.mustache b/config/clients/python/template/test/sync/client/client_test.py.mustache index 8cc7fb936..1fc1c5934 100644 --- a/config/clients/python/template/test/sync/client/client_test.py.mustache +++ b/config/clients/python/template/test/sync/client/client_test.py.mustache @@ -75,7 +75,7 @@ from {{packageName}}.models.write_authorization_model_response import ( WriteAuthorizationModelResponse, ) from {{packageName}}.sync import rest -from {{packageName}}.sync.client.client import OpenFgaClient +from {{packageName}}.sync.client.client import OpenFgaClient, set_heading_if_not_set store_id = "01YCP46JKYM8FJCQ37NMBYHE5X" @@ -3265,3 +3265,100 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): authorization_model_id="abcd", ) self.assertRaises(FgaValidationException, configuration.is_valid) + + def test_set_heading_if_not_set_when_none_provided(self): + """Should set header when no options provided""" + result = set_heading_if_not_set(None, "X-Test-Header", "default-value") + + self.assertIsNotNone(result) + self.assertIn("headers", result) + self.assertEqual(result["headers"]["X-Test-Header"], "default-value") + + def test_set_heading_if_not_set_when_empty_options_provided(self): + """Should set header when empty options dict provided""" + result = set_heading_if_not_set({}, "X-Test-Header", "default-value") + + self.assertIn("headers", result) + self.assertEqual(result["headers"]["X-Test-Header"], "default-value") + + def test_set_heading_if_not_set_when_no_headers_in_options(self): + """Should set header when options dict has no headers key""" + options = {"page_size": 10} + result = set_heading_if_not_set(options, "X-Test-Header", "default-value") + + self.assertIn("headers", result) + self.assertEqual(result["headers"]["X-Test-Header"], "default-value") + self.assertEqual(result["page_size"], 10) + + def test_set_heading_if_not_set_when_headers_empty(self): + """Should set header when headers dict is empty""" + options = {"headers": {}} + result = set_heading_if_not_set(options, "X-Test-Header", "default-value") + + self.assertEqual(result["headers"]["X-Test-Header"], "default-value") + + def test_set_heading_if_not_set_does_not_override_existing_custom_header(self): + """Should NOT override when custom header already exists - this is the critical test for the bug fix""" + options = {"headers": {"X-Test-Header": "custom-value"}} + result = set_heading_if_not_set(options, "X-Test-Header", "default-value") + + # Custom header should be preserved, NOT overridden by default + self.assertEqual(result["headers"]["X-Test-Header"], "custom-value") + + def test_set_heading_if_not_set_preserves_other_headers_when_setting_new_header( + self, + ): + """Should preserve existing headers when setting a new one""" + options = {"headers": {"X-Existing-Header": "existing-value"}} + result = set_heading_if_not_set(options, "X-New-Header", "new-value") + + self.assertEqual(result["headers"]["X-Existing-Header"], "existing-value") + self.assertEqual(result["headers"]["X-New-Header"], "new-value") + + def test_set_heading_if_not_set_handles_integer_header_values(self): + """Should not override existing integer header values""" + options = {"headers": {"X-Retry-Count": 5}} + result = set_heading_if_not_set(options, "X-Retry-Count", 1) + + # Existing integer value should be preserved + self.assertEqual(result["headers"]["X-Retry-Count"], 5) + + def test_set_heading_if_not_set_handles_non_dict_headers_value(self): + """Should convert non-dict headers value to dict""" + options = {"headers": "invalid"} + result = set_heading_if_not_set(options, "X-Test-Header", "default-value") + + self.assertIsInstance(result["headers"], dict) + self.assertEqual(result["headers"]["X-Test-Header"], "default-value") + + def test_set_heading_if_not_set_does_not_mutate_when_header_exists(self): + """Should return same dict when header already exists""" + options = {"headers": {"X-Test-Header": "custom-value"}} + original_value = options["headers"]["X-Test-Header"] + + result = set_heading_if_not_set(options, "X-Test-Header", "default-value") + + # Should return the same modified dict + self.assertIs(result, options) + # Value should not have changed + self.assertEqual(result["headers"]["X-Test-Header"], original_value) + + def test_set_heading_if_not_set_multiple_headers_with_mixed_states(self): + """Should handle multiple headers, some existing and some new""" + options = { + "headers": { + "X-Custom-Header": "custom-value", + "X-Another-Header": "another-value", + } + } + + # Try to set a custom header (should not override) + result = set_heading_if_not_set(options, "X-Custom-Header", "default-value") + self.assertEqual(result["headers"]["X-Custom-Header"], "custom-value") + + # Try to set a new header (should be added) + result = set_heading_if_not_set(result, "X-New-Header", "new-value") + self.assertEqual(result["headers"]["X-New-Header"], "new-value") + + # Original headers should still exist + self.assertEqual(result["headers"]["X-Another-Header"], "another-value") From 18b61c51a598d449f0fc3ec34d86a2e3ca08a3df Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Fri, 3 Oct 2025 16:09:10 -0500 Subject: [PATCH 2/5] test: expand code coverage for changes --- .../test/client/client_test.py.mustache | 261 ++++++++++++++++ .../test/sync/client/client_test.py.mustache | 282 ++++++++++++++++++ 2 files changed, 543 insertions(+) diff --git a/config/clients/python/template/test/client/client_test.py.mustache b/config/clients/python/template/test/client/client_test.py.mustache index ddc84713d..19c9865fb 100644 --- a/config/clients/python/template/test/client/client_test.py.mustache +++ b/config/clients/python/template/test/client/client_test.py.mustache @@ -3360,3 +3360,264 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # Original headers should still exist self.assertEqual(result["headers"]["X-Another-Header"], "another-value") + + def test_set_heading_if_not_set_with_empty_string_value(self): + """Test that empty string values in custom headers are preserved and not overridden.""" + options = {"headers": {"X-Custom-Header": ""}} + result = set_heading_if_not_set(options, "X-Custom-Header", "default-value") + self.assertEqual(result["headers"]["X-Custom-Header"], "") + + def test_set_heading_if_not_set_with_unicode_characters(self): + """Test that headers with Unicode characters are handled correctly.""" + options = {"headers": {"X-Custom-Header": "日本語"}} + result = set_heading_if_not_set(options, "X-Custom-Header", "default-value") + self.assertEqual(result["headers"]["X-Custom-Header"], "日本語") + + # Test setting a new header with Unicode + result = set_heading_if_not_set({}, "X-Unicode-Header", "🔒") + self.assertEqual(result["headers"]["X-Unicode-Header"], "🔒") + + def test_set_heading_if_not_set_with_special_characters(self): + """Test that headers with special characters are handled correctly.""" + options = {"headers": {"X-Custom-Header": "value-with-special!@#$%^&*()_+"}} + result = set_heading_if_not_set(options, "X-Custom-Header", "default") + self.assertEqual(result["headers"]["X-Custom-Header"], "value-with-special!@#$%^&*()_+") + + def test_set_heading_if_not_set_case_sensitivity(self): + """Test that header names are treated as case-sensitive by the helper function.""" + options = {"headers": {"x-custom-header": "lowercase"}} + result = set_heading_if_not_set(options, "X-Custom-Header", "uppercase") + self.assertEqual(result["headers"]["X-Custom-Header"], "uppercase") + self.assertEqual(result["headers"]["x-custom-header"], "lowercase") + + @patch.object(rest.RESTClientObject, "request") + async def test_check_with_custom_headers_override_defaults(self, mock_request): + """Test that custom headers in options override default headers for check API.""" + response_body = '{"allowed": true, "resolution": "1234"}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" + + async with OpenFgaClient(configuration) as api_client: + custom_options = {"headers": {"X-Custom-Request-Id": "custom-request-123"}} + api_response = await api_client.check(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + self.assertEqual(headers.get("X-Custom-Request-Id"), "custom-request-123") + self.assertIsInstance(api_response, CheckResponse) + + @patch.object(rest.RESTClientObject, "request") + async def test_write_with_custom_headers(self, mock_request): + """Test that custom headers work correctly with write API.""" + response_body = '{"writes": [], "deletes": []}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientWriteRequest( + writes=[ + ClientTuple( + object="document:budget", + relation="reader", + user="user:anne", + ) + ] + ) + + configuration = self.configuration + configuration.store_id = store_id + + async with OpenFgaClient(configuration) as api_client: + custom_options = { + "headers": {"X-Trace-Id": "trace-xyz-789"}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + } + await api_client.write(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + self.assertEqual(headers.get("X-Trace-Id"), "trace-xyz-789") + + @patch.object(rest.RESTClientObject, "request") + async def test_expand_with_custom_headers(self, mock_request): + """Test that custom headers are passed correctly in expand API calls.""" + response_body = '{"tree": {"root": {"name": "document:1#viewer", "leaf": {"users": {"users": ["user:anne"]}}}}}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientExpandRequest( + object="document:1", + relation="viewer", + ) + + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" + + async with OpenFgaClient(configuration) as api_client: + custom_options = {"headers": {"X-Expand-Id": "expand-456"}} + api_response = await api_client.expand(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + self.assertEqual(headers.get("X-Expand-Id"), "expand-456") + self.assertIsInstance(api_response, ExpandResponse) + + @patch.object(rest.RESTClientObject, "request") + async def test_list_objects_with_custom_headers(self, mock_request): + """Test that custom headers are passed correctly in list_objects API calls.""" + response_body = '{"objects": ["document:1", "document:2"]}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientListObjectsRequest( + type="document", + relation="viewer", + user="user:anne", + ) + + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" + + async with OpenFgaClient(configuration) as api_client: + custom_options = {"headers": {"X-List-Objects-Id": "list-obj-999"}} + api_response = await api_client.list_objects(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + self.assertEqual(headers.get("X-List-Objects-Id"), "list-obj-999") + self.assertIsInstance(api_response, ListObjectsResponse) + + @patch.object(rest.RESTClientObject, "request") + async def test_list_users_with_custom_headers(self, mock_request): + """Test that custom headers are passed correctly in list_users API calls.""" + response_body = '{"users": [{"object": {"type": "user", "id": "anne"}}, {"object": {"type": "user", "id": "bob"}}]}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientListUsersRequest( + object=FgaObject(type="document", id="1"), + relation="viewer", + user_filters=[{"type": "user"}], + ) + + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" + + async with OpenFgaClient(configuration) as api_client: + custom_options = {"headers": {"X-List-Users-Id": "list-users-777"}} + api_response = await api_client.list_users(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + self.assertEqual(headers.get("X-List-Users-Id"), "list-users-777") + self.assertIsInstance(api_response, ListUsersResponse) + + @patch.object(rest.RESTClientObject, "request") + async def test_multiple_api_calls_with_different_custom_headers(self, mock_request): + """Test that different custom headers can be used for different API calls.""" + + def mock_side_effect(*args, **kwargs): + path = args[1] + if "check" in path: + return mock_response('{"allowed": true, "resolution": "1234"}', 200) + elif "expand" in path: + return mock_response( + '{"tree": {"root": {"name": "document:1#viewer", "leaf": {"users": {"users": ["user:anne"]}}}}}', + 200, + ) + return mock_response("{}", 200) + + mock_request.side_effect = mock_side_effect + + check_body = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + + expand_body = ClientExpandRequest( + object="document:1", + relation="viewer", + ) + + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" + + async with OpenFgaClient(configuration) as api_client: + # First call with custom header 1 + check_options = {"headers": {"X-Request-Id": "check-request-111"}} + check_response = await api_client.check(body=check_body, options=check_options) + + # Second call with custom header 2 + expand_options = {"headers": {"X-Request-Id": "expand-request-222"}} + expand_response = await api_client.expand(body=expand_body, options=expand_options) + + # Verify first call had correct header + first_call_args = mock_request.call_args_list[0] + first_headers = first_call_args[1]["headers"] + self.assertEqual(first_headers.get("X-Request-Id"), "check-request-111") + + # Verify second call had correct header + second_call_args = mock_request.call_args_list[1] + second_headers = second_call_args[1]["headers"] + self.assertEqual(second_headers.get("X-Request-Id"), "expand-request-222") + + self.assertIsInstance(check_response, CheckResponse) + self.assertIsInstance(expand_response, ExpandResponse) + + @patch.object(rest.RESTClientObject, "request") + async def test_client_batch_check_with_custom_headers(self, mock_request): + """Test that custom headers work correctly in batch check operations.""" + + def mock_side_effect(*args, **kwargs): + body = kwargs.get("body", {}) + user = body.get("tuple_key", {}).get("user", "") + if user == "user:anne": + return mock_response('{"allowed": true, "resolution": "1234"}', 200) + elif user == "user:bob": + return mock_response('{"allowed": false, "resolution": "5678"}', 200) + return mock_response('{"allowed": false, "resolution": "0000"}', 200) + + mock_request.side_effect = mock_side_effect + + body = [ + ClientCheckRequest( + object="document:budget", + relation="reader", + user="user:anne", + ), + ClientCheckRequest( + object="document:roadmap", + relation="writer", + user="user:bob", + ), + ] + + configuration = self.configuration + configuration.store_id = store_id + + async with OpenFgaClient(configuration) as api_client: + custom_options = { + "headers": {"X-Batch-Id": "batch-xyz-123"}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + } + api_response = await api_client.client_batch_check( + body=body, options=custom_options + ) + + # Verify all calls had the custom header + for call_args in mock_request.call_args_list: + headers = call_args[1]["headers"] + self.assertEqual(headers.get("X-Batch-Id"), "batch-xyz-123") + + self.assertEqual(len(api_response), 2) + self.assertTrue(api_response[0].allowed) + self.assertFalse(api_response[1].allowed) diff --git a/config/clients/python/template/test/sync/client/client_test.py.mustache b/config/clients/python/template/test/sync/client/client_test.py.mustache index 1fc1c5934..4859f0265 100644 --- a/config/clients/python/template/test/sync/client/client_test.py.mustache +++ b/config/clients/python/template/test/sync/client/client_test.py.mustache @@ -3362,3 +3362,285 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # Original headers should still exist self.assertEqual(result["headers"]["X-Another-Header"], "another-value") + + def test_set_heading_if_not_set_with_empty_string_value(self): + """Test that empty string values in custom headers are preserved and not overridden.""" + options = {"headers": {"X-Custom-Header": ""}} + result = set_heading_if_not_set(options, "X-Custom-Header", "default-value") + # Empty string should be preserved, not overridden + self.assertEqual(result["headers"]["X-Custom-Header"], "") + + def test_set_heading_if_not_set_with_unicode_characters(self): + """Test that headers with Unicode characters are handled correctly.""" + options = {"headers": {"X-Custom-Header": "日本語"}} + result = set_heading_if_not_set(options, "X-Custom-Header", "default-value") + self.assertEqual(result["headers"]["X-Custom-Header"], "日本語") + + # Test setting a new header with Unicode + result = set_heading_if_not_set({}, "X-Unicode-Header", "🔒") + self.assertEqual(result["headers"]["X-Unicode-Header"], "🔒") + + def test_set_heading_if_not_set_with_special_characters(self): + """Test that headers with special characters are handled correctly.""" + options = {"headers": {"X-Custom-Header": "value-with-special!@#$%^&*()_+"}} + result = set_heading_if_not_set(options, "X-Custom-Header", "default") + self.assertEqual( + result["headers"]["X-Custom-Header"], "value-with-special!@#$%^&*()_+" + ) + + def test_set_heading_if_not_set_case_sensitivity(self): + """Test that header names are treated as case-sensitive by the helper function.""" + options = {"headers": {"x-custom-header": "lowercase"}} + # Different case - should add new header + result = set_heading_if_not_set(options, "X-Custom-Header", "uppercase") + self.assertEqual(result["headers"]["X-Custom-Header"], "uppercase") + self.assertEqual(result["headers"]["x-custom-header"], "lowercase") + + @patch.object(rest.RESTClientObject, "request") + def test_check_with_custom_headers_override_defaults(self, mock_request): + """Test that custom headers in options override default headers for check API.""" + response_body = '{"allowed": true, "resolution": "1234"}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" + + with OpenFgaClient(configuration) as api_client: + # Add custom header that should override any defaults + custom_options = { + "headers": {"X-Custom-Request-Id": "custom-request-123"} + } + api_response = api_client.check(body=body, options=custom_options) + + # Verify the API was called and extract the headers + call_args = mock_request.call_args + headers = call_args[1]["headers"] + + # Verify custom header is present + self.assertEqual(headers.get("X-Custom-Request-Id"), "custom-request-123") + self.assertIsInstance(api_response, CheckResponse) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_write_with_custom_headers(self, mock_request): + """Test that custom headers work correctly with write API.""" + response_body = '{"writes": [], "deletes": []}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientWriteRequest( + writes=[ + ClientTuple( + object="document:budget", + relation="reader", + user="user:anne", + ) + ] + ) + + configuration = self.configuration + configuration.store_id = store_id + + with OpenFgaClient(configuration) as api_client: + custom_options = { + "headers": {"X-Trace-Id": "trace-xyz-789"}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + } + api_client.write(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + + self.assertEqual(headers.get("X-Trace-Id"), "trace-xyz-789") + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_expand_with_custom_headers(self, mock_request): + """Test that custom headers work correctly with expand API.""" + response_body = '{"tree": {"root": {"name": "document:budget#viewer", "leaf": {"users": {"users": ["user:anne"]}}}}}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientExpandRequest( + object="document:budget", + relation="viewer", + ) + + configuration = self.configuration + configuration.store_id = store_id + + with OpenFgaClient(configuration) as api_client: + custom_options = { + "headers": {"X-Correlation-Id": "corr-abc-123"}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + } + api_response = api_client.expand(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + + self.assertEqual(headers.get("X-Correlation-Id"), "corr-abc-123") + self.assertIsInstance(api_response, ExpandResponse) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_list_objects_with_custom_headers(self, mock_request): + """Test that custom headers work correctly with list_objects API.""" + response_body = '{"objects": ["document:budget", "document:roadmap"]}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientListObjectsRequest( + type="document", + relation="viewer", + user="user:anne", + ) + + configuration = self.configuration + configuration.store_id = store_id + + with OpenFgaClient(configuration) as api_client: + custom_options = { + "headers": {"X-Session-Id": "session-456"}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + } + api_response = api_client.list_objects(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + + self.assertEqual(headers.get("X-Session-Id"), "session-456") + self.assertIsInstance(api_response, ListObjectsResponse) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_list_users_with_custom_headers(self, mock_request): + """Test that custom headers work correctly with list_users API.""" + response_body = '{"users": [{"object": {"type": "user", "id": "anne"}}]}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientListUsersRequest( + object=FgaObject(type="document", id="budget"), + relation="viewer", + user_filters=[UserTypeFilter(type="user")], + ) + + configuration = self.configuration + configuration.store_id = store_id + + with OpenFgaClient(configuration) as api_client: + custom_options = { + "headers": {"X-Client-Version": "1.2.3"}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + } + api_response = api_client.list_users(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + + self.assertEqual(headers.get("X-Client-Version"), "1.2.3") + self.assertIsInstance(api_response, ListUsersResponse) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_multiple_api_calls_with_different_custom_headers(self, mock_request): + """Test that different custom headers can be used for different API calls.""" + check_response = '{"allowed": true, "resolution": "1234"}' + expand_response = '{"tree": {"root": {"name": "document:budget#viewer", "leaf": {"users": {"users": ["user:anne"]}}}}}' + + mock_request.side_effect = [ + mock_response(check_response, 200), + mock_response(expand_response, 200), + ] + + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" + + with OpenFgaClient(configuration) as api_client: + # First API call with custom header 1 + check_body = ClientCheckRequest( + object="document:budget", + relation="reader", + user="user:anne", + ) + api_client.check( + body=check_body, + options={"headers": {"X-Request-Type": "check-call"}}, + ) + + # Second API call with custom header 2 + expand_body = ClientExpandRequest( + object="document:budget", + relation="viewer", + ) + api_client.expand( + body=expand_body, + options={"headers": {"X-Request-Type": "expand-call"}}, + ) + + # Verify first call had the check header + first_call_headers = mock_request.call_args_list[0][1]["headers"] + self.assertEqual(first_call_headers.get("X-Request-Type"), "check-call") + + # Verify second call had the expand header + second_call_headers = mock_request.call_args_list[1][1]["headers"] + self.assertEqual(second_call_headers.get("X-Request-Type"), "expand-call") + + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_client_batch_check_with_custom_headers(self, mock_request): + """Test that custom headers work correctly in batch check operations.""" + + def mock_side_effect(*args, **kwargs): + body = kwargs.get("body", {}) + user = body.get("tuple_key", {}).get("user", "") + if user == "user:anne": + return mock_response('{"allowed": true, "resolution": "1234"}', 200) + elif user == "user:bob": + return mock_response('{"allowed": false, "resolution": "5678"}', 200) + return mock_response('{"allowed": false, "resolution": "0000"}', 200) + + mock_request.side_effect = mock_side_effect + + body = [ + ClientCheckRequest( + object="document:budget", + relation="reader", + user="user:anne", + ), + ClientCheckRequest( + object="document:roadmap", + relation="writer", + user="user:bob", + ), + ] + + configuration = self.configuration + configuration.store_id = store_id + + with OpenFgaClient(configuration) as api_client: + custom_options = { + "headers": {"X-Batch-Id": "batch-xyz-123"}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + } + api_response = api_client.client_batch_check( + body=body, options=custom_options + ) + + # Verify all calls had the custom header + for call_args in mock_request.call_args_list: + headers = call_args[1]["headers"] + self.assertEqual(headers.get("X-Batch-Id"), "batch-xyz-123") + + # Verify responses are correct + self.assertEqual(len(api_response), 2) + self.assertTrue(api_response[0].allowed) + self.assertFalse(api_response[1].allowed) + + api_client.close() From 0c56b64890bc2a798b53e9c0e1e40d7b64413f7f Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Fri, 3 Oct 2025 16:17:53 -0500 Subject: [PATCH 3/5] test: replace hard coded user-agent strings --- .../python/template/test/api_test.py.mustache | 12 ++++++------ .../python/template/test/sync/api_test.py.mustache | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/clients/python/template/test/api_test.py.mustache b/config/clients/python/template/test/api_test.py.mustache index c8d71bad6..69b63c1fa 100644 --- a/config/clients/python/template/test/api_test.py.mustache +++ b/config/clients/python/template/test/api_test.py.mustache @@ -1770,7 +1770,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/{{packageVersion}}", + "User-Agent": "{{packageName}} python/{{packageVersion}}", "Authorization": "Bearer TOKEN1", } ) @@ -1824,7 +1824,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/{{packageVersion}}", + "User-Agent": "{{packageName}} python/{{packageVersion}}", "Custom Header": "custom value", } ) @@ -2049,7 +2049,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - async with openfga_sdk.ApiClient(configuration) as api_client: + async with {{packageName}}.ApiClient(configuration) as api_client: # Set a default header api_client.set_default_header("X-Custom-Header", "default-value") api_instance = open_fga_api.OpenFgaApi(api_client) @@ -2072,7 +2072,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/0.9.6", + "User-Agent": "{{packageName}} python/{{packageVersion}}", "X-Custom-Header": "per-request-value", # Should be the per-request value } ) @@ -2108,7 +2108,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - async with openfga_sdk.ApiClient(configuration) as api_client: + async with {{packageName}}.ApiClient(configuration) as api_client: # Set a default header api_client.set_default_header("X-Default-Header", "default-value") api_instance = open_fga_api.OpenFgaApi(api_client) @@ -2131,7 +2131,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/0.9.6", + "User-Agent": "{{packageName}} python/{{packageVersion}}", "X-Default-Header": "default-value", # Default header preserved "X-Per-Request-Header": "per-request-value", # Per-request header added } diff --git a/config/clients/python/template/test/sync/api_test.py.mustache b/config/clients/python/template/test/sync/api_test.py.mustache index 41b7a1f9b..9b2f71c47 100644 --- a/config/clients/python/template/test/sync/api_test.py.mustache +++ b/config/clients/python/template/test/sync/api_test.py.mustache @@ -1835,7 +1835,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/{{packageVersion}}", + "User-Agent": "{{packageName}} python/{{packageVersion}}", "Authorization": "Bearer TOKEN1", } ) @@ -1889,7 +1889,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/{{packageVersion}}", + "User-Agent": "{{packageName}} python/{{packageVersion}}", "Custom Header": "custom value", } ) @@ -1946,7 +1946,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/0.9.6", + "User-Agent": "{{packageName}} python/{{packageVersion}}", "X-Custom-Header": "per-request-value", # Should be the per-request value } ) @@ -2003,7 +2003,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/0.9.6", + "User-Agent": "{{packageName}} python/{{packageVersion}}", "X-Default-Header": "default-value", # Default header preserved "X-Per-Request-Header": "per-request-value", # Per-request header added } From 9ddca8b22f18394bc3005a21340193d70ce239cb Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Fri, 3 Oct 2025 16:20:10 -0500 Subject: [PATCH 4/5] test: replace hard coded user-agent strings --- config/clients/python/template/test/api_test.py.mustache | 8 ++++---- .../python/template/test/sync/api_test.py.mustache | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/config/clients/python/template/test/api_test.py.mustache b/config/clients/python/template/test/api_test.py.mustache index 69b63c1fa..69d2842d2 100644 --- a/config/clients/python/template/test/api_test.py.mustache +++ b/config/clients/python/template/test/api_test.py.mustache @@ -1770,7 +1770,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "{{packageName}} python/{{packageVersion}}", + "User-Agent": "openfga-sdk python/{{packageVersion}}", "Authorization": "Bearer TOKEN1", } ) @@ -1824,7 +1824,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "{{packageName}} python/{{packageVersion}}", + "User-Agent": "openfga-sdk python/{{packageVersion}}", "Custom Header": "custom value", } ) @@ -2072,7 +2072,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "{{packageName}} python/{{packageVersion}}", + "User-Agent": "openfga-sdk python/{{packageVersion}}", "X-Custom-Header": "per-request-value", # Should be the per-request value } ) @@ -2131,7 +2131,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "{{packageName}} python/{{packageVersion}}", + "User-Agent": "openfga-sdk python/{{packageVersion}}", "X-Default-Header": "default-value", # Default header preserved "X-Per-Request-Header": "per-request-value", # Per-request header added } diff --git a/config/clients/python/template/test/sync/api_test.py.mustache b/config/clients/python/template/test/sync/api_test.py.mustache index 9b2f71c47..2ef3f4f5f 100644 --- a/config/clients/python/template/test/sync/api_test.py.mustache +++ b/config/clients/python/template/test/sync/api_test.py.mustache @@ -1835,7 +1835,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "{{packageName}} python/{{packageVersion}}", + "User-Agent": "openfga-sdk python/{{packageVersion}}", "Authorization": "Bearer TOKEN1", } ) @@ -1889,7 +1889,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "{{packageName}} python/{{packageVersion}}", + "User-Agent": "openfga-sdk python/{{packageVersion}}", "Custom Header": "custom value", } ) @@ -1946,7 +1946,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "{{packageName}} python/{{packageVersion}}", + "User-Agent": "openfga-sdk python/{{packageVersion}}", "X-Custom-Header": "per-request-value", # Should be the per-request value } ) @@ -2003,7 +2003,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "{{packageName}} python/{{packageVersion}}", + "User-Agent": "openfga-sdk python/{{packageVersion}}", "X-Default-Header": "default-value", # Default header preserved "X-Per-Request-Header": "per-request-value", # Per-request header added } From 6f50aaddece1281f03e4adefbb397dbb60d904a3 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Fri, 3 Oct 2025 17:02:38 -0500 Subject: [PATCH 5/5] test: add tests per @rhamzeh feedback --- .../test/client/client_test.py.mustache | 50 +++++++++++++++++++ .../test/sync/client/client_test.py.mustache | 49 ++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/config/clients/python/template/test/client/client_test.py.mustache b/config/clients/python/template/test/client/client_test.py.mustache index 19c9865fb..b58da3bcb 100644 --- a/config/clients/python/template/test/client/client_test.py.mustache +++ b/config/clients/python/template/test/client/client_test.py.mustache @@ -3361,6 +3361,26 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # Original headers should still exist self.assertEqual(result["headers"]["X-Another-Header"], "another-value") + def test_set_heading_if_not_set_two_defaults_two_customs_one_override(self): + """Test setting two default headers when two custom headers exist, with one custom overriding one default""" + # Start with two custom headers + options = { + "headers": { + "X-Request-ID": "my-custom-request-id", # This should override the default + "X-Tenant-ID": "tenant-123", # This is custom-only + } + } + + # Try to set two default headers + result = set_heading_if_not_set(options, "X-SDK-Version", "1.0.0") + result = set_heading_if_not_set(result, "X-Request-ID", "default-uuid") + + # Verify all four headers exist with correct values + self.assertEqual(result["headers"]["X-SDK-Version"], "1.0.0") # Default was set + self.assertEqual(result["headers"]["X-Request-ID"], "my-custom-request-id") # Custom overrode default + self.assertEqual(result["headers"]["X-Tenant-ID"], "tenant-123") # Custom preserved + self.assertEqual(len(result["headers"]), 3) # Exactly 3 headers + def test_set_heading_if_not_set_with_empty_string_value(self): """Test that empty string values in custom headers are preserved and not overridden.""" options = {"headers": {"X-Custom-Header": ""}} @@ -3390,6 +3410,36 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(result["headers"]["X-Custom-Header"], "uppercase") self.assertEqual(result["headers"]["x-custom-header"], "lowercase") + @patch.object(rest.RESTClientObject, "request") + async def test_content_type_cannot_be_overridden_by_custom_headers( + self, mock_request + ): + """Test that Content-Type header cannot be overridden by custom headers.""" + response_body = '{"allowed": true, "resolution": "1234"}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" + + async with OpenFgaClient(configuration) as api_client: + # Try to override Content-Type with a custom value + custom_options = {"headers": {"Content-Type": "text/plain"}} + await api_client.check(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + + # Content-Type should be application/json, NOT the custom text/plain + self.assertEqual(headers.get("Content-Type"), "application/json") + self.assertNotEqual(headers.get("Content-Type"), "text/plain") + @patch.object(rest.RESTClientObject, "request") async def test_check_with_custom_headers_override_defaults(self, mock_request): """Test that custom headers in options override default headers for check API.""" diff --git a/config/clients/python/template/test/sync/client/client_test.py.mustache b/config/clients/python/template/test/sync/client/client_test.py.mustache index 4859f0265..0f4e62b54 100644 --- a/config/clients/python/template/test/sync/client/client_test.py.mustache +++ b/config/clients/python/template/test/sync/client/client_test.py.mustache @@ -3363,6 +3363,26 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # Original headers should still exist self.assertEqual(result["headers"]["X-Another-Header"], "another-value") + def test_set_heading_if_not_set_two_defaults_two_customs_one_override(self): + """Test setting two default headers when two custom headers exist, with one custom overriding one default""" + # Start with two custom headers + options = { + "headers": { + "X-Request-ID": "my-custom-request-id", # This should override the default + "X-Tenant-ID": "tenant-123", # This is custom-only + } + } + + # Try to set two default headers + result = set_heading_if_not_set(options, "X-SDK-Version", "1.0.0") + result = set_heading_if_not_set(result, "X-Request-ID", "default-uuid") + + # Verify all four headers exist with correct values + self.assertEqual(result["headers"]["X-SDK-Version"], "1.0.0") # Default was set + self.assertEqual(result["headers"]["X-Request-ID"], "my-custom-request-id") # Custom overrode default + self.assertEqual(result["headers"]["X-Tenant-ID"], "tenant-123") # Custom preserved + self.assertEqual(len(result["headers"]), 3) # Exactly 3 headers + def test_set_heading_if_not_set_with_empty_string_value(self): """Test that empty string values in custom headers are preserved and not overridden.""" options = {"headers": {"X-Custom-Header": ""}} @@ -3396,6 +3416,35 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(result["headers"]["X-Custom-Header"], "uppercase") self.assertEqual(result["headers"]["x-custom-header"], "lowercase") + @patch.object(rest.RESTClientObject, "request") + def test_content_type_cannot_be_overridden_by_custom_headers(self, mock_request): + """Test that Content-Type header cannot be overridden by custom headers.""" + response_body = '{"allowed": true, "resolution": "1234"}' + mock_request.return_value = mock_response(response_body, 200) + + body = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" + + with OpenFgaClient(configuration) as api_client: + # Try to override Content-Type with a custom value + custom_options = {"headers": {"Content-Type": "text/plain"}} + api_client.check(body=body, options=custom_options) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + + # Content-Type should be application/json, NOT the custom text/plain + self.assertEqual(headers.get("Content-Type"), "application/json") + self.assertNotEqual(headers.get("Content-Type"), "text/plain") + api_client.close() + @patch.object(rest.RESTClientObject, "request") def test_check_with_custom_headers_override_defaults(self, mock_request): """Test that custom headers in options override default headers for check API."""