Fix TestClient JSON null bodies - #122
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR updates TestClient JSON body handling so callers can explicitly send a JSON null (via json=None) while still allowing “no JSON argument provided” to mean “no request body”.
Changes:
- Introduce a private sentinel (
_JSON_NOT_GIVEN) to distinguish omittedjsonfrom explicitly providedNone. - Update
_request_bodyambiguity detection to account for the new sentinel. - Add/extend unit tests to assert explicit
json=Nonesendsapplication/jsonwith payloadnull, and to reject mixed body styles.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| tests/unit/test_test_client.py | Adds coverage for explicit JSON null bodies and new ambiguity error cases. |
| src/quater/testing.py | Implements sentinel-based JSON argument handling so json=None becomes an intentional JSON body. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| _MCP_PATH = "/mcp" | ||
| _MCP_PROTOCOL_VERSION = "2025-11-25" | ||
| _JSON_NOT_GIVEN: Final = object() |
| headers: HeaderItems | Mapping[str, str] | None = None, | ||
| cookies: Mapping[str, str] | None = None, | ||
| json: object = None, | ||
| json: object = _JSON_NOT_GIVEN, |
| headers: HeaderItems | Mapping[str, str] | None = None, | ||
| cookies: Mapping[str, str] | None = None, | ||
| json: object = None, | ||
| json: object = _JSON_NOT_GIVEN, |
| headers: HeaderItems | Mapping[str, str] | None = None, | ||
| cookies: Mapping[str, str] | None = None, | ||
| json: object = None, | ||
| json: object = _JSON_NOT_GIVEN, |
| headers: HeaderItems | Mapping[str, str] | None = None, | ||
| cookies: Mapping[str, str] | None = None, | ||
| json: object = None, | ||
| json: object = _JSON_NOT_GIVEN, |
| headers: HeaderItems | Mapping[str, str] | None = None, | ||
| cookies: Mapping[str, str] | None = None, | ||
| json: object = None, | ||
| json: object = _JSON_NOT_GIVEN, |
| ) | ||
| from quater.typing import AuthContext | ||
|
|
||
| ANY_BODY = Body() |
| async def test_test_client_sends_explicit_json_null_body() -> None: | ||
| app = Quater() | ||
|
|
||
| async def echo(request: Request, payload: Any = ANY_BODY) -> dict[str, object]: |
| responses = [ | ||
| await client.request("POST", "/echo", json=None), | ||
| await client.post("/echo", json=None), | ||
| await client.put("/echo", json=None), | ||
| await client.patch("/echo", json=None), | ||
| await client.delete("/echo", json=None), | ||
| ] |
Greptile SummaryThis PR fixes a long-standing ambiguity in
|
| Filename | Overview |
|---|---|
| src/quater/testing.py | Introduces _JsonNotGiven sentinel, updates all json parameter defaults and _request_body logic; implementation is correct and well-contained. |
| tests/unit/test_test_client.py | Adds a regression test for json=None across all five HTTP methods and extends conflict-detection tests; coverage is thorough. |
| tests/integration/test_cross_surface_parity.py | Correctly replaces the arguments.get('payload') shortcut with an explicit key-presence check to avoid unintentionally sending a null JSON body. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["caller: client.post(path, json=X)"] --> B{"X is _JSON_NOT_GIVEN?"}
B -- Yes --> C["No json body\n(omitted argument)"]
B -- No --> D{"X is None?"}
D -- Yes --> E["dumps_json(None) → b'null'\ncontent-type: application/json"]
D -- No --> F["dumps_json(X) → bytes\ncontent-type: application/json"]
C --> G{"content / data / files?"}
G -- None --> H["body = b'', content-type = None"]
G -- data+files --> I["multipart body"]
G -- data only --> J["urlencoded body"]
G -- content only --> K["raw content body"]
E --> L["Request dispatched to app"]
F --> L
H --> L
I --> L
J --> L
K --> L
Reviews (3): Last reviewed commit: "Preserve missing body parity in tests" | Re-trigger Greptile
Summary
TestClientJSON bodies from explicit JSONnullwith aninternal sentinel.
b"null"andcontent-type: application/jsonwhen users passjson=None.request(), POST, PUT, PATCH, DELETE, and thebody-style conflict checks.
Linked Issue
Fixes #118
Contributor Checklist
acceptedbefore I started.issue_118.one for each focused bug fix.
Checks Run
python -m pytest tests/unit/test_test_client.py -qpython -m ruff check src/quater/testing.py tests/unit/test_test_client.pypython -m mypy src/quater/testing.py tests/unit/test_test_client.py