mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-04-29 09:10:06 +00:00
- Add pyproject.toml with uv and ruff configuration - Pin Python version to 3.13 via .python-version - Add Makefile commands: lint, format, fix - Apply ruff formatting to entire codebase - Remove unused imports (base64 in yookassa/simple_subscription) - Update .gitignore for new config files
375 lines
11 KiB
Python
375 lines
11 KiB
Python
"""Tests for WATA payment mixin."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT_DIR = Path(__file__).resolve().parents[2]
|
|
if str(ROOT_DIR) not in sys.path:
|
|
sys.path.insert(0, str(ROOT_DIR))
|
|
|
|
import app.services.payment_service as payment_service_module
|
|
from app.config import settings
|
|
from app.services.payment_service import PaymentService
|
|
from app.services.wata_service import WataService
|
|
|
|
|
|
@pytest.fixture
|
|
def anyio_backend() -> str:
|
|
return 'asyncio'
|
|
|
|
|
|
class DummySession:
|
|
async def commit(self) -> None: # pragma: no cover - no logic required
|
|
return None
|
|
|
|
async def refresh(self, *_: Any) -> None: # pragma: no cover - no logic required
|
|
return None
|
|
|
|
|
|
class DummyLocalPayment:
|
|
def __init__(self, payment_id: int = 42) -> None:
|
|
self.id = payment_id
|
|
self.created_at = datetime.utcnow()
|
|
|
|
|
|
class StubWataService:
|
|
def __init__(self, response: dict[str, Any] | None) -> None:
|
|
self.response = response
|
|
self.calls: list[dict[str, Any]] = []
|
|
|
|
async def create_payment_link(self, **kwargs: Any) -> dict[str, Any] | None:
|
|
self.calls.append(kwargs)
|
|
return self.response
|
|
|
|
|
|
class DummyWataPayment:
|
|
def __init__(self) -> None:
|
|
self.id = 1
|
|
self.user_id = 42
|
|
self.payment_link_id = 'link-123'
|
|
self.order_id = 'order-123'
|
|
self.amount_kopeks = 15_000
|
|
self.currency = 'RUB'
|
|
self.description = 'Пополнение'
|
|
self.status = 'Opened'
|
|
self.is_paid = False
|
|
self.metadata_json: dict[str, Any] = {}
|
|
self.transaction_id: int | None = None
|
|
self.callback_payload: dict[str, Any] | None = None
|
|
self.terminal_public_id: str | None = None
|
|
|
|
|
|
def _make_service(stub: StubWataService | None) -> PaymentService:
|
|
service = PaymentService.__new__(PaymentService) # type: ignore[call-arg]
|
|
service.bot = None
|
|
service.wata_service = stub
|
|
service.mulenpay_service = None
|
|
service.pal24_service = None
|
|
service.yookassa_service = None
|
|
service.stars_service = None
|
|
service.cryptobot_service = None
|
|
service.heleket_service = None
|
|
return service
|
|
|
|
|
|
def test_wata_service_format_datetime_accepts_naive_utc() -> None:
|
|
value = datetime(2024, 5, 20, 12, 30, 0)
|
|
formatted = WataService._format_datetime(value)
|
|
assert formatted == '2024-05-20T12:30:00Z'
|
|
|
|
|
|
def test_wata_service_parse_datetime_returns_naive_utc() -> None:
|
|
parsed = WataService._parse_datetime('2024-05-20T12:30:00Z')
|
|
assert parsed == datetime(2024, 5, 20, 12, 30, 0)
|
|
assert parsed.tzinfo is None
|
|
|
|
parsed_with_offset = WataService._parse_datetime('2024-05-20T15:30:00+03:00')
|
|
assert parsed_with_offset == datetime(2024, 5, 20, 12, 30, 0)
|
|
assert parsed_with_offset.tzinfo is None
|
|
|
|
|
|
@pytest.mark.anyio('asyncio')
|
|
async def test_create_wata_payment_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
response = {
|
|
'id': '123e4567-e89b-12d3-a456-426614174000',
|
|
'url': 'https://wata.example/link',
|
|
'status': 'Opened',
|
|
'type': 'OneTime',
|
|
'terminalPublicId': 'terminal-id',
|
|
'successRedirectUrl': 'https://example.com/success',
|
|
'failRedirectUrl': 'https://example.com/fail',
|
|
'expirationDateTime': '2030-01-01T00:00:00Z',
|
|
}
|
|
stub = StubWataService(response)
|
|
service = _make_service(stub)
|
|
db = DummySession()
|
|
|
|
captured_args: dict[str, Any] = {}
|
|
|
|
async def fake_create_wata_payment(**kwargs: Any) -> DummyLocalPayment:
|
|
captured_args.update(kwargs)
|
|
return DummyLocalPayment(payment_id=777)
|
|
|
|
monkeypatch.setattr(payment_service_module, 'create_wata_payment', fake_create_wata_payment, raising=False)
|
|
monkeypatch.setattr(settings, 'WATA_MIN_AMOUNT_KOPEKS', 5000, raising=False)
|
|
monkeypatch.setattr(settings, 'WATA_MAX_AMOUNT_KOPEKS', 500_000, raising=False)
|
|
|
|
result = await service.create_wata_payment(
|
|
db=db,
|
|
user_id=101,
|
|
amount_kopeks=15000,
|
|
description='Пополнение',
|
|
language='ru',
|
|
)
|
|
|
|
assert result is not None
|
|
assert result['local_payment_id'] == 777
|
|
assert result['payment_link_id'] == response['id']
|
|
assert result['payment_url'] == response['url']
|
|
assert captured_args['user_id'] == 101
|
|
assert captured_args['amount_kopeks'] == 15000
|
|
assert captured_args['payment_link_id'] == response['id']
|
|
assert stub.calls and stub.calls[0]['amount_kopeks'] == 15000
|
|
|
|
|
|
@pytest.mark.anyio('asyncio')
|
|
async def test_create_wata_payment_respects_amount_limits(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
stub = StubWataService({'id': 'link'})
|
|
service = _make_service(stub)
|
|
db = DummySession()
|
|
|
|
monkeypatch.setattr(settings, 'WATA_MIN_AMOUNT_KOPEKS', 10_000, raising=False)
|
|
monkeypatch.setattr(settings, 'WATA_MAX_AMOUNT_KOPEKS', 20_000, raising=False)
|
|
|
|
too_low = await service.create_wata_payment(
|
|
db=db,
|
|
user_id=1,
|
|
amount_kopeks=5_000,
|
|
description='Пополнение',
|
|
)
|
|
assert too_low is None
|
|
|
|
too_high = await service.create_wata_payment(
|
|
db=db,
|
|
user_id=1,
|
|
amount_kopeks=25_000,
|
|
description='Пополнение',
|
|
)
|
|
assert too_high is None
|
|
assert not stub.calls
|
|
|
|
|
|
@pytest.mark.anyio('asyncio')
|
|
async def test_create_wata_payment_returns_none_without_service() -> None:
|
|
service = _make_service(None)
|
|
db = DummySession()
|
|
|
|
result = await service.create_wata_payment(
|
|
db=db,
|
|
user_id=5,
|
|
amount_kopeks=10_000,
|
|
description='Пополнение',
|
|
)
|
|
assert result is None
|
|
|
|
|
|
@pytest.mark.anyio('asyncio')
|
|
async def test_process_wata_webhook_updates_status(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
service = _make_service(None)
|
|
db = DummySession()
|
|
payment = DummyWataPayment()
|
|
update_kwargs: dict[str, Any] = {}
|
|
link_lookup_called = False
|
|
|
|
async def fake_get_by_order_id(db_arg: Any, order_id: str) -> DummyWataPayment:
|
|
assert db_arg is db
|
|
assert order_id == payment.order_id
|
|
return payment
|
|
|
|
async def fake_get_by_link_id(*_: Any, **__: Any) -> DummyWataPayment | None:
|
|
nonlocal link_lookup_called
|
|
link_lookup_called = True
|
|
return None
|
|
|
|
async def fake_update_status(
|
|
db_arg: Any,
|
|
*,
|
|
payment: DummyWataPayment,
|
|
**kwargs: Any,
|
|
) -> DummyWataPayment:
|
|
assert db_arg is db
|
|
update_kwargs.update(kwargs)
|
|
if 'status' in kwargs:
|
|
payment.status = kwargs['status']
|
|
if 'is_paid' in kwargs:
|
|
payment.is_paid = kwargs['is_paid']
|
|
if 'metadata' in kwargs:
|
|
payment.metadata_json = kwargs['metadata']
|
|
if 'callback_payload' in kwargs:
|
|
payment.callback_payload = kwargs['callback_payload']
|
|
if 'terminal_public_id' in kwargs:
|
|
payment.terminal_public_id = kwargs['terminal_public_id']
|
|
return payment
|
|
|
|
monkeypatch.setattr(
|
|
payment_service_module,
|
|
'get_wata_payment_by_order_id',
|
|
fake_get_by_order_id,
|
|
raising=False,
|
|
)
|
|
monkeypatch.setattr(
|
|
payment_service_module,
|
|
'get_wata_payment_by_link_id',
|
|
fake_get_by_link_id,
|
|
raising=False,
|
|
)
|
|
monkeypatch.setattr(
|
|
payment_service_module,
|
|
'update_wata_payment_status',
|
|
fake_update_status,
|
|
raising=False,
|
|
)
|
|
|
|
payload = {
|
|
'orderId': payment.order_id,
|
|
'transactionStatus': 'Declined',
|
|
'terminalPublicId': 'terminal-001',
|
|
}
|
|
|
|
processed = await service.process_wata_webhook(db, payload)
|
|
|
|
assert processed is True
|
|
assert link_lookup_called is False
|
|
assert payment.status == 'Declined'
|
|
assert payment.is_paid is False
|
|
assert payment.metadata_json.get('last_webhook') == payload
|
|
assert payment.callback_payload == payload
|
|
assert payment.terminal_public_id == 'terminal-001'
|
|
assert update_kwargs['status'] == 'Declined'
|
|
assert update_kwargs['is_paid'] is False
|
|
|
|
|
|
@pytest.mark.anyio('asyncio')
|
|
async def test_process_wata_webhook_finalizes_paid(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
service = _make_service(None)
|
|
db = DummySession()
|
|
payment = DummyWataPayment()
|
|
finalize_called = False
|
|
|
|
async def fake_get_by_order_id(*_: Any, **__: Any) -> DummyWataPayment:
|
|
return payment
|
|
|
|
async def fake_update_status(
|
|
db_arg: Any,
|
|
*,
|
|
payment: DummyWataPayment,
|
|
**kwargs: Any,
|
|
) -> DummyWataPayment:
|
|
if 'metadata' in kwargs:
|
|
payment.metadata_json = kwargs['metadata']
|
|
if 'callback_payload' in kwargs:
|
|
payment.callback_payload = kwargs['callback_payload']
|
|
if 'status' in kwargs:
|
|
payment.status = kwargs['status']
|
|
return payment
|
|
|
|
async def fake_finalize(
|
|
db_arg: Any,
|
|
payment_arg: DummyWataPayment,
|
|
payload_arg: dict[str, Any],
|
|
) -> DummyWataPayment:
|
|
nonlocal finalize_called
|
|
finalize_called = True
|
|
payment_arg.is_paid = True
|
|
return payment_arg
|
|
|
|
monkeypatch.setattr(
|
|
payment_service_module,
|
|
'get_wata_payment_by_order_id',
|
|
fake_get_by_order_id,
|
|
raising=False,
|
|
)
|
|
monkeypatch.setattr(
|
|
payment_service_module,
|
|
'get_wata_payment_by_link_id',
|
|
lambda *args, **kwargs: None,
|
|
raising=False,
|
|
)
|
|
monkeypatch.setattr(
|
|
payment_service_module,
|
|
'update_wata_payment_status',
|
|
fake_update_status,
|
|
raising=False,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
'_finalize_wata_payment',
|
|
fake_finalize,
|
|
raising=False,
|
|
)
|
|
|
|
payload = {
|
|
'orderId': payment.order_id,
|
|
'transactionStatus': 'Paid',
|
|
'transactionId': 'tx-001',
|
|
}
|
|
|
|
processed = await service.process_wata_webhook(db, payload)
|
|
|
|
assert processed is True
|
|
assert finalize_called is True
|
|
assert payment.is_paid is True
|
|
assert payment.metadata_json.get('last_webhook') == payload
|
|
|
|
|
|
@pytest.mark.anyio('asyncio')
|
|
async def test_process_wata_webhook_returns_false_when_payment_missing(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service = _make_service(None)
|
|
db = DummySession()
|
|
|
|
async def fake_get_by_order_id(*_: Any, **__: Any) -> None:
|
|
return None
|
|
|
|
async def fake_get_by_link_id(*_: Any, **__: Any) -> None:
|
|
return None
|
|
|
|
async def fail_update(*_: Any, **__: Any) -> None:
|
|
pytest.fail('update_wata_payment_status should not be called')
|
|
|
|
monkeypatch.setattr(
|
|
payment_service_module,
|
|
'get_wata_payment_by_order_id',
|
|
fake_get_by_order_id,
|
|
raising=False,
|
|
)
|
|
monkeypatch.setattr(
|
|
payment_service_module,
|
|
'get_wata_payment_by_link_id',
|
|
fake_get_by_link_id,
|
|
raising=False,
|
|
)
|
|
monkeypatch.setattr(
|
|
payment_service_module,
|
|
'update_wata_payment_status',
|
|
fail_update,
|
|
raising=False,
|
|
)
|
|
|
|
payload = {
|
|
'orderId': 'missing-order',
|
|
'transactionStatus': 'Paid',
|
|
}
|
|
|
|
processed = await service.process_wata_webhook(db, payload)
|
|
|
|
assert processed is False
|