Skip to content

Commit 67b1d75

Browse files
committed
Addressed PR review with TDD
1 parent ec7e47c commit 67b1d75

2 files changed

Lines changed: 94 additions & 3 deletions

File tree

fastapi_gcp_tasks/requester.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Standard Library Imports
2-
from typing import Any, Dict, List, Tuple
2+
from typing import Any, Dict, List, Tuple, get_origin
33
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
44

55
# Third Party Imports
@@ -86,10 +86,11 @@ def _body(self, *, values: Dict[str, Any]) -> bytes | None:
8686
got_body = values.get(body_field.name)
8787
if got_body is None:
8888
if body_field.field_info.is_required():
89-
raise MissingParamError(name=body_field.name)
89+
raise MissingParamError(param=body_field.name)
9090
got_body = body_field.get_default()
9191
body_type = body_field.field_info.annotation
92-
if body_type is not None and not isinstance(got_body, body_type):
92+
check_type = get_origin(body_type) or body_type
93+
if body_type is not None and check_type is not None and not isinstance(got_body, check_type):
9394
raise WrongTypeError(field=body_field.name, type=body_type)
9495
body = json.dumps(jsonable_encoder(got_body)).encode()
9596
return body

tests/test_requester.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Unit tests for Requester._body covering missing-param and generic-type bugs."""
2+
3+
# Standard Library Imports
4+
from typing import List
5+
6+
# Third Party Imports
7+
import pytest
8+
from fastapi import FastAPI
9+
from fastapi.routing import APIRoute
10+
from pydantic import BaseModel
11+
12+
# Imports from this repository
13+
from fastapi_gcp_tasks.exception import MissingParamError, WrongTypeError
14+
from fastapi_gcp_tasks.requester import Requester
15+
16+
17+
class Item(BaseModel):
18+
"""Simple model for testing."""
19+
20+
name: str
21+
22+
23+
app = FastAPI()
24+
25+
26+
@app.post("/required_body")
27+
async def required_body_endpoint(item: Item) -> None:
28+
"""Endpoint with a required body param."""
29+
30+
31+
@app.post("/optional_body")
32+
async def optional_body_endpoint(item: Item = Item(name="default")) -> None:
33+
"""Endpoint with an optional body param."""
34+
35+
36+
@app.post("/list_body")
37+
async def list_body_endpoint(items: List[Item]) -> None:
38+
"""Endpoint with a parameterized generic body."""
39+
40+
41+
def _get_route(path: str) -> APIRoute:
42+
for route in app.routes:
43+
if isinstance(route, APIRoute) and route.path == path:
44+
return route
45+
raise ValueError(f"Route {path} not found")
46+
47+
48+
class TestMissingRequiredBody:
49+
"""Bug: MissingParamError called with name= but template expects {param}."""
50+
51+
def test_missing_required_body_raises_missing_param_error(self) -> None:
52+
"""Missing required body should raise MissingParamError, not KeyError."""
53+
route = _get_route("/required_body")
54+
requester = Requester(route=route, base_url="http://localhost")
55+
with pytest.raises(MissingParamError, match="field required"):
56+
requester._body(values={})
57+
58+
def test_optional_body_uses_default(self) -> None:
59+
"""Optional body should fall back to default when not provided."""
60+
route = _get_route("/optional_body")
61+
requester = Requester(route=route, base_url="http://localhost")
62+
body = requester._body(values={})
63+
assert body is not None
64+
assert b"default" in body
65+
66+
67+
class TestGenericBodyType:
68+
"""Bug: isinstance crashes with parameterized generics like list[Item]."""
69+
70+
def test_list_body_does_not_crash_with_valid_input(self) -> None:
71+
"""Parameterized generic body should not raise TypeError on isinstance."""
72+
route = _get_route("/list_body")
73+
requester = Requester(route=route, base_url="http://localhost")
74+
items = [Item(name="a"), Item(name="b")]
75+
body = requester._body(values={"items": items})
76+
assert body is not None
77+
78+
def test_list_body_wrong_type_raises(self) -> None:
79+
"""Wrong type for a generic body should raise WrongTypeError."""
80+
route = _get_route("/list_body")
81+
requester = Requester(route=route, base_url="http://localhost")
82+
with pytest.raises(WrongTypeError):
83+
requester._body(values={"items": "not a list"})
84+
85+
def test_simple_body_wrong_type_raises(self) -> None:
86+
"""Wrong type for a simple body should raise WrongTypeError."""
87+
route = _get_route("/required_body")
88+
requester = Requester(route=route, base_url="http://localhost")
89+
with pytest.raises(WrongTypeError):
90+
requester._body(values={"item": "not an Item"})

0 commit comments

Comments
 (0)