import asyncio import httpx from app.controllers.v1.delivery import get_aggregator_service from app.main import create_app from app.schemas.payment import TBankPaymentNotification from app.services.aggregator import ( InvalidTBankPaymentNotificationError, TBankPaymentNotificationProcessingError, ) class StubAggregatorService: def __init__(self, *, response: str = "OK", error: Exception | None = None) -> None: self._response = response self._error = error self.calls: list[TBankPaymentNotification] = [] async def handle_tbank_payment_notification( self, notification: TBankPaymentNotification, ) -> str: self.calls.append(notification) if self._error is not None: raise self._error return self._response def _install_service_override(app, service: StubAggregatorService) -> None: async def override_service() -> StubAggregatorService: return service app.dependency_overrides[get_aggregator_service] = override_service def _valid_payload() -> dict[str, object]: return { "TerminalKey": "TestTerminal", "OrderId": "order-uuid-1", "Success": True, "Status": "CONFIRMED", "PaymentId": 8347568144, "ErrorCode": "0", "Amount": 125000, "Token": "signed-token", "Data": {"nested": "accepted by schema"}, } def _post_notification(app, payload: dict[str, object]) -> httpx.Response: async def run_request() -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url="http://testserver", ) as client: return await client.post( "/api/v1/delivery/tbank/notifications", json=payload, ) return asyncio.run(run_request()) def test_post_tbank_notification_returns_plain_text_ok_and_delegates_once() -> None: service = StubAggregatorService(response="OK") app = create_app() _install_service_override(app, service) response = _post_notification(app, _valid_payload()) assert response.status_code == 200 assert response.text == "OK" assert response.headers["content-type"].startswith("text/plain") assert service.calls == [TBankPaymentNotification(**_valid_payload())] def test_post_tbank_notification_maps_invalid_token_to_400() -> None: service = StubAggregatorService( error=InvalidTBankPaymentNotificationError("invalid token") ) app = create_app() _install_service_override(app, service) response = _post_notification(app, _valid_payload()) assert response.status_code == 400 assert response.json()["detail"]["code"] == "invalid_tbank_payment_notification" assert service.calls == [TBankPaymentNotification(**_valid_payload())] def test_post_tbank_notification_maps_processing_failure_to_503() -> None: service = StubAggregatorService( error=TBankPaymentNotificationProcessingError("temporary failure") ) app = create_app() _install_service_override(app, service) response = _post_notification(app, _valid_payload()) assert response.status_code == 503 assert ( response.json()["detail"]["code"] == "tbank_payment_notification_processing_unavailable" ) assert service.calls == [TBankPaymentNotification(**_valid_payload())] def test_post_tbank_notification_rejects_invalid_payload_without_service_call() -> None: service = StubAggregatorService() app = create_app() _install_service_override(app, service) payload = _valid_payload() del payload["Token"] response = _post_notification(app, payload) assert response.status_code == 422 assert service.calls == []