Skip to content

Commit 73f3ca1

Browse files
fix http tests
1 parent bcc9951 commit 73f3ca1

File tree

2 files changed

+304
-4
lines changed

2 files changed

+304
-4
lines changed

aws_lambda_powertools/event_handler/http_resolver.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def serialize(self, headers: dict[str, str | list[str]], cookies: list[Cookie])
2828
"""Serialize headers for HTTP response format."""
2929
combined_headers: dict[str, str] = {}
3030
for key, values in headers.items():
31-
if values is None:
31+
if values is None: # pragma: no cover
3232
continue
3333
if isinstance(values, str):
3434
combined_headers[key] = values
@@ -160,7 +160,7 @@ class MockLambdaContext:
160160
log_group_name = "/aws/lambda/http-resolver"
161161
log_stream_name = "local"
162162

163-
def get_remaining_time_in_millis(self) -> int:
163+
def get_remaining_time_in_millis(self) -> int: # pragma: no cover
164164
return 300000 # 5 minutes
165165

166166

@@ -329,7 +329,7 @@ def sync_next(app):
329329
# Store for later await
330330
app.context["_async_next_result"] = future
331331
return Response(status_code=200, body="") # Placeholder
332-
else:
332+
else: # pragma: no cover
333333
return loop.run_until_complete(next_handler(app))
334334

335335
# Check if middleware is async
@@ -470,7 +470,7 @@ async def _send_response(self, send: Callable, response: dict) -> None:
470470
body_bytes = base64.b64decode(body)
471471
elif isinstance(body, str):
472472
body_bytes = body.encode("utf-8")
473-
else:
473+
else: # pragma: no cover
474474
body_bytes = body
475475

476476
# Send response body

tests/functional/event_handler/required_dependencies/test_http_resolver.py

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,3 +915,303 @@ def get_image():
915915
# THEN it decodes base64 and returns binary data
916916
assert captured["status_code"] == 200
917917
assert captured["body"] == binary_data
918+
919+
920+
@pytest.mark.asyncio
921+
async def test_asgi_duplicate_headers():
922+
# GIVEN an ASGI request with duplicate headers
923+
app = HttpResolverAlpha()
924+
925+
@app.get("/headers")
926+
def get_headers():
927+
# Return the accept header which has duplicates
928+
accept = app.current_event.headers.get("accept", "")
929+
return {"accept": accept}
930+
931+
scope = {
932+
"type": "http",
933+
"method": "GET",
934+
"path": "/headers",
935+
"query_string": b"",
936+
"headers": [
937+
(b"accept", b"text/html"),
938+
(b"accept", b"application/json"), # Duplicate header
939+
],
940+
}
941+
942+
receive = make_asgi_receive()
943+
send, captured = make_asgi_send()
944+
945+
# WHEN called via ASGI interface
946+
await app(scope, receive, send)
947+
948+
# THEN duplicate headers are joined with comma
949+
assert captured["status_code"] == 200
950+
body = json.loads(captured["body"])
951+
assert body["accept"] == "text/html, application/json"
952+
953+
954+
@pytest.mark.asyncio
955+
async def test_asgi_with_cookies():
956+
# GIVEN an app that sets cookies
957+
from aws_lambda_powertools.shared.cookies import Cookie
958+
959+
app = HttpResolverAlpha()
960+
961+
@app.get("/set-cookie")
962+
def set_cookie():
963+
cookie = Cookie(name="session", value="abc123")
964+
return Response(
965+
status_code=200,
966+
content_type="application/json",
967+
body={"message": "Cookie set"},
968+
cookies=[cookie],
969+
)
970+
971+
scope = {
972+
"type": "http",
973+
"method": "GET",
974+
"path": "/set-cookie",
975+
"query_string": b"",
976+
"headers": [],
977+
}
978+
979+
receive = make_asgi_receive()
980+
captured_headers: list[tuple[bytes, bytes]] = []
981+
982+
async def send(message: dict[str, Any]) -> None:
983+
await asyncio.sleep(0)
984+
if message["type"] == "http.response.start":
985+
captured_headers.extend(message.get("headers", []))
986+
987+
# WHEN called via ASGI interface
988+
await app(scope, receive, send)
989+
990+
# THEN Set-Cookie header is present
991+
cookie_headers = [h for h in captured_headers if h[0] == b"set-cookie"]
992+
assert len(cookie_headers) == 1
993+
assert b"session=abc123" in cookie_headers[0][1]
994+
995+
996+
@pytest.mark.asyncio
997+
async def test_async_middleware():
998+
# GIVEN an app with async middleware
999+
app = HttpResolverAlpha()
1000+
order: list[str] = []
1001+
1002+
async def async_middleware(app, next_middleware):
1003+
order.append("async_before")
1004+
await asyncio.sleep(0.001)
1005+
response = await next_middleware(app)
1006+
order.append("async_after")
1007+
return response
1008+
1009+
app.use([async_middleware])
1010+
1011+
@app.get("/test")
1012+
async def test_route():
1013+
order.append("handler")
1014+
return {"ok": True}
1015+
1016+
scope = {
1017+
"type": "http",
1018+
"method": "GET",
1019+
"path": "/test",
1020+
"query_string": b"",
1021+
"headers": [],
1022+
}
1023+
1024+
receive = make_asgi_receive()
1025+
send, captured = make_asgi_send()
1026+
1027+
# WHEN called via ASGI interface
1028+
await app(scope, receive, send)
1029+
1030+
# THEN async middleware executes correctly
1031+
assert captured["status_code"] == 200
1032+
assert order == ["async_before", "handler", "async_after"]
1033+
1034+
1035+
def test_unhandled_exception_raises():
1036+
# GIVEN an app without exception handler for ValueError
1037+
app = HttpResolverAlpha()
1038+
1039+
@app.get("/error")
1040+
def raise_error():
1041+
raise ValueError("Unhandled error")
1042+
1043+
event = {
1044+
"httpMethod": "GET",
1045+
"path": "/error",
1046+
"headers": {},
1047+
"queryStringParameters": {},
1048+
"body": None,
1049+
}
1050+
1051+
# WHEN the route raises an unhandled exception
1052+
# THEN it propagates up
1053+
with pytest.raises(ValueError, match="Unhandled error"):
1054+
app.resolve(event, MockLambdaContext())
1055+
1056+
1057+
def test_default_not_found_without_custom_handler():
1058+
# GIVEN an app WITHOUT custom not_found handler
1059+
app = HttpResolverAlpha()
1060+
1061+
@app.get("/exists")
1062+
def exists():
1063+
return {"exists": True}
1064+
1065+
event = {
1066+
"httpMethod": "GET",
1067+
"path": "/unknown",
1068+
"headers": {},
1069+
"queryStringParameters": {},
1070+
"body": None,
1071+
}
1072+
1073+
# WHEN requesting unknown route
1074+
result = app.resolve(event, MockLambdaContext())
1075+
1076+
# THEN default 404 response is returned
1077+
assert result["statusCode"] == 404
1078+
body = json.loads(result["body"])
1079+
assert body["message"] == "Not found"
1080+
1081+
1082+
def test_method_not_matching_continues_search():
1083+
# GIVEN an app with routes for different methods on same path
1084+
app = HttpResolverAlpha()
1085+
1086+
@app.get("/resource")
1087+
def get_resource():
1088+
return {"method": "GET"}
1089+
1090+
@app.post("/resource")
1091+
def post_resource():
1092+
return {"method": "POST"}
1093+
1094+
# WHEN requesting with POST
1095+
event = {
1096+
"httpMethod": "POST",
1097+
"path": "/resource",
1098+
"headers": {},
1099+
"queryStringParameters": {},
1100+
"body": None,
1101+
}
1102+
result = app.resolve(event, MockLambdaContext())
1103+
1104+
# THEN it finds the POST handler (skipping GET)
1105+
assert result["statusCode"] == 200
1106+
body = json.loads(result["body"])
1107+
assert body["method"] == "POST"
1108+
1109+
1110+
def test_list_headers_serialization():
1111+
# GIVEN an app that returns list headers
1112+
app = HttpResolverAlpha()
1113+
1114+
@app.get("/multi-header")
1115+
def multi_header():
1116+
return Response(
1117+
status_code=200,
1118+
content_type="application/json",
1119+
body={"ok": True},
1120+
headers={"X-Custom": ["value1", "value2"]},
1121+
)
1122+
1123+
event = {
1124+
"httpMethod": "GET",
1125+
"path": "/multi-header",
1126+
"headers": {},
1127+
"queryStringParameters": {},
1128+
"body": None,
1129+
}
1130+
1131+
# WHEN the route is resolved
1132+
result = app.resolve(event, MockLambdaContext())
1133+
1134+
# THEN list headers are joined with comma
1135+
assert result["statusCode"] == 200
1136+
assert result["headers"]["X-Custom"] == "value1, value2"
1137+
1138+
1139+
def test_string_body_in_event():
1140+
# GIVEN an event with string body (not bytes)
1141+
app = HttpResolverAlpha()
1142+
1143+
@app.post("/echo")
1144+
def echo():
1145+
return {"body": app.current_event.body}
1146+
1147+
# Body is already a string, not bytes
1148+
event = {
1149+
"httpMethod": "POST",
1150+
"path": "/echo",
1151+
"headers": {"content-type": "text/plain"},
1152+
"queryStringParameters": {},
1153+
"body": "plain text body",
1154+
}
1155+
1156+
# WHEN the route is resolved
1157+
result = app.resolve(event, MockLambdaContext())
1158+
1159+
# THEN string body is handled correctly
1160+
assert result["statusCode"] == 200
1161+
body = json.loads(result["body"])
1162+
assert body["body"] == "plain text body"
1163+
1164+
1165+
@pytest.mark.asyncio
1166+
async def test_asgi_default_not_found():
1167+
# GIVEN an app WITHOUT custom not_found handler
1168+
app = HttpResolverAlpha()
1169+
1170+
@app.get("/exists")
1171+
def exists():
1172+
return {"exists": True}
1173+
1174+
scope = {
1175+
"type": "http",
1176+
"method": "GET",
1177+
"path": "/unknown-route",
1178+
"query_string": b"",
1179+
"headers": [],
1180+
}
1181+
1182+
receive = make_asgi_receive()
1183+
send, captured = make_asgi_send()
1184+
1185+
# WHEN requesting unknown route via ASGI
1186+
await app(scope, receive, send)
1187+
1188+
# THEN default 404 is returned
1189+
assert captured["status_code"] == 404
1190+
body = json.loads(captured["body"])
1191+
assert body["message"] == "Not found"
1192+
1193+
1194+
@pytest.mark.asyncio
1195+
async def test_asgi_unhandled_exception_raises():
1196+
# GIVEN an app without exception handler for ValueError
1197+
app = HttpResolverAlpha()
1198+
1199+
@app.get("/error")
1200+
async def raise_error():
1201+
raise ValueError("Async unhandled error")
1202+
1203+
scope = {
1204+
"type": "http",
1205+
"method": "GET",
1206+
"path": "/error",
1207+
"query_string": b"",
1208+
"headers": [],
1209+
}
1210+
1211+
receive = make_asgi_receive()
1212+
send, _ = make_asgi_send()
1213+
1214+
# WHEN the route raises an unhandled exception
1215+
# THEN it propagates up
1216+
with pytest.raises(ValueError, match="Async unhandled error"):
1217+
await app(scope, receive, send)

0 commit comments

Comments
 (0)