Files
responder/tests/test_v713_improvements.py
kennethreitz 428181a096 Add JWT/OAuth2 auth, Problem exception, function middleware, and a broad feature sweep for 8.1.0
Flagship: JWTAuth (HS256/RS256/ES256, exp/aud/iss, JWKS with off-loop
fetch, scope claims -> requires()) and OAuth2Auth flow descriptions that
light up Swagger UI's Authorize button, behind a new responder[jwt] extra.

Also additive: responder.Problem (raisable RFC 9457) + RFC 9457 abort();
@api.middleware function-style HTTP middleware; Pydantic bodies and
Query/Header/Cookie/Path markers on class-based and WebSocket handlers;
Form-model binding; write-side preconditions (If-Match/If-Unmodified-Since
-> 412); q-ranked content negotiation; get_list for repeated headers;
status_code= route option; OpenAPI document caching; rate-limiter key=/
fail-open/X-RateLimit-Reset; configurable metrics buckets + in-flight
gauge; RFC 8288 pagination Link headers; Redis session serializer;
unified request-ID policy; CLI --host/--port/--server/--reload; optional
orjson encode backend; AsyncTestClient + SSE test helpers; fakeredis
backend coverage. Five DeprecationWarnings start the 9.0 clock.

Every feature was implemented test-first, then the combined diff went
through a 64-agent adversarial review that caught 19 issues including two
criticals (an auth scope-claim regression that flipped authorization
decisions, and a sync-middleware threadpool deadlock) -- all fixed with
regression tests before release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:08:42 -04:00

132 lines
4.2 KiB
Python

"""Regression tests for two reliability improvements:
- Background task pool drains in-flight work on shutdown.
- Redis rate-limit backends increment + expire atomically (no permanent
lock-out if the process dies between the two operations).
"""
import threading
import pytest
import responder
from responder.background import BackgroundQueue
from responder.ext.ratelimit import RateLimiter, RedisBackend
# --------------------------------------------------------------------------
# Background queue graceful shutdown
# --------------------------------------------------------------------------
def test_background_shutdown_drains_in_flight_tasks():
"""shutdown(wait=True) blocks until submitted tasks finish."""
queue = BackgroundQueue(n=2)
done = threading.Event()
started = threading.Event()
def slow_task():
started.set()
# Long enough that the task is still running when shutdown is called.
time_to_finish.wait(timeout=5)
done.set()
time_to_finish = threading.Event()
queue.run(slow_task)
assert started.wait(timeout=5)
# Let the task complete, then drain.
time_to_finish.set()
queue.shutdown(wait=True)
assert done.is_set()
def test_background_shutdown_rejects_new_tasks():
"""After shutdown the pool no longer accepts work."""
queue = BackgroundQueue(n=1)
queue.shutdown(wait=True)
with pytest.raises(RuntimeError):
queue.run(lambda: None)
def test_app_shutdown_triggers_background_drain():
"""The app registers a shutdown handler that drains the pool."""
api = responder.API(allowed_hosts=[";"], session_https_only=False)
@api.route("/")
def index(req, resp):
resp.text = "ok"
# Entering/exiting the TestClient context fires startup/shutdown events.
with api.requests as client:
assert client.get("/").status_code == 200
# The shutdown event drained (and closed) the background pool.
with pytest.raises(RuntimeError):
api.background.run(lambda: None)
# --------------------------------------------------------------------------
# Redis rate-limit atomicity
# --------------------------------------------------------------------------
class FakeRedis:
"""Minimal Redis stand-in that only understands the INCR+EXPIRE script.
``incr``/``expire`` raise if called directly, proving the backend routes
through the atomic ``eval`` path rather than two separate round-trips.
"""
def __init__(self):
self.counts = {}
self.expiries = {}
def eval(self, script, numkeys, *keys_and_args):
key = keys_and_args[0]
period = keys_and_args[1]
count = self.counts.get(key, 0) + 1
self.counts[key] = count
if count == 1:
self.expiries[key] = period
return count
def incr(self, key): # pragma: no cover - must not be called
raise AssertionError("incr called directly; expected atomic eval")
def expire(self, key, period): # pragma: no cover - must not be called
raise AssertionError("expire called directly; expected atomic eval")
def test_redis_backend_uses_atomic_eval_and_sets_expiry():
fake = FakeRedis()
backend = RedisBackend(client=fake)
allowed, remaining, _ = backend.hit("1.2.3.4", max_requests=2, period=60)
assert (allowed, remaining) == (True, 1)
# Expiry attached on the very first hit, atomically with the increment.
assert fake.expiries[backend.prefix + "1.2.3.4"] == 60
allowed, remaining, _ = backend.hit("1.2.3.4", max_requests=2, period=60)
assert (allowed, remaining) == (True, 0)
allowed, remaining, _ = backend.hit("1.2.3.4", max_requests=2, period=60)
assert (allowed, remaining) == (False, 0)
# Only one expiry was ever set for this window.
assert list(fake.expiries.values()) == [60]
def test_redis_backend_end_to_end():
api = responder.API(allowed_hosts=[";"], session_https_only=False)
limiter = RateLimiter(requests=2, period=60, backend=RedisBackend(client=FakeRedis()))
limiter.install(api)
@api.route("/")
def view(req, resp):
resp.text = "ok"
client = api.requests
assert client.get("/").status_code == 200
assert client.get("/").status_code == 200
assert client.get("/").status_code == 429