fix: harden hwid provider payment edge cases

This commit is contained in:
3252a8
2026-05-27 18:31:14 +03:00
parent f2fc335221
commit 75586d8883
16 changed files with 444 additions and 25 deletions
+179
View File
@@ -0,0 +1,179 @@
import asyncio
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock
from bot.payment_providers import cryptopay, severpay, stars
class _FakeSession:
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
class _FakeJsonRequest:
def __init__(self, payload):
self._payload = payload
async def json(self):
return self._payload
def test_cryptopay_duplicate_success_webhook_does_not_finalize_again(monkeypatch):
session = _FakeSession()
payment = SimpleNamespace(payment_id=77, status="succeeded")
update = SimpleNamespace(
payload=SimpleNamespace(
payload=json.dumps(
{
"user_id": "42",
"subscription_months": "0",
"payment_db_id": "77",
"sale_mode": "hwid_devices@standard",
}
),
invoice_id=9001,
amount=100,
asset="USDT",
)
)
app = {
"async_session_factory": session,
"bot": SimpleNamespace(),
"settings": SimpleNamespace(traffic_sale_mode=False, DEFAULT_CURRENCY_SYMBOL="RUB"),
"i18n": SimpleNamespace(),
"subscription_service": SimpleNamespace(),
"referral_service": SimpleNamespace(),
}
monkeypatch.setattr(
cryptopay.payment_dal,
"get_payment_by_db_id",
AsyncMock(return_value=payment),
)
monkeypatch.setattr(
cryptopay.payment_dal,
"update_provider_payment_and_status",
AsyncMock(side_effect=AssertionError("duplicate webhook must not update payment")),
)
monkeypatch.setattr(
cryptopay,
"finalize_successful_payment",
AsyncMock(side_effect=AssertionError("duplicate webhook must not finalize")),
)
service = SimpleNamespace(settings=SimpleNamespace(traffic_sale_mode=False))
asyncio.run(cryptopay.CryptoPayService._invoice_paid_handler(service, update, app))
def test_severpay_duplicate_success_webhook_does_not_finalize_again(monkeypatch):
session = _FakeSession()
payment = SimpleNamespace(
payment_id=88,
user_id=42,
status="succeeded",
sale_mode="hwid_devices@standard",
purchased_hwid_devices=3,
purchased_gb=None,
subscription_duration_months=None,
amount=150.0,
currency="RUB",
user=None,
)
async def lookup_payment(_session, *, order_id_raw=None, provider_payment_id=None):
assert _session is session
assert order_id_raw == "88"
assert provider_payment_id == "sev-1"
return payment
monkeypatch.setattr(severpay, "lookup_payment_by_order_or_provider_id", lookup_payment)
monkeypatch.setattr(
severpay.payment_dal,
"update_provider_payment_and_status",
AsyncMock(side_effect=AssertionError("duplicate webhook must not update payment")),
)
monkeypatch.setattr(
severpay,
"finalize_successful_payment",
AsyncMock(side_effect=AssertionError("duplicate webhook must not finalize")),
)
service = SimpleNamespace(
configured=True,
_validate_signature=lambda _payload: True,
async_session_factory=session,
settings=SimpleNamespace(traffic_sale_mode=False),
bot=SimpleNamespace(),
i18n=SimpleNamespace(),
subscription_service=SimpleNamespace(),
referral_service=SimpleNamespace(),
)
response = asyncio.run(
severpay.SeverPayService.webhook_route(
service,
_FakeJsonRequest(
{
"type": "payin",
"data": {
"id": "sev-1",
"order_id": "88",
"status": "success",
},
}
),
)
)
assert response.status == 200
def test_stars_duplicate_success_message_does_not_finalize_again(monkeypatch):
session = _FakeSession()
payment = SimpleNamespace(payment_id=99, status="succeeded")
message = SimpleNamespace(
successful_payment=SimpleNamespace(provider_payment_charge_id="stars-charge-1"),
from_user=SimpleNamespace(id=42),
)
monkeypatch.setattr(
stars.payment_dal,
"get_payment_by_db_id",
AsyncMock(return_value=payment),
)
monkeypatch.setattr(
stars.payment_dal,
"update_provider_payment_and_status",
AsyncMock(side_effect=AssertionError("duplicate stars payment must not update")),
)
monkeypatch.setattr(
stars,
"finalize_successful_payment",
AsyncMock(side_effect=AssertionError("duplicate stars payment must not finalize")),
)
service = SimpleNamespace()
asyncio.run(
stars.StarsService.process_successful_payment(
service,
session=session,
message=message,
payment_db_id=99,
months=3,
stars_amount=150,
i18n_data={},
sale_mode="hwid_devices@standard",
)
)