@@ -46,12 +46,18 @@ def _assert_expected_lang(self, accept_language, expected_lang):
4646 self .assertEqual (response .content , expected_lang )
4747
4848 def test_call (self ):
49+ """Test basic endpoint call returns expected JSON response."""
4950 route = "/fastapi_demo/demo/"
5051 response = self .url_open (route )
5152 self .assertEqual (response .status_code , 200 )
5253 self .assertEqual (response .content , b'{"Hello":"World"}' )
5354
5455 def test_lang (self ):
56+ """Test language negotiation based on Accept-Language header.
57+
58+ Verifies that the correct language is selected based on the quality
59+ values and available languages in the system.
60+ """
5561 self ._assert_expected_lang ("fr,en;q=0.7,en-GB;q=0.3" , b'"fr_BE"' )
5662 self ._assert_expected_lang ("en,fr;q=0.7,en-GB;q=0.3" , b'"en_US"' )
5763 self ._assert_expected_lang ("fr-FR,en;q=0.7,en-GB;q=0.3" , b'"fr_BE"' )
@@ -103,6 +109,7 @@ def assert_exception_processed(
103109 self .assertEqual (response .status_code , expected_status_code )
104110
105111 def test_user_error (self ) -> None :
112+ """Test that UserError exceptions are properly handled and return 400."""
106113 self .assert_exception_processed (
107114 exception_type = DemoExceptionType .user_error ,
108115 error_message = "test" ,
@@ -111,6 +118,7 @@ def test_user_error(self) -> None:
111118 )
112119
113120 def test_validation_error (self ) -> None :
121+ """Test that ValidationError exceptions are properly handled and return 400."""
114122 self .assert_exception_processed (
115123 exception_type = DemoExceptionType .validation_error ,
116124 error_message = "test" ,
@@ -119,6 +127,7 @@ def test_validation_error(self) -> None:
119127 )
120128
121129 def test_bare_exception (self ) -> None :
130+ """Test that unhandled exceptions return 500 with generic error message."""
122131 self .assert_exception_processed (
123132 exception_type = DemoExceptionType .bare_exception ,
124133 error_message = "test" ,
@@ -127,6 +136,7 @@ def test_bare_exception(self) -> None:
127136 )
128137
129138 def test_access_error (self ) -> None :
139+ """Test that AccessError exceptions are properly handled and return 403."""
130140 self .assert_exception_processed (
131141 exception_type = DemoExceptionType .access_error ,
132142 error_message = "test" ,
@@ -135,6 +145,7 @@ def test_access_error(self) -> None:
135145 )
136146
137147 def test_missing_error (self ) -> None :
148+ """Test that MissingError exceptions are properly handled and return 404."""
138149 self .assert_exception_processed (
139150 exception_type = DemoExceptionType .missing_error ,
140151 error_message = "test" ,
@@ -143,6 +154,7 @@ def test_missing_error(self) -> None:
143154 )
144155
145156 def test_http_exception (self ) -> None :
157+ """Test that HTTPException is properly handled with custom status code."""
146158 self .assert_exception_processed (
147159 exception_type = DemoExceptionType .http_exception ,
148160 error_message = "test" ,
@@ -152,21 +164,26 @@ def test_http_exception(self) -> None:
152164
153165 @mute_logger ("odoo.http" )
154166 def test_request_validation_error (self ) -> None :
167+ """Test that invalid request parameters trigger validation error (422)."""
155168 with self ._mocked_commit () as mocked_commit :
156169 route = "/fastapi_demo/demo/exception?exception_type=BAD&error_message="
157170 response = self .url_open (route , timeout = 200 )
158171 mocked_commit .assert_not_called ()
159172 self .assertEqual (response .status_code , 422 )
160173
161174 def test_no_commit_on_exception (self ) -> None :
162- # this test check that the way we mock the cursor is working as expected
163- # and that the transaction is rolled back in case of exception.
175+ """Test that database transactions are rolled back on exceptions.
176+
177+ Verifies that successful requests commit and failed requests rollback.
178+ """
179+ # Test successful request commits
164180 with self ._mocked_commit () as mocked_commit :
165181 url = "/fastapi_demo/demo"
166182 response = self .url_open (url , timeout = 600 )
167183 self .assertEqual (response .status_code , 200 )
168184 mocked_commit .assert_called_once ()
169185
186+ # Test exception doesn't commit
170187 self .assert_exception_processed (
171188 exception_type = DemoExceptionType .http_exception ,
172189 error_message = "test" ,
@@ -175,7 +192,11 @@ def test_no_commit_on_exception(self) -> None:
175192 )
176193
177194 def test_url_matching (self ):
178- # Test the URL mathing method on the endpoint
195+ """Test URL path matching logic for endpoint routing.
196+
197+ Ensures that the most specific matching path is selected when multiple
198+ paths could match a given URL.
199+ """
179200 paths = ["/fastapi" , "/fastapi_demo" , "/fastapi/v1" ]
180201 EndPoint = self .env ["fastapi.endpoint" ]
181202 self .assertEqual (
@@ -195,7 +216,128 @@ def test_url_matching(self):
195216 )
196217
197218 def test_multi_slash (self ):
219+ """Test endpoint with multiple slashes in path works correctly."""
198220 route = "/fastapi/demo-multi/demo/"
199221 response = self .url_open (route , timeout = 20 )
200222 self .assertEqual (response .status_code , 200 )
201223 self .assertIn (self .fastapi_multi_demo_app .root_path , str (response .url ))
224+
225+ def test_response_content_type (self ):
226+ """Test that responses have correct Content-Type headers."""
227+ route = "/fastapi_demo/demo/"
228+ response = self .url_open (route )
229+ self .assertEqual (response .status_code , 200 )
230+ self .assertIn ("application/json" , response .headers .get ("Content-Type" , "" ))
231+
232+ def test_lang_with_no_header (self ):
233+ """Test language defaults to en_US when no Accept-Language header provided."""
234+ route = "/fastapi_demo/demo/lang"
235+ response = self .url_open (route )
236+ self .assertEqual (response .status_code , 200 )
237+ # Default language should be returned
238+ self .assertIn (response .content , [b'"en_US"' , b'"en_GB"' ])
239+
240+ def test_lang_with_invalid_header (self ):
241+ """Test handling of malformed Accept-Language headers."""
242+ route = "/fastapi_demo/demo/lang"
243+ response = self .url_open (route , headers = {"Accept-language" : "invalid-lang" })
244+ self .assertEqual (response .status_code , 200 )
245+ # Should fallback to default language
246+ self .assertIsNotNone (response .content )
247+
248+ def test_retrying_minimum_boundary (self ):
249+ """Test retrying with minimum valid retry count."""
250+ nbr_retries = 2
251+ route = f"/fastapi_demo/demo/retrying?nbr_retries={ nbr_retries } "
252+ response = self .url_open (route , timeout = 20 )
253+ self .assertEqual (response .status_code , 200 )
254+ self .assertEqual (int (response .content ), nbr_retries )
255+
256+ @mute_logger ("odoo.http" )
257+ def test_retrying_invalid_parameter (self ):
258+ """Test that invalid retry count parameters are rejected."""
259+ # Test with retry count too low
260+ route = "/fastapi_demo/demo/retrying?nbr_retries=1"
261+ response = self .url_open (route , timeout = 20 )
262+ self .assertEqual (response .status_code , 422 ) # Validation error
263+
264+ @mute_logger ("odoo.http" )
265+ def test_retrying_missing_parameter (self ):
266+ """Test that missing required parameters return validation error."""
267+ route = "/fastapi_demo/demo/retrying"
268+ response = self .url_open (route , timeout = 20 )
269+ self .assertEqual (response .status_code , 422 ) # Validation error
270+
271+ def test_exception_with_special_characters (self ) -> None :
272+ """Test exception handling with special characters in error messages."""
273+ special_msg = "Error: <script>alert('test')</script> & special chars!"
274+ self .assert_exception_processed (
275+ exception_type = DemoExceptionType .user_error ,
276+ error_message = special_msg ,
277+ expected_message = special_msg ,
278+ expected_status_code = status .HTTP_400_BAD_REQUEST ,
279+ )
280+
281+ def test_url_matching_no_match (self ):
282+ """Test URL matching returns None when no path matches."""
283+ paths = ["/fastapi" , "/fastapi_demo" ]
284+ EndPoint = self .env ["fastapi.endpoint" ]
285+ result = EndPoint ._find_first_matching_url_path (paths , "/other/test" )
286+ self .assertIsNone (result )
287+
288+ def test_url_matching_exact_match (self ):
289+ """Test URL matching with exact path match."""
290+ paths = ["/fastapi" , "/fastapi_demo" ]
291+ EndPoint = self .env ["fastapi.endpoint" ]
292+ result = EndPoint ._find_first_matching_url_path (paths , "/fastapi_demo" )
293+ self .assertEqual (result , "/fastapi_demo" )
294+
295+ def test_url_matching_empty_paths (self ):
296+ """Test URL matching with empty path list."""
297+ paths = []
298+ EndPoint = self .env ["fastapi.endpoint" ]
299+ result = EndPoint ._find_first_matching_url_path (paths , "/fastapi/test" )
300+ self .assertIsNone (result )
301+
302+ def test_endpoint_with_trailing_slash (self ):
303+ """Test that endpoints work with and without trailing slashes."""
304+ # With trailing slash
305+ route = "/fastapi_demo/demo/"
306+ response = self .url_open (route )
307+ self .assertEqual (response .status_code , 200 )
308+
309+ # Without trailing slash
310+ route = "/fastapi_demo/demo"
311+ response = self .url_open (route )
312+ self .assertEqual (response .status_code , 200 )
313+
314+ def test_concurrent_language_requests (self ):
315+ """Test that concurrent requests with different languages are isolated."""
316+ import concurrent .futures
317+
318+ def make_lang_request (lang ):
319+ route = "/fastapi_demo/demo/lang"
320+ response = self .url_open (route , headers = {"Accept-language" : lang })
321+ return response .content
322+
323+ with concurrent .futures .ThreadPoolExecutor (max_workers = 3 ) as executor :
324+ futures = [
325+ executor .submit (make_lang_request , "fr" ),
326+ executor .submit (make_lang_request , "en" ),
327+ executor .submit (make_lang_request , "fr-FR" ),
328+ ]
329+ results = [f .result () for f in concurrent .futures .as_completed (futures )]
330+
331+ # All requests should complete successfully
332+ self .assertEqual (len (results ), 3 )
333+ for result in results :
334+ self .assertIsNotNone (result )
335+
336+ def test_http_methods_not_allowed (self ):
337+ """Test that incorrect HTTP methods return appropriate status."""
338+ route = "/fastapi_demo/demo/"
339+ # The demo endpoint only accepts GET
340+ response = self .url_open (route , data = {"test" : "data" })
341+ # POST should either be not allowed or handled differently
342+ # Depending on FastAPI behavior
343+ self .assertIn (response .status_code , [405 , 422 , 200 ])
0 commit comments