Files
remnawave-minishop/tests/test_wata_webhook.py
T

451 lines
14 KiB
Python

import asyncio
import json
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from bot.payment_providers import wata
from bot.payment_providers.wata import WataConfig, WataService
class _FakeRequest:
def __init__(self, payload):
self.headers = {}
self.remote = "127.0.0.1"
self._raw_body = json.dumps(payload).encode("utf-8")
async def read(self):
return self._raw_body
class _FakeSession:
def __init__(self):
self.commits = 0
self.rollbacks = 0
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):
self.commits += 1
async def rollback(self):
self.rollbacks += 1
def _payment(**overrides):
values = {
"payment_id": 465,
"user_id": 748116183,
"status": "pending_wata",
"amount": 100.0,
"provider_payment_id": "link-id",
"purchased_gb": None,
"subscription_duration_months": 1,
"sale_mode": "subscription",
"user": None,
"created_at": datetime.now(timezone.utc),
}
values.update(overrides)
return SimpleNamespace(**values)
def _service(session, **config_overrides):
settings = SimpleNamespace(
DEFAULT_CURRENCY_SYMBOL="RUB",
DEFAULT_LANGUAGE="ru",
traffic_sale_mode=False,
trusted_proxies=[],
)
config_values = {
"ENABLED": True,
"API_TOKEN": "token",
"WEBHOOK_VERIFY_SIGNATURE": False,
"TRUSTED_IPS": "",
}
config_values.update(config_overrides)
return WataService(
bot=SimpleNamespace(),
settings=settings,
config=WataConfig(**config_values),
i18n=SimpleNamespace(),
async_session_factory=session,
subscription_service=SimpleNamespace(),
referral_service=SimpleNamespace(),
default_return_url="test_bot",
)
def test_wata_created_webhook_returns_ok_and_persists_transaction_id(monkeypatch):
session = _FakeSession()
payment = _payment()
updates = []
async def lookup_payment(_session, *, order_id_raw=None, provider_payment_id=None):
assert _session is session
assert order_id_raw == "465"
assert provider_payment_id == "tx-1"
return payment
async def update_provider_payment_and_status(
_session,
payment_id,
provider_payment_id,
status,
):
updates.append((payment_id, provider_payment_id, status))
monkeypatch.setattr(wata, "lookup_payment_by_order_or_provider_id", lookup_payment)
monkeypatch.setattr(
wata.payment_dal,
"update_provider_payment_and_status",
update_provider_payment_and_status,
)
response = asyncio.run(
_service(session).webhook_route(
_FakeRequest(
{
"transactionStatus": "Created",
"transactionId": "tx-1",
"orderId": "465",
"amount": 100,
"currency": "RUB",
}
)
)
)
assert response.status == 200
assert updates == [(465, "tx-1", "pending_wata")]
assert session.commits == 1
assert session.rollbacks == 0
def test_wata_created_webhook_can_find_payment_by_payment_link_id(monkeypatch):
session = _FakeSession()
payment = _payment()
lookup_calls = []
updates = []
async def lookup_payment(_session, *, order_id_raw=None, provider_payment_id=None):
lookup_calls.append((order_id_raw, provider_payment_id))
if provider_payment_id == "link-id":
return payment
return None
async def update_provider_payment_and_status(
_session,
payment_id,
provider_payment_id,
status,
):
updates.append((payment_id, provider_payment_id, status))
monkeypatch.setattr(wata, "lookup_payment_by_order_or_provider_id", lookup_payment)
monkeypatch.setattr(
wata.payment_dal,
"update_provider_payment_and_status",
update_provider_payment_and_status,
)
response = asyncio.run(
_service(session).webhook_route(
_FakeRequest(
{
"transactionStatus": "Created",
"transactionId": "tx-1",
"paymentLinkId": "link-id",
"amount": 100,
"currency": "RUB",
}
)
)
)
assert response.status == 200
assert lookup_calls == [(None, "tx-1"), (None, "link-id")]
assert updates == [(465, "tx-1", "pending_wata")]
assert session.commits == 1
def test_wata_known_payment_with_unknown_status_still_acknowledges_webhook(monkeypatch):
session = _FakeSession()
payment = _payment(provider_payment_id="tx-1")
async def lookup_payment(_session, *, order_id_raw=None, provider_payment_id=None):
return payment
async def update_provider_payment_and_status(*args, **kwargs):
raise AssertionError("unknown statuses must not mutate payment state")
monkeypatch.setattr(wata, "lookup_payment_by_order_or_provider_id", lookup_payment)
monkeypatch.setattr(
wata.payment_dal,
"update_provider_payment_and_status",
update_provider_payment_and_status,
)
response = asyncio.run(
_service(session).webhook_route(
_FakeRequest(
{
"transactionStatus": "WaitingForBank",
"transactionId": "tx-1",
"orderId": "465",
}
)
)
)
assert response.status == 200
assert session.commits == 0
def test_wata_refresh_finds_paid_transaction_by_order_id_and_finalizes(monkeypatch):
session = _FakeSession()
payment = _payment(provider="wata", provider_payment_id="link-id")
updates = []
finalized = []
service = _service(session)
async def search_transactions(*, order_id=None, payment_link_id=None, status=None, limit=5):
assert order_id == "465"
assert payment_link_id is None
assert status == "Paid"
assert limit == 5
return True, {
"items": [
{
"id": "tx-paid",
"status": "Paid",
"orderId": "465",
"amount": 100,
"currency": "RUB",
"paymentLinkId": "link-id",
}
]
}
async def get_payment_by_db_id(_session, payment_id):
assert _session is session
assert payment_id == 465
return payment
async def update_provider_payment_and_status(
_session,
payment_id,
provider_payment_id,
status,
):
updates.append((payment_id, provider_payment_id, status))
payment.provider_payment_id = provider_payment_id
payment.status = status
async def finalize_successful_payment(request):
finalized.append(
(
request.payment.payment_id,
request.provider_subscription,
request.provider_notification,
)
)
return SimpleNamespace()
service.search_transactions = search_transactions
monkeypatch.setattr(wata.payment_dal, "get_payment_by_db_id", get_payment_by_db_id)
monkeypatch.setattr(
wata.payment_dal,
"update_provider_payment_and_status",
update_provider_payment_and_status,
)
monkeypatch.setattr(wata, "finalize_successful_payment", finalize_successful_payment)
result = asyncio.run(service.refresh_payment_status(session, payment))
assert result is payment
assert updates == [(465, "tx-paid", "succeeded")]
assert finalized == [(465, "wata", "wata")]
assert session.commits == 1
def test_try_reuse_pending_link_returns_url_for_opened_link():
service = _service(_FakeSession())
payment = _payment(provider_payment_id="link-id")
future_iso = "2099-01-01T00:00:00Z"
async def fake_get_payment_link(payment_link_id):
assert payment_link_id == "link-id"
return True, {
"id": "link-id",
"status": "Opened",
"url": "https://wata.pro/p/link-id",
"expirationDateTime": future_iso,
}
service.get_payment_link = fake_get_payment_link
url = asyncio.run(service.try_reuse_pending_link(payment))
assert url == "https://wata.pro/p/link-id"
def test_try_reuse_pending_link_returns_none_for_closed_link():
service = _service(_FakeSession())
payment = _payment(provider_payment_id="link-id")
async def fake_get_payment_link(payment_link_id):
return True, {
"id": "link-id",
"status": "Closed",
"url": "https://wata.pro/p/link-id",
"expirationDateTime": "2099-01-01T00:00:00Z",
}
service.get_payment_link = fake_get_payment_link
url = asyncio.run(service.try_reuse_pending_link(payment))
assert url is None
def test_try_reuse_pending_link_returns_none_for_expired_link():
service = _service(_FakeSession())
payment = _payment(provider_payment_id="link-id")
async def fake_get_payment_link(payment_link_id):
return True, {
"id": "link-id",
"status": "Opened",
"url": "https://wata.pro/p/link-id",
"expirationDateTime": "2000-01-01T00:00:00Z",
}
service.get_payment_link = fake_get_payment_link
url = asyncio.run(service.try_reuse_pending_link(payment))
assert url is None
def test_try_reuse_pending_link_returns_none_when_no_provider_payment_id():
service = _service(_FakeSession())
payment = _payment(provider_payment_id=None)
async def fake_get_payment_link(payment_link_id):
raise AssertionError("get_payment_link must not be called without provider id")
service.get_payment_link = fake_get_payment_link
url = asyncio.run(service.try_reuse_pending_link(payment))
assert url is None
def test_try_reuse_pending_link_returns_none_when_get_fails():
service = _service(_FakeSession())
payment = _payment(provider_payment_id="link-id")
async def fake_get_payment_link(payment_link_id):
return False, {"status": 429, "message": "rate_limited"}
service.get_payment_link = fake_get_payment_link
url = asyncio.run(service.try_reuse_pending_link(payment))
assert url is None
def test_create_payment_link_uses_clean_iso_expiration_without_microseconds(monkeypatch):
captured = {}
async def fake_post_json_request(session, url, *, body, headers, log_prefix, is_success):
captured["body"] = body
return True, {"id": "link-1", "url": "https://wata.pro/p/link-1"}
monkeypatch.setattr(wata, "post_json_request", fake_post_json_request)
service = _service(_FakeSession())
success, _ = asyncio.run(
service.create_payment_link(
payment_db_id=465,
amount=199.5,
currency="RUB",
description="Оплата подписки на 1 мес.",
)
)
assert success is True
expiration = captured["body"]["expirationDateTime"]
assert expiration.endswith("Z"), expiration
assert "." not in expiration, expiration
assert "+" not in expiration, expiration
expiration_dt = wata._parse_wata_datetime(expiration)
assert expiration_dt is not None
ttl_delta = expiration_dt - datetime.now(timezone.utc)
assert timedelta(minutes=14, seconds=30) <= ttl_delta <= timedelta(minutes=15, seconds=30)
def test_wata_link_ttl_uses_minutes_with_default_and_minimum():
default_config = WataConfig(ENABLED=True, API_TOKEN="token")
assert default_config.LINK_TTL_MINUTES == 15
too_short_config = WataConfig(ENABLED=True, API_TOKEN="token", LINK_TTL_MINUTES=10)
assert too_short_config.LINK_TTL_MINUTES == 15
custom_config = WataConfig(ENABLED=True, API_TOKEN="token", LINK_TTL_MINUTES=45)
assert custom_config.LINK_TTL_MINUTES == 45
def test_refresh_marks_expired_wata_link_as_canceled(monkeypatch):
session = _FakeSession()
service = _service(session)
payment = _payment(
provider="wata",
provider_payment_id="link-id",
created_at=datetime.now(timezone.utc) - timedelta(minutes=16),
)
updates = []
async def find_transaction_for_payment(_payment, *, status):
return None
async def get_payment_link(payment_link_id):
assert payment_link_id == "link-id"
return True, {
"id": "link-id",
"status": "Opened",
"expirationDateTime": "2000-01-01T00:00:00Z",
}
async def update_provider_payment_and_status(
_session,
payment_id,
provider_payment_id,
status,
):
updates.append((payment_id, provider_payment_id, status))
payment.provider_payment_id = provider_payment_id
payment.status = status
async def get_payment_by_db_id(_session, payment_id):
assert payment_id == payment.payment_id
return payment
service._find_transaction_for_payment = find_transaction_for_payment
service.get_payment_link = get_payment_link
monkeypatch.setattr(
wata.payment_dal,
"update_provider_payment_and_status",
update_provider_payment_and_status,
)
monkeypatch.setattr(wata.payment_dal, "get_payment_by_db_id", get_payment_by_db_id)
result = asyncio.run(service.refresh_payment_status(session, payment))
assert result is payment
assert updates == [(465, "link-id", "canceled")]
assert session.commits == 1