From b413904ace1dae1d9b82ea2e3f5d6bc214a41c74 Mon Sep 17 00:00:00 2001 From: Louis King Date: Thu, 25 Jun 2026 21:00:35 +0100 Subject: [PATCH] fix(tests): introspect mounted paths via OpenAPI schema, unpin fastapi FastAPI 0.137.0 refactored include_router to keep included routers as nested objects rather than flattening their routes into app.routes, so test_app_factory's `{route.path for route in app.routes}` no longer found the /metrics route (the endpoint still serves; only this introspection broke). FastAPI now treats router.routes as an internal implementation detail. Switch the metrics route checks to the public OpenAPI schema (app.openapi()["paths"]), which is stable across versions and resolves router prefixes correctly, and drop the <0.137.0 pin that was blocking the upgrade. Verified: app factory tests pass on both 0.136.3 and 0.137.2; full tests/test_api suite (462 tests) passes on 0.137.2. Co-Authored-By: Claude Opus 4.8 --- pyproject.toml | 5 +---- tests/test_api/test_app_factory.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2cbf274..9433f84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,10 +30,7 @@ dependencies = [ "python-dotenv>=1.0.0", "sqlalchemy>=2.0.0", "alembic>=1.12.0", - # 0.137.0 regressed include_router: routed endpoints no longer appear in - # app.routes (they still serve, but introspection breaks). Pin below it - # until a fixed release lands. See test_app_factory metrics route checks. - "fastapi>=0.100.0,<0.137.0", + "fastapi>=0.100.0", "starlette>=0.40.0,<1.0.0", "uvicorn[standard]>=0.23.0", "paho-mqtt>=2.0.0", diff --git a/tests/test_api/test_app_factory.py b/tests/test_api/test_app_factory.py index 3d37157..e75a098 100644 --- a/tests/test_api/test_app_factory.py +++ b/tests/test_api/test_app_factory.py @@ -27,6 +27,18 @@ def clean_env(monkeypatch): return monkeypatch +def _served_paths(app): + """Return the set of paths the app actually serves. + + FastAPI 0.137 refactored ``include_router`` to keep included routers as + nested objects instead of flattening their routes into ``app.routes``, so + iterating ``app.routes`` no longer surfaces routed endpoints like + ``/metrics``. The OpenAPI schema is the stable, version-independent way to + introspect mounted paths (and it resolves router prefixes correctly). + """ + return set(app.openapi()["paths"]) + + def test_factory_reads_database_and_redis_from_env(clean_env): """Workers must pick up the real DB/Redis config from env, not the hardcoded create_app defaults.""" @@ -71,13 +83,11 @@ def test_factory_metrics_enabled_via_env(clean_env): """METRICS_ENABLED=true mounts the /metrics endpoint.""" clean_env.setenv("METRICS_ENABLED", "true") app = create_app_from_env() - paths = {getattr(route, "path", None) for route in app.routes} - assert "/metrics" in paths + assert "/metrics" in _served_paths(app) def test_factory_metrics_disabled_via_env(clean_env): """METRICS_ENABLED=false omits the /metrics endpoint.""" clean_env.setenv("METRICS_ENABLED", "false") app = create_app_from_env() - paths = {getattr(route, "path", None) for route in app.routes} - assert "/metrics" not in paths + assert "/metrics" not in _served_paths(app)