Commit 228afa96 authored by George Papathanail's avatar George Papathanail
Browse files

[200~feat: wire JWKS health into /readyz

parent 7691bb71
Loading
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -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

@@ -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,
+5 −0
Original line number Diff line number Diff line
@@ -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
@@ -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)


+3 −0
Original line number Diff line number Diff line
@@ -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,
@@ -38,6 +39,7 @@ from tests.unit.fakes import (
    FakeCallbackDeliveryRepository,
    FakeCallbackRegistrationRepository,
    FakeDataBus,
    FakeJWKSCache,
    FakeOperationRepository,
    FakeQodCallbackDeliveryPort,
    FakeQodSessionRepository,
@@ -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.
+9 −0
Original line number Diff line number Diff line
@@ -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)
+13 −1
Original line number Diff line number Diff line
@@ -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:
@@ -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(