Skip to content

Commit 2324a38

Browse files
author
Daniel Yeam
committed
Add comprehensive tests for per-request custom HTTP headers functionality
- Add core functionality tests for all API methods (check, write, read, etc.) - Add edge case tests for invalid inputs and boundary conditions - Add synchronous client compatibility tests - Add summary test demonstrating real-world usage patterns - Covers both async and sync clients with 1,270+ lines of test coverage Resolves #217
1 parent 89a39d1 commit 2324a38

File tree

4 files changed

+1271
-0
lines changed

4 files changed

+1271
-0
lines changed
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
"""
2+
Test edge cases and error scenarios for per-request custom HTTP headers functionality
3+
4+
This module tests edge cases, invalid inputs, and error scenarios for the
5+
per-request headers feature to ensure robust handling.
6+
"""
7+
8+
import json
9+
from unittest import IsolatedAsyncioTestCase
10+
from unittest.mock import ANY, patch
11+
12+
import urllib3
13+
14+
from openfga_sdk import rest
15+
from openfga_sdk.client import ClientConfiguration
16+
from openfga_sdk.client.client import OpenFgaClient, options_to_kwargs, set_heading_if_not_set
17+
from openfga_sdk.client.models.check_request import ClientCheckRequest
18+
19+
20+
store_id = "01YCP46JKYM8FJCQ37NMBYHE5X"
21+
auth_model_id = "01YCP46JKYM8FJCQ37NMBYHE6X"
22+
request_id = "x1y2z3"
23+
24+
25+
def http_mock_response(body, status):
26+
headers = urllib3.response.HTTPHeaderDict(
27+
{"content-type": "application/json", "Fga-Request-Id": request_id}
28+
)
29+
return urllib3.HTTPResponse(
30+
body.encode("utf-8"), headers, status, preload_content=False
31+
)
32+
33+
34+
def mock_response(body, status):
35+
obj = http_mock_response(body, status)
36+
return rest.RESTResponse(obj, obj.data)
37+
38+
39+
class TestPerRequestHeadersEdgeCases(IsolatedAsyncioTestCase):
40+
"""Test edge cases and error scenarios for per-request headers"""
41+
42+
def setUp(self):
43+
self.configuration = ClientConfiguration(
44+
api_url="http://api.fga.example",
45+
store_id=store_id,
46+
authorization_model_id=auth_model_id,
47+
)
48+
49+
def tearDown(self):
50+
pass
51+
52+
def test_options_to_kwargs_with_headers(self):
53+
"""Test options_to_kwargs function properly handles headers"""
54+
options = {
55+
"headers": {
56+
"x-test-header": "test-value",
57+
"x-another": "another-value"
58+
},
59+
"authorization_model_id": "test-model",
60+
"page_size": 25
61+
}
62+
63+
result = options_to_kwargs(options)
64+
65+
# Check that headers are converted to headers
66+
self.assertIn("headers", result)
67+
self.assertEqual(result["headers"]["x-test-header"], "test-value")
68+
self.assertEqual(result["headers"]["x-another"], "another-value")
69+
70+
# Check that other options are preserved
71+
self.assertEqual(result.get("page_size"), 25)
72+
73+
def test_options_to_kwargs_without_headers(self):
74+
"""Test options_to_kwargs function works without headers"""
75+
options = {
76+
"authorization_model_id": "test-model",
77+
"page_size": 25
78+
}
79+
80+
result = options_to_kwargs(options)
81+
82+
# Check that headers is not present when no headers option
83+
self.assertNotIn("headers", result)
84+
85+
# Check that other options are preserved
86+
self.assertEqual(result.get("page_size"), 25)
87+
88+
def test_options_to_kwargs_with_none(self):
89+
"""Test options_to_kwargs function handles None input"""
90+
result = options_to_kwargs(None)
91+
92+
# Should return empty dict
93+
self.assertEqual(result, {})
94+
95+
def test_options_to_kwargs_with_empty_dict(self):
96+
"""Test options_to_kwargs function handles empty dict input"""
97+
result = options_to_kwargs({})
98+
99+
# Should return empty dict
100+
self.assertEqual(result, {})
101+
102+
def test_set_heading_if_not_set_with_existing_headers(self):
103+
"""Test set_heading_if_not_set function with existing headers"""
104+
options = {
105+
"headers": {
106+
"x-existing": "existing-value"
107+
}
108+
}
109+
110+
result = set_heading_if_not_set(options, "x-new-header", "new-value")
111+
112+
# Check that new header was added
113+
self.assertEqual(result["headers"]["x-new-header"], "new-value")
114+
# Check that existing header is preserved
115+
self.assertEqual(result["headers"]["x-existing"], "existing-value")
116+
117+
def test_set_heading_if_not_set_without_headers(self):
118+
"""Test set_heading_if_not_set function when headers dict doesn't exist"""
119+
options = {
120+
"other_option": "value"
121+
}
122+
123+
result = set_heading_if_not_set(options, "x-new-header", "new-value")
124+
125+
# Check that headers dict was created and header was added
126+
self.assertIn("headers", result)
127+
self.assertEqual(result["headers"]["x-new-header"], "new-value")
128+
# Check that other options are preserved
129+
self.assertEqual(result["other_option"], "value")
130+
131+
def test_set_heading_if_not_set_with_none_options(self):
132+
"""Test set_heading_if_not_set function with None options"""
133+
result = set_heading_if_not_set(None, "x-new-header", "new-value")
134+
135+
# Check that options dict was created with headers
136+
self.assertIn("headers", result)
137+
self.assertEqual(result["headers"]["x-new-header"], "new-value")
138+
139+
def test_set_heading_if_not_set_header_already_exists(self):
140+
"""Test set_heading_if_not_set function when header already exists"""
141+
options = {
142+
"headers": {
143+
"x-existing": "original-value"
144+
}
145+
}
146+
147+
result = set_heading_if_not_set(options, "x-existing", "new-value")
148+
149+
# Check that original value is preserved (not overwritten)
150+
self.assertEqual(result["headers"]["x-existing"], "original-value")
151+
152+
def test_set_heading_if_not_set_with_invalidheaders_type(self):
153+
"""Test set_heading_if_not_set function with invalid headers type"""
154+
options = {
155+
"headers": "not-a-dict" # Invalid type
156+
}
157+
158+
result = set_heading_if_not_set(options, "x-new-header", "new-value")
159+
160+
# Function should create new headers dict, replacing the invalid one
161+
self.assertIsInstance(result["headers"], dict)
162+
self.assertEqual(result["headers"]["x-new-header"], "new-value")
163+
164+
@patch.object(rest.RESTClientObject, "request")
165+
async def testheaders_with_invalid_type_in_options(self, mock_request):
166+
"""Test that invalid headers type in options is handled gracefully"""
167+
response_body = '{"allowed": true}'
168+
mock_request.return_value = mock_response(response_body, 200)
169+
170+
# This should be handled gracefully - converted to dict or ignored
171+
options_with_invalidheaders = {
172+
"headers": "not-a-dict"
173+
}
174+
175+
async with OpenFgaClient(self.configuration) as fga_client:
176+
body = ClientCheckRequest(
177+
user="user:test-user",
178+
relation="viewer",
179+
object="document:test-doc",
180+
)
181+
182+
# This should not raise an exception
183+
await fga_client.check(body, options_with_invalidheaders)
184+
185+
# Verify the request was made
186+
mock_request.assert_called_once()
187+
188+
@patch.object(rest.RESTClientObject, "request")
189+
async def test_large_number_of_headers(self, mock_request):
190+
"""Test that a large number of headers is handled correctly"""
191+
response_body = '{"allowed": true}'
192+
mock_request.return_value = mock_response(response_body, 200)
193+
194+
# Create a large number of headers
195+
largeheaders = {f"x-header-{i}": f"value-{i}" for i in range(100)}
196+
197+
async with OpenFgaClient(self.configuration) as fga_client:
198+
options = {
199+
"headers": largeheaders
200+
}
201+
202+
body = ClientCheckRequest(
203+
user="user:test-user",
204+
relation="viewer",
205+
object="document:test-doc",
206+
)
207+
208+
await fga_client.check(body, options)
209+
210+
# Verify the request was made with all headers
211+
mock_request.assert_called_once()
212+
call_args = mock_request.call_args
213+
headers = call_args.kwargs.get("headers", {})
214+
215+
# Check that all headers were included
216+
self.assertEqual(len(headers), 100)
217+
for i in range(100):
218+
self.assertEqual(headers[f"x-header-{i}"], f"value-{i}")
219+
220+
@patch.object(rest.RESTClientObject, "request")
221+
async def test_unicode_headers(self, mock_request):
222+
"""Test that unicode characters in headers are handled correctly"""
223+
response_body = '{"allowed": true}'
224+
mock_request.return_value = mock_response(response_body, 200)
225+
226+
unicode_headers = {
227+
"x-unicode-header": "测试值", # Chinese characters
228+
"x-emoji-header": "🚀🔐", # Emojis
229+
"x-accented-header": "café-résumé", # Accented characters
230+
}
231+
232+
async with OpenFgaClient(self.configuration) as fga_client:
233+
options = {
234+
"headers": unicode_headers
235+
}
236+
237+
body = ClientCheckRequest(
238+
user="user:test-user",
239+
relation="viewer",
240+
object="document:test-doc",
241+
)
242+
243+
await fga_client.check(body, options)
244+
245+
# Verify the request was made with unicode headers
246+
mock_request.assert_called_once()
247+
call_args = mock_request.call_args
248+
headers = call_args.kwargs.get("headers", {})
249+
250+
# Check that unicode headers were included
251+
self.assertEqual(headers["x-unicode-header"], "测试值")
252+
self.assertEqual(headers["x-emoji-header"], "🚀🔐")
253+
self.assertEqual(headers["x-accented-header"], "café-résumé")
254+
255+
@patch.object(rest.RESTClientObject, "request")
256+
async def test_long_header_values(self, mock_request):
257+
"""Test that very long header values are handled correctly"""
258+
response_body = '{"allowed": true}'
259+
mock_request.return_value = mock_response(response_body, 200)
260+
261+
# Create a very long header value
262+
long_value = "x" * 10000 # 10KB header value
263+
264+
longheaders = {
265+
"x-long-header": long_value,
266+
"x-normal-header": "normal-value"
267+
}
268+
269+
async with OpenFgaClient(self.configuration) as fga_client:
270+
options = {
271+
"headers": longheaders
272+
}
273+
274+
body = ClientCheckRequest(
275+
user="user:test-user",
276+
relation="viewer",
277+
object="document:test-doc",
278+
)
279+
280+
await fga_client.check(body, options)
281+
282+
# Verify the request was made with long headers
283+
mock_request.assert_called_once()
284+
call_args = mock_request.call_args
285+
headers = call_args.kwargs.get("headers", {})
286+
287+
# Check that long header was included
288+
self.assertEqual(headers["x-long-header"], long_value)
289+
self.assertEqual(headers["x-normal-header"], "normal-value")
290+
291+
@patch.object(rest.RESTClientObject, "request")
292+
async def test_header_case_sensitivity(self, mock_request):
293+
"""Test that header case is preserved"""
294+
response_body = '{"allowed": true}'
295+
mock_request.return_value = mock_response(response_body, 200)
296+
297+
case_sensitiveheaders = {
298+
"X-Upper-Case": "upper-value",
299+
"x-lower-case": "lower-value",
300+
"X-Mixed-Case": "mixed-value",
301+
"x-WEIRD-cAsE": "weird-value"
302+
}
303+
304+
async with OpenFgaClient(self.configuration) as fga_client:
305+
options = {
306+
"headers": case_sensitiveheaders
307+
}
308+
309+
body = ClientCheckRequest(
310+
user="user:test-user",
311+
relation="viewer",
312+
object="document:test-doc",
313+
)
314+
315+
await fga_client.check(body, options)
316+
317+
# Verify the request was made with case-preserved headers
318+
mock_request.assert_called_once()
319+
call_args = mock_request.call_args
320+
headers = call_args.kwargs.get("headers", {})
321+
322+
# Check that header case was preserved
323+
self.assertEqual(headers["X-Upper-Case"], "upper-value")
324+
self.assertEqual(headers["x-lower-case"], "lower-value")
325+
self.assertEqual(headers["X-Mixed-Case"], "mixed-value")
326+
self.assertEqual(headers["x-WEIRD-cAsE"], "weird-value")
327+
328+
@patch.object(rest.RESTClientObject, "request")
329+
async def test_header_overrides_default_headers(self, mock_request):
330+
"""Test that custom headers can override any default headers if needed"""
331+
response_body = '{"allowed": true}'
332+
mock_request.return_value = mock_response(response_body, 200)
333+
334+
# Test with headers that might override defaults
335+
overrideheaders = {
336+
"User-Agent": "custom-user-agent",
337+
"Content-Type": "custom-content-type",
338+
"Accept": "custom-accept"
339+
}
340+
341+
async with OpenFgaClient(self.configuration) as fga_client:
342+
options = {
343+
"headers": overrideheaders
344+
}
345+
346+
body = ClientCheckRequest(
347+
user="user:test-user",
348+
relation="viewer",
349+
object="document:test-doc",
350+
)
351+
352+
await fga_client.check(body, options)
353+
354+
# Verify the request was made
355+
mock_request.assert_called_once()
356+
call_args = mock_request.call_args
357+
headers = call_args.kwargs.get("headers", {})
358+
359+
# Check that override headers were included
360+
# Note: The behavior depends on how the underlying HTTP client handles header precedence
361+
self.assertEqual(headers["User-Agent"], "custom-user-agent")
362+
self.assertEqual(headers["Content-Type"], "custom-content-type")
363+
self.assertEqual(headers["Accept"], "custom-accept")

0 commit comments

Comments
 (0)