Coverage for fastapi_restly / testing / _client.py: 89%
51 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 09:54 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 09:54 +0000
1import json
3import httpx
4from fastapi.testclient import TestClient
7class RestlyTestClient(TestClient):
8 """Custom TestClient that automatically checks response codes and provides clear error messages."""
10 def assert_status(self, response: httpx.Response, expected_code: int | None = None):
11 """Check if the response status code matches the expected code."""
12 __tracebackhide__ = True
14 status_code = response.status_code
16 if expected_code is not None and status_code == expected_code:
17 return # All good
19 if expected_code is None and status_code < 400:
20 return # Also fine
22 # Raise AssertionError with detailed error message
23 try:
24 response_content = response.json()
25 except (ValueError, TypeError, json.JSONDecodeError):
26 response_content = response.content.decode(errors="ignore")
28 content_str_raw = str(response_content)
29 if len(content_str_raw) > 1000: 29 ↛ 30line 29 didn't jump to line 30 because the condition on line 29 was never true
30 content_str_raw = content_str_raw[:1000] + "...(truncated)"
31 content_str = f"Response JSON: {content_str_raw}"
33 # Safe method/URL extraction
34 try:
35 method = response.request.method.upper()
36 url = str(response.request.url)
37 request_info = f"{method} {url}"
38 except Exception:
39 request_info = "(request info unavailable)"
41 raise AssertionError(
42 f"Expected {request_info} to return {expected_code}, got {status_code}\n"
43 f"{content_str}"
44 )
46 def get(self, url: str, *, assert_status_code: int | None = 200, **kwargs) -> httpx.Response:
47 """Make a GET request. Asserts the response status code matches `assert_status_code` (default: 200).
48 Pass `assert_status_code=None` to skip the assertion."""
49 __tracebackhide__ = True
50 response = super().get(url, **kwargs)
51 self.assert_status(response, assert_status_code)
52 return response
54 def post(self, url: str, *, assert_status_code: int | None = 201, **kwargs) -> httpx.Response:
55 """Make a POST request. Asserts the response status code matches `assert_status_code` (default: 201).
56 Pass `assert_status_code=None` to skip the assertion."""
57 __tracebackhide__ = True
58 response = super().post(url, **kwargs)
59 self.assert_status(response, assert_status_code)
60 return response
62 def put(self, url: str, *, assert_status_code: int | None = 200, **kwargs) -> httpx.Response:
63 """Make a PUT request. Asserts the response status code matches `assert_status_code` (default: 200).
64 Pass `assert_status_code=None` to skip the assertion."""
65 __tracebackhide__ = True
66 response = super().put(url, **kwargs)
67 self.assert_status(response, assert_status_code)
68 return response
70 def patch(self, url: str, *, assert_status_code: int | None = 200, **kwargs) -> httpx.Response:
71 """Make a PATCH request. Asserts the response status code matches `assert_status_code` (default: 200).
72 Pass `assert_status_code=None` to skip the assertion."""
73 __tracebackhide__ = True
74 response = super().patch(url, **kwargs)
75 self.assert_status(response, assert_status_code)
76 return response
78 def delete(self, url: str, *, assert_status_code: int | None = 204, **kwargs) -> httpx.Response:
79 """Make a DELETE request. Asserts the response status code matches `assert_status_code` (default: 204).
80 Pass `assert_status_code=None` to skip the assertion."""
81 __tracebackhide__ = True
82 response = super().delete(url, **kwargs)
83 self.assert_status(response, assert_status_code)
84 return response