mirror of
https://github.com/kennethreitz/responder.git
synced 2026-07-21 18:39:29 +00:00
ca1c259ff9
- secret_key default is now None, not the public "NOTASECRET" (which now
raises). sessions="auto" (default) signs with secret_key /
RESPONDER_SECRET_KEY, else mints a random per-process key with a loud
warning; sessions=True requires a real key; sessions=False disables sessions.
- Session cookies are Secure by default in production (session_https_only=None
-> Secure unless debug); SameSite=None without Secure is rejected. New
session_max_age knob.
- resolve_secret_key() + SessionConfigError in ext/sessions.py.
- req.session raises a guiding RuntimeError when sessions are disabled;
resp.session is now a delegating property (removed from __slots__ + the
eager bind), so resp.session = {...} writes through to the cookie.
- Migrated test fixtures (conftest + make_api + test_round7) to
session_https_only=False so the http TestClient round-trips cookies.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
3.2 KiB
Python
126 lines
3.2 KiB
Python
"""v5: deferred middleware stack + add_exception_handler."""
|
|
|
|
import pytest
|
|
from starlette.datastructures import MutableHeaders
|
|
from starlette.testclient import TestClient
|
|
|
|
import responder
|
|
|
|
|
|
@pytest.fixture
|
|
def make_api():
|
|
def _make(**kwargs):
|
|
kwargs.setdefault("allowed_hosts", [";"])
|
|
kwargs.setdefault("session_https_only", False)
|
|
return responder.API(**kwargs)
|
|
|
|
return _make
|
|
|
|
|
|
def _no_raise_client(api):
|
|
return TestClient(api, base_url="http://;", raise_server_exceptions=False)
|
|
|
|
|
|
class _HeaderMW:
|
|
def __init__(self, app, name="X-Custom", value="yes"):
|
|
self.app = app
|
|
self.name = name
|
|
self.value = value
|
|
|
|
async def __call__(self, scope, receive, send):
|
|
if scope["type"] != "http":
|
|
await self.app(scope, receive, send)
|
|
return
|
|
|
|
async def send_wrapper(message):
|
|
if message["type"] == "http.response.start":
|
|
MutableHeaders(scope=message)[self.name] = self.value
|
|
await send(message)
|
|
|
|
await self.app(scope, receive, send_wrapper)
|
|
|
|
|
|
class _BoomMW:
|
|
def __init__(self, app):
|
|
self.app = app
|
|
|
|
async def __call__(self, scope, receive, send):
|
|
if scope["type"] == "http":
|
|
raise RuntimeError("middleware boom")
|
|
await self.app(scope, receive, send)
|
|
|
|
|
|
def test_add_middleware_after_construction(make_api):
|
|
api = make_api()
|
|
|
|
@api.route("/")
|
|
def view(req, resp):
|
|
resp.text = "ok"
|
|
|
|
api.add_middleware(_HeaderMW) # added AFTER __init__
|
|
r = api.requests.get("/")
|
|
assert r.headers.get("X-Custom") == "yes"
|
|
|
|
|
|
def test_add_exception_handler_programmatic(make_api):
|
|
api = make_api()
|
|
|
|
def handle(req, resp, exc):
|
|
resp.status_code = 400
|
|
resp.media = {"err": str(exc)}
|
|
|
|
api.add_exception_handler(ValueError, handle)
|
|
|
|
@api.route("/")
|
|
def view(req, resp):
|
|
raise ValueError("boom")
|
|
|
|
r = api.requests.get("/", headers={"Accept": "application/json"})
|
|
assert r.status_code == 400
|
|
assert r.json() == {"err": "boom"}
|
|
|
|
|
|
def test_request_id_present_on_500(make_api):
|
|
# The reconciliation: ServerErrorMiddleware renders the 500 INSIDE the
|
|
# request-id tier, so 500s still carry X-Request-ID (v4.1 couldn't).
|
|
api = make_api(request_id=True)
|
|
|
|
@api.route("/boom")
|
|
def boom(req, resp):
|
|
raise RuntimeError("kaboom")
|
|
|
|
r = _no_raise_client(api).get("/boom")
|
|
assert r.status_code == 500
|
|
assert "X-Request-ID" in r.headers
|
|
|
|
|
|
def test_user_middleware_errors_are_caught(make_api):
|
|
# ServerErrorMiddleware is outermost over user middleware, so a crash in
|
|
# user middleware renders a 500 instead of escaping as a raw ASGI error.
|
|
api = make_api()
|
|
|
|
@api.route("/")
|
|
def view(req, resp):
|
|
resp.text = "ok"
|
|
|
|
api.add_middleware(_BoomMW)
|
|
r = _no_raise_client(api).get("/")
|
|
assert r.status_code == 500
|
|
|
|
|
|
def test_exception_handler_decorator_still_works(make_api):
|
|
api = make_api()
|
|
|
|
@api.exception_handler(KeyError)
|
|
async def handle(req, resp, exc):
|
|
resp.status_code = 422
|
|
resp.text = "missing key"
|
|
|
|
@api.route("/")
|
|
def view(req, resp):
|
|
raise KeyError("x")
|
|
|
|
r = api.requests.get("/")
|
|
assert r.status_code == 422
|
|
assert r.text == "missing key"
|