DealDocumentScreening/tests/unit/test_billing_yookassa.py

165 lines
4.8 KiB
Python

"""Ю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"