Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions framework/hosting/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ async def test_app_state_has_registries(self, app: FastAPI):

async def test_modules_enabled_limits_loaded_modules(self, settings: Settings):
"""Host respects settings.modules_enabled — only listed modules contribute routes."""
from simple_module_test import effective_route_paths

# Only Auth should be loaded; Dashboard routes must be absent.
restricted = settings.model_copy(update={"modules_enabled": ["Auth"]})
app = create_app(restricted)
paths: set[str] = {str(r.path) for r in app.routes if hasattr(r, "path")}
paths = effective_route_paths(app)
# Auth is now contracts-only, so it has no routes — only health remains.
assert "/dashboard" not in paths
assert not any(p.startswith("/dashboard") for p in paths)

async def test_module_static_mounts_become_app_routes(
self,
Expand Down Expand Up @@ -203,7 +205,9 @@ async def test_empty_env_var_uses_fallback(self, monkeypatch, tmp_path):
class TestRouteRegistration:
async def test_expected_routes_registered(self, app: FastAPI):
"""All modules should have their routes registered in the app."""
route_paths = [r.path for r in app.routes if hasattr(r, "path")]
from simple_module_test import effective_route_paths

route_paths = effective_route_paths(app)

assert "/health" in route_paths
assert "/health/live" in route_paths
Expand All @@ -216,8 +220,9 @@ async def test_expected_routes_registered(self, app: FastAPI):
# landing page at "/" is owned by the host and added in host/main.py,
# which the create_app fixture doesn't run.
assert "/dashboard/" in route_paths
# Bare-prefix alias — see wire_module_routes for the X-Inertia rationale.
assert "/dashboard" in route_paths
# The bare-prefix Inertia alias ("/dashboard" without the slash) is
# registered with include_in_schema=False, so it isn't enumerable here;
# TestProtectedPages::test_dashboard_redirects_unauthenticated covers it.


class TestProtectedPages:
Expand Down
2 changes: 2 additions & 0 deletions framework/testing/simple_module_test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@

from simple_module_test.app_factory import build_test_app
from simple_module_test.fake_events import FakeEventBus, RecordedEvent
from simple_module_test.routes import effective_route_paths
from simple_module_test.session_cookie import forge_session_cookie

__all__ = [
"FakeEventBus",
"RecordedEvent",
"build_test_app",
"effective_route_paths",
"forge_session_cookie",
]
34 changes: 34 additions & 0 deletions framework/testing/simple_module_test/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Enumerate an app's effective route paths, robust to lazy router inclusion.

FastAPI 0.137 / Starlette 1.3 made ``include_router()`` lazy: an included router
now appears in ``app.routes`` as a ``_IncludedRouter`` wrapper that carries no
``.path`` and resolves its routes only at request time. Code that introspects
``app.routes`` by ``.path`` therefore no longer sees routes contributed via
``include_router`` — including every module's API/view routes and the health
router. (Routes still resolve correctly at request time; only static
introspection broke.)

:func:`effective_route_paths` reads the OpenAPI schema instead — a stable public
API that lists every schema-included route with its fully-resolved prefix — and
unions it with any top-level routes/mounts that still carry a ``.path`` directly
(e.g. ``StaticFiles`` mounts). This works across FastAPI versions and is the
supported way to assert on registered routes.
"""

from __future__ import annotations

from fastapi import FastAPI


def effective_route_paths(app: FastAPI) -> set[str]:
"""Return the set of route paths registered on ``app``.

Includes schema routes contributed via ``include_router`` (resolved through
the OpenAPI schema, so FastAPI's lazy ``_IncludedRouter`` wrappers don't hide
them) plus any top-level mounts. Routes registered with
``include_in_schema=False`` — e.g. the bare-prefix Inertia aliases — are not
listed here; assert those with a request instead.
"""
paths = set(app.openapi().get("paths", {}).keys())
paths |= {r.path for r in app.routes if getattr(r, "path", None) is not None}
return paths
13 changes: 6 additions & 7 deletions framework/testing/tests/test_app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,10 @@ async def test_returns_fastapi_instance_from_instance(self):

async def test_registers_module_routes_with_prefix(self):
"""The module's register_routes() runs and its api routes appear under the prefix."""
from simple_module_test import build_test_app
from simple_module_test import build_test_app, effective_route_paths

app = build_test_app(_EchoModule)
paths = {getattr(r, "path", None) for r in app.routes}
assert "/api/echo/ping" in paths
assert "/api/echo/ping" in effective_route_paths(app)

async def test_module_accessible_on_app_state(self):
"""The instance is stored on app.state.module so tests can poke at it."""
Expand All @@ -61,12 +60,12 @@ async def test_module_accessible_on_app_state(self):

async def test_isolates_from_other_installed_modules(self):
"""build_test_app only mounts the given module — Products/Auth routes are absent."""
from simple_module_test import build_test_app
from simple_module_test import build_test_app, effective_route_paths

app = build_test_app(_EchoModule)
paths = {getattr(r, "path", None) for r in app.routes}
assert not any(p and p.startswith("/api/products") for p in paths)
assert not any(p and p.startswith("/auth") for p in paths)
paths = effective_route_paths(app)
assert not any(p.startswith("/api/products") for p in paths)
assert not any(p.startswith("/auth") for p in paths)


# ── pytest plugin fixtures ──────────────────────────────────────────
Expand Down
Loading