Skip to content

Fix TestClient JSON null bodies - #122

Open
cancaries wants to merge 3 commits into
DevilsAutumn:mainfrom
cancaries:issue_118
Open

Fix TestClient JSON null bodies#122
cancaries wants to merge 3 commits into
DevilsAutumn:mainfrom
cancaries:issue_118

Conversation

@cancaries

Copy link
Copy Markdown

Summary

  • Distinguish omitted TestClient JSON bodies from explicit JSON null with an
    internal sentinel.
  • Send b"null" and content-type: application/json when users pass
    json=None.
  • Add regression coverage for request(), POST, PUT, PATCH, DELETE, and the
    body-style conflict checks.

Linked Issue

Fixes #118

Contributor Checklist

  • The linked issue was marked accepted before I started.
  • Nobody else was assigned or already working on the issue before I started.
  • My branch is named issue_118.
  • The PR is focused on one issue with no unrelated changes.
  • Tests are added or updated when behavior changes.
  • Docs are not updated because this is covered by TestClient behavior tests.
  • Changelog is not updated because this repository does not appear to keep
    one for each focused bug fix.
  • I listed the checks I ran below.

Checks Run

  • python -m pytest tests/unit/test_test_client.py -q
  • python -m ruff check src/quater/testing.py tests/unit/test_test_client.py
  • python -m mypy src/quater/testing.py tests/unit/test_test_client.py

Copilot AI review requested due to automatic review settings June 12, 2026 09:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 omitted json from explicitly provided None.
  • Update _request_body ambiguity detection to account for the new sentinel.
  • Add/extend unit tests to assert explicit json=None sends application/json with payload null, 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.

Comment thread src/quater/testing.py Outdated

_MCP_PATH = "/mcp"
_MCP_PROTOCOL_VERSION = "2025-11-25"
_JSON_NOT_GIVEN: Final = object()
Comment thread src/quater/testing.py
headers: HeaderItems | Mapping[str, str] | None = None,
cookies: Mapping[str, str] | None = None,
json: object = None,
json: object = _JSON_NOT_GIVEN,
Comment thread src/quater/testing.py
headers: HeaderItems | Mapping[str, str] | None = None,
cookies: Mapping[str, str] | None = None,
json: object = None,
json: object = _JSON_NOT_GIVEN,
Comment thread src/quater/testing.py
headers: HeaderItems | Mapping[str, str] | None = None,
cookies: Mapping[str, str] | None = None,
json: object = None,
json: object = _JSON_NOT_GIVEN,
Comment thread src/quater/testing.py
headers: HeaderItems | Mapping[str, str] | None = None,
cookies: Mapping[str, str] | None = None,
json: object = None,
json: object = _JSON_NOT_GIVEN,
Comment thread src/quater/testing.py
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]:
Comment on lines +110 to +116
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-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a long-standing ambiguity in TestClient where passing json=None was indistinguishable from omitting the json argument entirely — both resulted in an empty body with no Content-Type. The fix introduces a named private sentinel _JsonNotGiven so that explicit json=None now correctly serialises to b"null" with content-type: application/json.

  • A _JsonNotGiven sentinel class (with __slots__ and __repr__) replaces the bare object() pattern; _JSON_NOT_GIVEN: Final is the singleton used as the default for json across all five verb methods and request().
  • _request_body now uses identity checks (json is not _JSON_NOT_GIVEN) for both the conflict-detection and the serialisation branch, correctly propagating None as a JSON value.
  • The integration test in test_cross_surface_parity.py is updated to use an explicit if "payload" in arguments guard, preventing the old arguments.get("payload") pattern from silently triggering the new "send null body" behaviour.

Confidence Score: 5/5

The change is safe to merge; it only adds a private sentinel class and updates default-value comparisons inside a testing helper, with no effect on production request handling.

All three changed files are testing infrastructure. The sentinel is private, identity-checked throughout, and Final-annotated to prevent reassignment. The conflict-detection and serialisation logic in _request_body was updated consistently, and the integration test was correctly adjusted so no existing call site accidentally sends a null JSON body. New tests cover the json=None round-trip for every affected verb as well as the conflict cases.

No files require special attention.

Important Files Changed

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
Loading

Reviews (3): Last reviewed commit: "Preserve missing body parity in tests" | Re-trigger Greptile

Comment thread src/quater/testing.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestClient should distinguish omitted JSON from JSON null

2 participants