Skip to content

Commit 3552e94

Browse files
committed
Improve FastAPI test coverage
1 parent a8795e1 commit 3552e94

File tree

2 files changed

+386
-3
lines changed

2 files changed

+386
-3
lines changed

fastapi/tests/test_fastapi.py

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)