Loading src/open_exposure_gateway/api/platform/health.py +4 −2 Original line number Diff line number Diff line Loading @@ -2,8 +2,9 @@ from typing import Annotated from fastapi import APIRouter, Depends, Response, status from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.config import get_settings from open_exposure_gateway.dependencies import get_database_health, get_publisher from open_exposure_gateway.dependencies import get_database_health, get_jwks_cache, get_publisher from open_exposure_gateway.ports.databus_port import DataBusPort from open_exposure_gateway.schemas.common import HealthResponse, HealthStatus Loading @@ -25,9 +26,10 @@ async def readyz( response: Response, publisher: Annotated[DataBusPort, Depends(get_publisher)], database_is_healthy: Annotated[bool, Depends(get_database_health)], jwks_cache: Annotated[JWKSCache, Depends(get_jwks_cache)], ) -> HealthResponse: settings = get_settings() if not publisher.is_connected or not database_is_healthy: if not publisher.is_connected or not database_is_healthy or not jwks_cache.is_healthy: response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE return HealthResponse( status=HealthStatus.NOT_OK, Loading src/open_exposure_gateway/dependencies.py +5 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ from open_exposure_gateway.application.services.location_retrieval_service impor from open_exposure_gateway.application.services.quality_on_demand_service import ( QualityOnDemandService, ) from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.auth.tokens import TokenFlow, validate_token from open_exposure_gateway.core.config import get_settings from open_exposure_gateway.core.exceptions import UnauthorizedException Loading Loading @@ -82,6 +83,10 @@ def get_publisher(request: Request) -> DataBusPort: return get_app_state(request=request).publisher def get_jwks_cache(request: Request) -> JWKSCache: return get_app_state(request=request).jwks_cache _bearer_scheme = HTTPBearer(auto_error=False) Loading tests/unit/conftest.py +3 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ from open_exposure_gateway.dependencies import ( get_caller_context, get_database_health, get_edge_app_service, get_jwks_cache, get_location_retrieval_service, get_publisher, get_qod_service, Loading @@ -38,6 +39,7 @@ from tests.unit.fakes import ( FakeCallbackDeliveryRepository, FakeCallbackRegistrationRepository, FakeDataBus, FakeJWKSCache, FakeOperationRepository, FakeQodCallbackDeliveryPort, FakeQodSessionRepository, Loading Loading @@ -286,6 +288,7 @@ def api_client( app.dependency_overrides[get_location_retrieval_service] = lambda: location_retrieval_service app.dependency_overrides[get_publisher] = lambda: fake_bus app.dependency_overrides[get_database_health] = lambda: True app.dependency_overrides[get_jwks_cache] = lambda: FakeJWKSCache() app.dependency_overrides[get_caller_context] = fake_caller_context # raise_server_exceptions=False: unhandled errors surface as the 500 envelope # a real client would see, so flow tests assert status codes, not tracebacks. Loading tests/unit/fakes.py +9 −0 Original line number Diff line number Diff line Loading @@ -87,6 +87,15 @@ from open_exposure_gateway.ports.qod_callback_port import QodCallbackDeliveryPor Handler = Callable[[dict[str, Any]], Awaitable[None]] class FakeJWKSCache: def __init__(self) -> None: self.healthy = True @property def is_healthy(self) -> bool: return self.healthy class FakeDataBus: def __init__(self) -> None: self._handlers: dict[str, list[Handler]] = defaultdict(list) Loading tests/unit/test_platform_flows.py +13 −1 Original line number Diff line number Diff line Loading @@ -2,7 +2,9 @@ from fastapi.testclient import TestClient from tests.unit.fakes import FakeDataBus from open_exposure_gateway.dependencies import get_jwks_cache from open_exposure_gateway.main import app from tests.unit.fakes import FakeDataBus, FakeJWKSCache class TestReadyzFlow: Loading @@ -19,6 +21,16 @@ class TestReadyzFlow: response = api_client.get("/platform/readyz") assert response.status_code == 503 def test_unavailable_when_jwks_unhealthy(self, api_client: TestClient) -> None: """architecture.md E3: /readyz fails if Keycloak's JWKS is unreachable.""" fake_jwks = FakeJWKSCache() fake_jwks.healthy = False app.dependency_overrides[get_jwks_cache] = lambda: fake_jwks response = api_client.get("/platform/readyz") assert response.status_code == 503 class TestHealthzFlow: def test_liveness_does_not_depend_on_downstreams( Loading Loading
src/open_exposure_gateway/api/platform/health.py +4 −2 Original line number Diff line number Diff line Loading @@ -2,8 +2,9 @@ from typing import Annotated from fastapi import APIRouter, Depends, Response, status from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.config import get_settings from open_exposure_gateway.dependencies import get_database_health, get_publisher from open_exposure_gateway.dependencies import get_database_health, get_jwks_cache, get_publisher from open_exposure_gateway.ports.databus_port import DataBusPort from open_exposure_gateway.schemas.common import HealthResponse, HealthStatus Loading @@ -25,9 +26,10 @@ async def readyz( response: Response, publisher: Annotated[DataBusPort, Depends(get_publisher)], database_is_healthy: Annotated[bool, Depends(get_database_health)], jwks_cache: Annotated[JWKSCache, Depends(get_jwks_cache)], ) -> HealthResponse: settings = get_settings() if not publisher.is_connected or not database_is_healthy: if not publisher.is_connected or not database_is_healthy or not jwks_cache.is_healthy: response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE return HealthResponse( status=HealthStatus.NOT_OK, Loading
src/open_exposure_gateway/dependencies.py +5 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ from open_exposure_gateway.application.services.location_retrieval_service impor from open_exposure_gateway.application.services.quality_on_demand_service import ( QualityOnDemandService, ) from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.auth.tokens import TokenFlow, validate_token from open_exposure_gateway.core.config import get_settings from open_exposure_gateway.core.exceptions import UnauthorizedException Loading Loading @@ -82,6 +83,10 @@ def get_publisher(request: Request) -> DataBusPort: return get_app_state(request=request).publisher def get_jwks_cache(request: Request) -> JWKSCache: return get_app_state(request=request).jwks_cache _bearer_scheme = HTTPBearer(auto_error=False) Loading
tests/unit/conftest.py +3 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ from open_exposure_gateway.dependencies import ( get_caller_context, get_database_health, get_edge_app_service, get_jwks_cache, get_location_retrieval_service, get_publisher, get_qod_service, Loading @@ -38,6 +39,7 @@ from tests.unit.fakes import ( FakeCallbackDeliveryRepository, FakeCallbackRegistrationRepository, FakeDataBus, FakeJWKSCache, FakeOperationRepository, FakeQodCallbackDeliveryPort, FakeQodSessionRepository, Loading Loading @@ -286,6 +288,7 @@ def api_client( app.dependency_overrides[get_location_retrieval_service] = lambda: location_retrieval_service app.dependency_overrides[get_publisher] = lambda: fake_bus app.dependency_overrides[get_database_health] = lambda: True app.dependency_overrides[get_jwks_cache] = lambda: FakeJWKSCache() app.dependency_overrides[get_caller_context] = fake_caller_context # raise_server_exceptions=False: unhandled errors surface as the 500 envelope # a real client would see, so flow tests assert status codes, not tracebacks. Loading
tests/unit/fakes.py +9 −0 Original line number Diff line number Diff line Loading @@ -87,6 +87,15 @@ from open_exposure_gateway.ports.qod_callback_port import QodCallbackDeliveryPor Handler = Callable[[dict[str, Any]], Awaitable[None]] class FakeJWKSCache: def __init__(self) -> None: self.healthy = True @property def is_healthy(self) -> bool: return self.healthy class FakeDataBus: def __init__(self) -> None: self._handlers: dict[str, list[Handler]] = defaultdict(list) Loading
tests/unit/test_platform_flows.py +13 −1 Original line number Diff line number Diff line Loading @@ -2,7 +2,9 @@ from fastapi.testclient import TestClient from tests.unit.fakes import FakeDataBus from open_exposure_gateway.dependencies import get_jwks_cache from open_exposure_gateway.main import app from tests.unit.fakes import FakeDataBus, FakeJWKSCache class TestReadyzFlow: Loading @@ -19,6 +21,16 @@ class TestReadyzFlow: response = api_client.get("/platform/readyz") assert response.status_code == 503 def test_unavailable_when_jwks_unhealthy(self, api_client: TestClient) -> None: """architecture.md E3: /readyz fails if Keycloak's JWKS is unreachable.""" fake_jwks = FakeJWKSCache() fake_jwks.healthy = False app.dependency_overrides[get_jwks_cache] = lambda: fake_jwks response = api_client.get("/platform/readyz") assert response.status_code == 503 class TestHealthzFlow: def test_liveness_does_not_depend_on_downstreams( Loading