fix(test-schemathesis): disable rate limiter in fuzz subprocess

Schemathesis fires up to 3000 examples per endpoint. POST /auth/login
caps at 10/5min per IP, so the second example onward returns 429 and
the positive_data_acceptance check flags it as RejectedPositiveData
(its allowed-status list is hardcoded in schemathesis to
2xx/401/403/404/409/5xx, so OpenAPI tweaks can't fix it).

DECNET_LIMITER_ENABLED=false exists for exactly this case (see
limiter.py docstring on stress/load testing).

Reverts the custom_openapi shim from 5d88346 / 9b1168c — the endpoint
already declares 429 in its responses= map (api_login.py:38), and the
shim turned out to address a problem that wasn't there. Drop the
companion test along with it.
This commit is contained in:
2026-04-28 09:51:49 -04:00
parent 9b1168ce0b
commit ccc8619387
3 changed files with 6 additions and 133 deletions

View File

@@ -258,89 +258,6 @@ if DECNET_PROFILE_REQUESTS:
app.include_router(api_router, prefix="/api/v1")
def _rate_limited_endpoint_names() -> set[str]:
"""Return ``{module}.{qualname}`` for every endpoint slowapi tracks.
slowapi tags each ``@limiter.limit(...)``-decorated function under
these two registries. We do NOT advertise 429 on undecorated routes:
SlowAPIMiddleware only consults the registries, so an endpoint
without a decorator can never legitimately return 429, and
documenting it would mislead clients.
"""
names: set[str] = set()
names.update(getattr(limiter, "_route_limits", {}).keys())
names.update(getattr(limiter, "_dynamic_route_limits", {}).keys())
return names
def _custom_openapi() -> dict:
"""Inject 429 into the OpenAPI spec for rate-limited operations only.
SlowAPI returns 429 from ``@limiter.limit(...)``-decorated routes
(currently just ``POST /api/v1/auth/login``). Without 429 in the
spec, schemathesis flags legitimate rate-limit responses as
status-code-nonconformant. We restrict the injection to actually
rate-limited endpoints so the spec stays honest.
"""
from fastapi.openapi.utils import get_openapi
from fastapi.routing import APIRoute
if app.openapi_schema:
return app.openapi_schema
schema = get_openapi(
title=app.title,
version=app.version,
routes=app.routes,
description=app.description,
)
rate_limited = _rate_limited_endpoint_names()
if not rate_limited:
app.openapi_schema = schema
return schema
too_many = {
"description": "Rate limit exceeded",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {"error": {"type": "string"}},
"required": ["error"],
}
}
},
}
# Build {(path, method): endpoint_qualname} from the live route table.
route_index: dict[tuple[str, str], str] = {}
for route in app.routes:
if not isinstance(route, APIRoute):
continue
endpoint = route.endpoint
# slowapi unwraps decorated functions but keeps the original
# __module__/__name__ via functools.wraps, so this matches.
qualname = f"{endpoint.__module__}.{endpoint.__name__}"
for method in route.methods or ():
route_index[(route.path, method.lower())] = qualname
for path, path_item in schema.get("paths", {}).items():
for method, op in path_item.items():
if method.lower() not in {
"get", "post", "put", "patch", "delete", "options", "head",
}:
continue
qualname = route_index.get((path, method.lower()))
if qualname and qualname in rate_limited:
op.setdefault("responses", {}).setdefault("429", too_many)
app.openapi_schema = schema
return schema
app.openapi = _custom_openapi # type: ignore[method-assign]
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> ORJSONResponse:
"""