"""ЮKassa adapter unit tests (ticket 011). Uses respx to mock the api.yookassa.ru/v3 surface. Tests auth header, idempotency key, happy paths, and error mapping. """ from __future__ import annotations import json as _json import httpx import pytest import respx from contract_check.core.billing.errors import ( PaymentNotFoundError, PaymentProviderAuthError, PaymentProviderDeclinedError, ) from contract_check.core.billing.yookassa import YookassaProvider BASE = "https://api.yookassa.ru/v3" def _provider() -> YookassaProvider: return YookassaProvider(shop_id="123456", secret_key="test-secret", base_url=BASE) def _basic_auth() -> str: from base64 import b64encode return f"Basic {b64encode(b'123456:test-secret').decode()}" @respx.mock async def test_create_payment_happy_path() -> None: route = respx.post(f"{BASE}/payments").mock( return_value=httpx.Response( 200, json={ "id": "pay-1", "status": "pending", "confirmation": {"confirmation_url": "https://yoo.test/pay-1"}, }, ) ) provider = _provider() result = await provider.create_payment( amount_kopecks=19900, description="1 документ", return_url="https://example.com/pay/ok", metadata={"invoice_id": "inv-1"}, idempotency_key="idem-1", ) assert result.payment_id == "pay-1" assert result.confirmation_url == "https://yoo.test/pay-1" assert route.calls[0].request.headers["Idempotence-Key"] == "idem-1" assert "Basic " in route.calls[0].request.headers.get("Authorization", "") @respx.mock async def test_get_payment_returns_status() -> None: respx.get(f"{BASE}/payments/pay-2").mock( return_value=httpx.Response( 200, json={ "id": "pay-2", "status": "succeeded", "paid": True, "amount": {"value": "199.00", "currency": "RUB"}, "metadata": {"invoice_id": "inv-2"}, }, ) ) provider = _provider() info = await provider.get_payment("pay-2") assert info.payment_id == "pay-2" assert info.status == "succeeded" assert info.paid is True assert info.amount_kopecks == 19900 assert info.metadata["invoice_id"] == "inv-2" @respx.mock async def test_refund_happy_path() -> None: route = respx.post(f"{BASE}/refunds").mock( return_value=httpx.Response(200, json={"id": "ref-1", "status": "succeeded"}) ) provider = _provider() result = await provider.refund( payment_id="pay-3", amount_kopecks=19900, idempotency_key="idem-ref-1", ) assert result.refund_id == "ref-1" assert result.status == "succeeded" assert route.calls[0].request.headers["Idempotence-Key"] == "idem-ref-1" @respx.mock async def test_auth_error_maps_to_auth_exception() -> None: respx.post(f"{BASE}/payments").mock(return_value=httpx.Response(401)) provider = _provider() with pytest.raises(PaymentProviderAuthError): await provider.create_payment( amount_kopecks=19900, description="x", return_url="https://x", metadata={}, idempotency_key="i", ) @respx.mock async def test_404_maps_to_not_found() -> None: respx.get(f"{BASE}/payments/missing").mock(return_value=httpx.Response(404)) provider = _provider() with pytest.raises(PaymentNotFoundError): await provider.get_payment("missing") @respx.mock async def test_provider_declined_maps_to_declined() -> None: respx.post(f"{BASE}/payments").mock( return_value=httpx.Response(400, json={"description": "bad request"}) ) provider = _provider() with pytest.raises(PaymentProviderDeclinedError): await provider.create_payment( amount_kopecks=19900, description="x", return_url="https://x", metadata={}, idempotency_key="i", ) @respx.mock async def test_kopecks_to_rub_formatting() -> None: called_body: dict | None = None def _capture(request: httpx.Request) -> httpx.Response: nonlocal called_body called_body = _json.loads(request.content) return httpx.Response( 200, json={ "id": "pay-fmt", "status": "pending", "confirmation": {"confirmation_url": "https://yoo.test/fmt"}, }, ) respx.post(f"{BASE}/payments").mock(side_effect=_capture) provider = _provider() await provider.create_payment( amount_kopecks=49000, description="pack", return_url="https://x", metadata={}, idempotency_key="i", ) assert called_body is not None assert called_body["amount"]["value"] == "490.00" assert called_body["amount"]["currency"] == "RUB"