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 test_headers_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 overrideable default headers"""
331+ response_body = '{"allowed": true}'
332+ mock_request .return_value = mock_response (response_body , 200 )
333+
334+ # Test with headers that can override defaults (User-Agent)
335+ # Note: Accept and Content-Type are set by the API method and cannot be overridden
336+ override_headers = {
337+ "User-Agent" : "custom-user-agent" ,
338+ "x-custom-header" : "custom-value" ,
339+ "Authorization" : "Bearer custom-token" ,
340+ }
341+
342+ async with OpenFgaClient (self .configuration ) as fga_client :
343+ options = {
344+ "headers" : override_headers
345+ }
346+
347+ body = ClientCheckRequest (
348+ user = "user:test-user" ,
349+ relation = "viewer" ,
350+ object = "document:test-doc" ,
351+ )
352+
353+ await fga_client .check (body , options )
354+
355+ # Verify the request was made
356+ mock_request .assert_called_once ()
357+ call_args = mock_request .call_args
358+ headers = call_args .kwargs .get ("headers" , {})
359+
360+ # Check that overrideable custom headers work
361+ self .assertEqual (headers ["User-Agent" ], "custom-user-agent" )
362+ self .assertEqual (headers ["x-custom-header" ], "custom-value" )
363+ self .assertEqual (headers ["Authorization" ], "Bearer custom-token" )
364+
365+ # System headers are still set by the API method
366+ self .assertEqual (headers ["Accept" ], "application/json" )
367+ self .assertTrue ("Content-Type" in headers )
0 commit comments