Skip to content
Merged
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
69 changes: 34 additions & 35 deletions backend/tests/unit/test_api_schema_exposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from app.bootstrap.app_factory import create_app
from app.config import Settings
Expand All @@ -34,31 +35,6 @@
SCHEMA_PATHS = ("/openapi.json", "/api-docs", "/api-redoc")


def _route_paths(app) -> list[str]:
"""Every path the app will actually serve.

``app.routes`` is not homogeneous. Mounted sub-applications appear as
``_IncludedRouter`` entries, which carry no ``path`` attribute, so
``route.path`` raises ``AttributeError`` and the assertion below errors out
instead of reporting a result. Read the attribute defensively and collect
the paths we can see; a schema route registered at the top level - which is
the only way FastAPI mounts one - is always directly addressable.
"""
return sorted(
path
for path in (getattr(route, "path", None) for route in app.routes)
if isinstance(path, str)
)


def _schema_routes(app) -> list[str]:
return sorted(
path
for path in _route_paths(app)
if any(marker in path for marker in ("openapi", "api-docs", "api-redoc"))
)


def test_schema_is_not_exposed_by_default():
"""The default posture must withhold the schema."""
assert Settings().EXPOSE_API_SCHEMA is False, (
Expand All @@ -71,21 +47,44 @@ def test_create_app_registers_no_schema_route_by_default(monkeypatch):
from app.config import settings as settings_singleton

monkeypatch.setattr(settings_singleton, "EXPOSE_API_SCHEMA", False)
app = create_app()
assert _schema_routes(app) == [], (
"the application registered a schema route with EXPOSE_API_SCHEMA off"
)
client = TestClient(create_app())

for path in SCHEMA_PATHS:
response = client.get(path)
assert response.status_code == 404, (
f"{path} must return 404 when EXPOSE_API_SCHEMA is off"
)


def test_create_app_still_serves_health_when_the_schema_is_withheld(monkeypatch):
"""Withholding the schema must not take down the probes deploys rely on."""
from app.bootstrap import health
from app.config import settings as settings_singleton

async def healthy_probes():
return (
{
"status": "ok",
"service": "reliastra-backend",
"version": "0.1.0",
"checks": {},
},
200,
)

# The health endpoint's DB/Redis checks are covered separately; this test
# isolates HTTP route availability while TestClient deliberately skips the
# app lifespan (and runs requests on its own event loop).
monkeypatch.setattr(health, "_run_health_checks", healthy_probes)
monkeypatch.setattr(health, "_ready_cache", {"ts": 0.0, "payload": None})
monkeypatch.setattr(settings_singleton, "EXPOSE_API_SCHEMA", False)
app = create_app()
paths = set(_route_paths(app))
assert "/health" in paths
assert "/health/ready" in paths
client = TestClient(create_app())

for path in ("/health", "/health/ready"):
response = client.get(path)
assert response.status_code == 200, (
f"{path} must remain available; got {response.status_code}: {response.text}"
)


def test_opt_in_restores_the_schema(monkeypatch):
Expand All @@ -100,8 +99,8 @@ def test_opt_in_restores_the_schema(monkeypatch):
from app.config import settings as settings_singleton

monkeypatch.setattr(settings_singleton, "EXPOSE_API_SCHEMA", True)
app = create_app()
assert _schema_routes(app) == sorted(SCHEMA_PATHS)
response = TestClient(create_app()).get("/openapi.json")
assert response.status_code == 200


def test_no_schema_path_is_committed_to_the_repository():
Expand Down
Loading