165 lines
4.8 KiB
Python
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"
|