|
|
|
@@ -0,0 +1,506 @@
|
|
|
|
|
"""Contract tests for the LAVA Business provider.
|
|
|
|
|
|
|
|
|
|
The LAVA API accepts exactly one signature scheme for outgoing requests:
|
|
|
|
|
HMAC-SHA256 over the raw body bytes in the ``Signature`` HTTP header. A
|
|
|
|
|
body-embedded ``signature`` field (legacy PHP SDK style) is rejected with
|
|
|
|
|
401, so these tests pin the header form. Webhook verification is tolerant:
|
|
|
|
|
LAVA shops sign either the raw body or a sorted-keys re-serialization.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import hashlib
|
|
|
|
|
import hmac
|
|
|
|
|
import json
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
|
|
|
|
|
|
from bot.payment_providers import lava
|
|
|
|
|
from bot.payment_providers.lava import LavaConfig, LavaService
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _hmac_hex(message: bytes, key: str) -> str:
|
|
|
|
|
return hmac.new(key.encode("utf-8"), message, hashlib.sha256).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_service(**config_overrides) -> LavaService:
|
|
|
|
|
config_values = {
|
|
|
|
|
"ENABLED": True,
|
|
|
|
|
"SHOP_ID": "shop-xyz",
|
|
|
|
|
"SECRET_KEY": "outgoing-secret",
|
|
|
|
|
"WEBHOOK_SECRET": "webhook-secret",
|
|
|
|
|
}
|
|
|
|
|
config_values.update(config_overrides)
|
|
|
|
|
service = object.__new__(LavaService)
|
|
|
|
|
service.config = LavaConfig(**config_values)
|
|
|
|
|
service.settings = SimpleNamespace(
|
|
|
|
|
DEFAULT_CURRENCY_SYMBOL="RUB",
|
|
|
|
|
WEBHOOK_BASE_URL="https://bot.example.com",
|
|
|
|
|
PAYMENT_REQUEST_TIMEOUT_SECONDS=30,
|
|
|
|
|
)
|
|
|
|
|
service._default_return_url = "testbot"
|
|
|
|
|
return service
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeResponse:
|
|
|
|
|
def __init__(self, status=200, payload=None):
|
|
|
|
|
self.status = status
|
|
|
|
|
self._payload = (
|
|
|
|
|
payload
|
|
|
|
|
if payload is not None
|
|
|
|
|
else {"status": "success", "data": {"id": "inv-1", "url": "https://pay.lava.ru/x"}}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def text(self):
|
|
|
|
|
return json.dumps(self._payload)
|
|
|
|
|
|
|
|
|
|
async def __aenter__(self):
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
async def __aexit__(self, *args):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _capture_session(captured, response=None):
|
|
|
|
|
session = SimpleNamespace()
|
|
|
|
|
|
|
|
|
|
def post(url, data=None, headers=None):
|
|
|
|
|
captured["url"] = url
|
|
|
|
|
captured["data"] = data
|
|
|
|
|
captured["headers"] = headers
|
|
|
|
|
return response or _FakeResponse()
|
|
|
|
|
|
|
|
|
|
session.post = post
|
|
|
|
|
return session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeWebhookRequest:
|
|
|
|
|
def __init__(self, payload, signature=None, secret="webhook-secret"):
|
|
|
|
|
self._body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
|
|
|
self.headers = {
|
|
|
|
|
"Authorization": signature if signature is not None else _hmac_hex(self._body, secret)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async def read(self):
|
|
|
|
|
return self._body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeDbSession:
|
|
|
|
|
def __call__(self):
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
async def __aenter__(self):
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
async def __aexit__(self, exc_type, exc, tb):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
async def commit(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def rollback(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Outgoing request contract
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_payment_signs_raw_body_in_signature_header(monkeypatch):
|
|
|
|
|
service = _make_service()
|
|
|
|
|
captured = {}
|
|
|
|
|
monkeypatch.setattr(service, "_get_session", AsyncMock(return_value=_capture_session(captured)))
|
|
|
|
|
|
|
|
|
|
success, data = asyncio.run(
|
|
|
|
|
service.create_payment(payment_db_id=77, amount=150.0, currency="RUB")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert success
|
|
|
|
|
assert data == {"id": "inv-1", "url": "https://pay.lava.ru/x"}
|
|
|
|
|
assert captured["url"] == "https://api.lava.ru/business/invoice/create"
|
|
|
|
|
# Signature travels in the header and matches the exact bytes sent.
|
|
|
|
|
assert captured["headers"]["Signature"] == _hmac_hex(captured["data"], "outgoing-secret")
|
|
|
|
|
body = json.loads(captured["data"])
|
|
|
|
|
assert "signature" not in body
|
|
|
|
|
assert body["sum"] == 150.0
|
|
|
|
|
assert body["orderId"] == "77"
|
|
|
|
|
assert body["shopId"] == "shop-xyz"
|
|
|
|
|
assert body["hookUrl"] == "https://bot.example.com/webhook/lava"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_payment_keeps_payload_key_order(monkeypatch):
|
|
|
|
|
# A sorted-keys re-serialization would diverge from the signed raw body,
|
|
|
|
|
# so the wire format must preserve insertion order ("sum" before "hookUrl").
|
|
|
|
|
service = _make_service()
|
|
|
|
|
captured = {}
|
|
|
|
|
monkeypatch.setattr(service, "_get_session", AsyncMock(return_value=_capture_session(captured)))
|
|
|
|
|
|
|
|
|
|
asyncio.run(service.create_payment(payment_db_id=1, amount=10.0, currency="RUB"))
|
|
|
|
|
|
|
|
|
|
text = captured["data"].decode("utf-8")
|
|
|
|
|
assert text.find('"sum"') < text.find('"hookUrl"')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_payment_rejects_non_rub_currency(monkeypatch):
|
|
|
|
|
service = _make_service()
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
service,
|
|
|
|
|
"_get_session",
|
|
|
|
|
AsyncMock(side_effect=AssertionError("must not reach the API for unsupported currency")),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
success, data = asyncio.run(
|
|
|
|
|
service.create_payment(payment_db_id=1, amount=10.0, currency="USD")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert not success
|
|
|
|
|
assert data["message"] == "unsupported_currency"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_payment_surfaces_api_error(monkeypatch):
|
|
|
|
|
service = _make_service()
|
|
|
|
|
captured = {}
|
|
|
|
|
response = _FakeResponse(
|
|
|
|
|
status=401, payload={"status": "error", "error": "Invalid signature", "code": 401}
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
service,
|
|
|
|
|
"_get_session",
|
|
|
|
|
AsyncMock(return_value=_capture_session(captured, response=response)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
success, data = asyncio.run(
|
|
|
|
|
service.create_payment(payment_db_id=1, amount=10.0, currency="RUB")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert not success
|
|
|
|
|
assert data["message"] == "Invalid signature"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_payment_passes_optional_invoice_fields(monkeypatch):
|
|
|
|
|
service = _make_service(LIFETIME_MINUTES=90, INCLUDE_SERVICES="card, sbp")
|
|
|
|
|
captured = {}
|
|
|
|
|
monkeypatch.setattr(service, "_get_session", AsyncMock(return_value=_capture_session(captured)))
|
|
|
|
|
|
|
|
|
|
asyncio.run(
|
|
|
|
|
service.create_payment(
|
|
|
|
|
payment_db_id=5, amount=10.0, currency="RUB", description="Subscription 1m"
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
body = json.loads(captured["data"])
|
|
|
|
|
assert body["expire"] == 90
|
|
|
|
|
assert body["comment"] == "Subscription 1m"
|
|
|
|
|
assert body["includeService"] == ["card", "sbp"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_service_is_unconfigured_without_credentials():
|
|
|
|
|
assert not _make_service(SHOP_ID=None).configured
|
|
|
|
|
assert not _make_service(SECRET_KEY=None).configured
|
|
|
|
|
assert not _make_service(ENABLED=False).configured
|
|
|
|
|
assert _make_service().configured
|
|
|
|
|
assert _make_service(ENABLED=False, ADMIN_ONLY_ENABLED=True).configured
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Webhook signature verification
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_signature_accepts_raw_body_hmac():
|
|
|
|
|
service = _make_service()
|
|
|
|
|
body = b'{"order_id":"1","status":"success"}'
|
|
|
|
|
|
|
|
|
|
assert service.verify_webhook_signature(body, _hmac_hex(body, "webhook-secret"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_signature_accepts_canonical_sorted_json_hmac():
|
|
|
|
|
# Legacy PHP-SDK shops sign a sorted-keys re-serialization instead of the raw body.
|
|
|
|
|
service = _make_service()
|
|
|
|
|
body = b'{"status":"success","order_id":"1"}'
|
|
|
|
|
canonical = json.dumps(
|
|
|
|
|
{"order_id": "1", "status": "success"}, sort_keys=True, separators=(",", ":")
|
|
|
|
|
)
|
|
|
|
|
signature = _hmac_hex(canonical.encode("utf-8"), "webhook-secret")
|
|
|
|
|
|
|
|
|
|
assert service.verify_webhook_signature(body, signature)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_signature_rejects_wrong_or_empty_signature():
|
|
|
|
|
service = _make_service()
|
|
|
|
|
body = b'{"order_id":"1","status":"success"}'
|
|
|
|
|
|
|
|
|
|
assert not service.verify_webhook_signature(body, "deadbeef" * 8)
|
|
|
|
|
assert not service.verify_webhook_signature(body, "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_signature_falls_back_to_secret_key():
|
|
|
|
|
service = _make_service(WEBHOOK_SECRET=None)
|
|
|
|
|
body = b'{"order_id":"1"}'
|
|
|
|
|
|
|
|
|
|
assert service.verify_webhook_signature(body, _hmac_hex(body, "outgoing-secret"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_signature_fails_closed_without_any_secret():
|
|
|
|
|
service = _make_service(SECRET_KEY=None, WEBHOOK_SECRET=None)
|
|
|
|
|
body = b'{"order_id":"1"}'
|
|
|
|
|
|
|
|
|
|
assert not service.verify_webhook_signature(body, _hmac_hex(body, ""))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Webhook route behaviour
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _webhook_service(session, payment, monkeypatch, **overrides):
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lava,
|
|
|
|
|
"lookup_payment_by_order_or_provider_id",
|
|
|
|
|
AsyncMock(return_value=payment),
|
|
|
|
|
)
|
|
|
|
|
service = SimpleNamespace(
|
|
|
|
|
configured=True,
|
|
|
|
|
verify_webhook_signature=lambda _raw, _sig: True,
|
|
|
|
|
async_session_factory=session,
|
|
|
|
|
settings=SimpleNamespace(traffic_sale_mode=False),
|
|
|
|
|
bot=SimpleNamespace(),
|
|
|
|
|
i18n=SimpleNamespace(),
|
|
|
|
|
subscription_service=SimpleNamespace(),
|
|
|
|
|
referral_service=SimpleNamespace(),
|
|
|
|
|
)
|
|
|
|
|
for key, value in overrides.items():
|
|
|
|
|
setattr(service, key, value)
|
|
|
|
|
return service
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_invalid_signature_is_rejected():
|
|
|
|
|
service = _make_service()
|
|
|
|
|
request = _FakeWebhookRequest({"order_id": "1", "status": "success"}, signature="f" * 64)
|
|
|
|
|
|
|
|
|
|
response = asyncio.run(service.webhook_route(request))
|
|
|
|
|
|
|
|
|
|
assert response.status == 403
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_duplicate_success_does_not_finalize_again(monkeypatch):
|
|
|
|
|
session = _FakeDbSession()
|
|
|
|
|
payment = SimpleNamespace(
|
|
|
|
|
payment_id=88,
|
|
|
|
|
user_id=42,
|
|
|
|
|
status="succeeded",
|
|
|
|
|
sale_mode="subscription",
|
|
|
|
|
purchased_hwid_devices=None,
|
|
|
|
|
purchased_gb=None,
|
|
|
|
|
subscription_duration_months=1,
|
|
|
|
|
amount=150.0,
|
|
|
|
|
currency="RUB",
|
|
|
|
|
user=None,
|
|
|
|
|
)
|
|
|
|
|
service = _webhook_service(session, payment, monkeypatch)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lava.payment_dal,
|
|
|
|
|
"update_provider_payment_and_status",
|
|
|
|
|
AsyncMock(side_effect=AssertionError("duplicate webhook must not update payment")),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lava,
|
|
|
|
|
"finalize_successful_payment",
|
|
|
|
|
AsyncMock(side_effect=AssertionError("duplicate webhook must not finalize")),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
response = asyncio.run(
|
|
|
|
|
LavaService.webhook_route(
|
|
|
|
|
service,
|
|
|
|
|
_FakeWebhookRequest({"invoice_id": "inv-1", "order_id": "88", "status": "success"}),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status == 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_success_finalizes_payment(monkeypatch):
|
|
|
|
|
session = _FakeDbSession()
|
|
|
|
|
payment = SimpleNamespace(
|
|
|
|
|
payment_id=88,
|
|
|
|
|
user_id=42,
|
|
|
|
|
status="pending_lava",
|
|
|
|
|
sale_mode="subscription",
|
|
|
|
|
purchased_hwid_devices=None,
|
|
|
|
|
purchased_gb=None,
|
|
|
|
|
subscription_duration_months=1,
|
|
|
|
|
amount=150.0,
|
|
|
|
|
currency="RUB",
|
|
|
|
|
user=None,
|
|
|
|
|
)
|
|
|
|
|
service = _webhook_service(session, payment, monkeypatch)
|
|
|
|
|
update_mock = AsyncMock()
|
|
|
|
|
finalize_mock = AsyncMock(return_value=SimpleNamespace())
|
|
|
|
|
monkeypatch.setattr(lava.payment_dal, "update_provider_payment_and_status", update_mock)
|
|
|
|
|
monkeypatch.setattr(lava, "finalize_successful_payment", finalize_mock)
|
|
|
|
|
|
|
|
|
|
response = asyncio.run(
|
|
|
|
|
LavaService.webhook_route(
|
|
|
|
|
service,
|
|
|
|
|
_FakeWebhookRequest(
|
|
|
|
|
{"invoice_id": "inv-1", "order_id": "88", "status": "success", "amount": 150.0}
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status == 200
|
|
|
|
|
update_mock.assert_awaited_once_with(session, 88, "inv-1", "succeeded")
|
|
|
|
|
finalize_mock.assert_awaited_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_success_with_amount_mismatch_is_rejected(monkeypatch):
|
|
|
|
|
session = _FakeDbSession()
|
|
|
|
|
payment = SimpleNamespace(
|
|
|
|
|
payment_id=88,
|
|
|
|
|
user_id=42,
|
|
|
|
|
status="pending_lava",
|
|
|
|
|
sale_mode="subscription",
|
|
|
|
|
purchased_hwid_devices=None,
|
|
|
|
|
purchased_gb=None,
|
|
|
|
|
subscription_duration_months=1,
|
|
|
|
|
amount=150.0,
|
|
|
|
|
currency="RUB",
|
|
|
|
|
user=None,
|
|
|
|
|
)
|
|
|
|
|
service = _webhook_service(session, payment, monkeypatch)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lava.payment_dal,
|
|
|
|
|
"update_provider_payment_and_status",
|
|
|
|
|
AsyncMock(side_effect=AssertionError("mismatched amount must not update payment")),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lava,
|
|
|
|
|
"finalize_successful_payment",
|
|
|
|
|
AsyncMock(side_effect=AssertionError("mismatched amount must not finalize")),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
response = asyncio.run(
|
|
|
|
|
LavaService.webhook_route(
|
|
|
|
|
service,
|
|
|
|
|
_FakeWebhookRequest(
|
|
|
|
|
{"invoice_id": "inv-1", "order_id": "88", "status": "success", "amount": 9999}
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_failed_status_marks_payment_failed(monkeypatch):
|
|
|
|
|
session = _FakeDbSession()
|
|
|
|
|
payment = SimpleNamespace(
|
|
|
|
|
payment_id=88,
|
|
|
|
|
user_id=42,
|
|
|
|
|
status="pending_lava",
|
|
|
|
|
sale_mode="subscription",
|
|
|
|
|
purchased_hwid_devices=None,
|
|
|
|
|
purchased_gb=None,
|
|
|
|
|
subscription_duration_months=1,
|
|
|
|
|
amount=150.0,
|
|
|
|
|
currency="RUB",
|
|
|
|
|
user=None,
|
|
|
|
|
)
|
|
|
|
|
service = _webhook_service(session, payment, monkeypatch)
|
|
|
|
|
update_mock = AsyncMock()
|
|
|
|
|
notify_mock = AsyncMock()
|
|
|
|
|
monkeypatch.setattr(lava.payment_dal, "update_provider_payment_and_status", update_mock)
|
|
|
|
|
monkeypatch.setattr(lava, "notify_user_payment_failed", notify_mock)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
lava,
|
|
|
|
|
"finalize_successful_payment",
|
|
|
|
|
AsyncMock(side_effect=AssertionError("failed webhook must not finalize")),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
response = asyncio.run(
|
|
|
|
|
LavaService.webhook_route(
|
|
|
|
|
service,
|
|
|
|
|
_FakeWebhookRequest({"invoice_id": "inv-1", "order_id": "88", "status": "expired"}),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status == 200
|
|
|
|
|
update_mock.assert_awaited_once_with(session, 88, "inv-1", "failed")
|
|
|
|
|
notify_mock.assert_awaited_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_webhook_unknown_payment_returns_404(monkeypatch):
|
|
|
|
|
session = _FakeDbSession()
|
|
|
|
|
service = _webhook_service(session, None, monkeypatch)
|
|
|
|
|
|
|
|
|
|
response = asyncio.run(
|
|
|
|
|
LavaService.webhook_route(
|
|
|
|
|
service,
|
|
|
|
|
_FakeWebhookRequest({"invoice_id": "inv-x", "order_id": "404", "status": "success"}),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Pending payment reuse
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reuse_returns_url_for_pending_invoice(monkeypatch):
|
|
|
|
|
service = _make_service()
|
|
|
|
|
payment = SimpleNamespace(
|
|
|
|
|
payment_id=88,
|
|
|
|
|
provider_payment_id="inv-1",
|
|
|
|
|
provider_payment_url="https://pay.lava.ru/x",
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
service,
|
|
|
|
|
"get_invoice_status",
|
|
|
|
|
AsyncMock(return_value=(True, {"id": "inv-1", "order_id": "88", "status": "created"})),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert asyncio.run(service.try_reuse_pending_payment(payment)) == "https://pay.lava.ru/x"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reuse_rejects_terminal_or_foreign_invoices(monkeypatch):
|
|
|
|
|
service = _make_service()
|
|
|
|
|
payment = SimpleNamespace(
|
|
|
|
|
payment_id=88,
|
|
|
|
|
provider_payment_id="inv-1",
|
|
|
|
|
provider_payment_url="https://pay.lava.ru/x",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
service,
|
|
|
|
|
"get_invoice_status",
|
|
|
|
|
AsyncMock(return_value=(True, {"id": "inv-1", "order_id": "88", "status": "expired"})),
|
|
|
|
|
)
|
|
|
|
|
assert asyncio.run(service.try_reuse_pending_payment(payment)) is None
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
service,
|
|
|
|
|
"get_invoice_status",
|
|
|
|
|
AsyncMock(return_value=(True, {"id": "other", "order_id": "88", "status": "created"})),
|
|
|
|
|
)
|
|
|
|
|
assert asyncio.run(service.try_reuse_pending_payment(payment)) is None
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
service,
|
|
|
|
|
"get_invoice_status",
|
|
|
|
|
AsyncMock(return_value=(True, {"id": "inv-1", "order_id": "99", "status": "created"})),
|
|
|
|
|
)
|
|
|
|
|
assert asyncio.run(service.try_reuse_pending_payment(payment)) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reuse_requires_stored_url_and_id():
|
|
|
|
|
service = _make_service()
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
asyncio.run(
|
|
|
|
|
service.try_reuse_pending_payment(
|
|
|
|
|
SimpleNamespace(payment_id=1, provider_payment_id="", provider_payment_url="")
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
is None
|
|
|
|
|
)
|