Files
Solo_bot/tests/test_payments_pipeline.py
T
2026-04-14 07:19:14 +00:00

293 lines
13 KiB
Python

import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from services.payments.pipeline import (
ParsedPayment,
PipelineResult,
process_cancelled_payment,
process_success_payment,
)
class _FakeSessionContext:
"""Async context manager, который возвращает переданный session при __aenter__."""
def __init__(self, session) -> None:
self._session = session
async def __aenter__(self):
return self._session
async def __aexit__(self, exc_type, exc, tb):
return False
def _sessionmaker_returning(session):
"""Фейковый ``async_session_maker`` — CallableType -> контекст."""
return lambda: _FakeSessionContext(session)
class ProcessSuccessPaymentTests(unittest.IsolatedAsyncioTestCase):
async def test_idempotent_when_already_success(self):
"""Повторный webhook: payment.status=='success' → сразу return already_processed."""
session = SimpleNamespace(commit=AsyncMock())
parsed = ParsedPayment(payment_id="p1", tg_id=42, amount=500.0)
with (
patch("services.payments.pipeline.async_session_maker", _sessionmaker_returning(session)),
patch(
"services.payments.pipeline.get_payment_by_payment_id",
new=AsyncMock(return_value={"id": 1, "tg_id": 42, "status": "success"}),
),
patch("services.payments.pipeline.update_payment_status", new=AsyncMock()) as upd_mock,
patch("services.payments.pipeline.add_payment", new=AsyncMock()) as add_mock,
patch("services.payments.pipeline.update_balance", new=AsyncMock()) as balance_mock,
patch(
"services.payments.pipeline.send_payment_success_notification",
new=AsyncMock(),
) as notify_mock,
patch("services.payments.pipeline.invalidate_payment_cache", new=AsyncMock()),
):
result = await process_success_payment("yookassa", parsed)
self.assertTrue(result.ok)
self.assertTrue(result.already_processed)
upd_mock.assert_not_awaited()
add_mock.assert_not_awaited()
balance_mock.assert_not_awaited()
notify_mock.assert_not_awaited()
session.commit.assert_not_awaited()
async def test_pending_to_success(self):
"""Payment pending → update_payment_status(success) + balance + notify."""
session = SimpleNamespace(commit=AsyncMock())
parsed = ParsedPayment(payment_id="p1", tg_id=42, amount=500.0)
with (
patch("services.payments.pipeline.async_session_maker", _sessionmaker_returning(session)),
patch(
"services.payments.pipeline.get_payment_by_payment_id",
new=AsyncMock(return_value={"id": 7, "tg_id": 42, "status": "pending"}),
),
patch(
"services.payments.pipeline.update_payment_status",
new=AsyncMock(return_value=True),
) as upd_mock,
patch("services.payments.pipeline.add_payment", new=AsyncMock()) as add_mock,
patch("services.payments.pipeline.update_balance", new=AsyncMock()) as balance_mock,
patch(
"services.payments.pipeline.send_payment_success_notification",
new=AsyncMock(),
) as notify_mock,
patch(
"services.payments.pipeline.invalidate_payment_cache",
new=AsyncMock(),
) as cache_mock,
):
result = await process_success_payment("robokassa", parsed)
self.assertTrue(result.ok)
self.assertFalse(result.already_processed)
upd_mock.assert_awaited_once()
add_mock.assert_not_awaited()
balance_mock.assert_awaited_once_with(session, 42, 500.0)
notify_mock.assert_awaited_once()
session.commit.assert_awaited_once()
cache_mock.assert_awaited_once_with("p1")
async def test_fresh_payment_uses_add_payment(self):
"""Нет pending-записи → add_payment создаёт новую."""
session = SimpleNamespace(commit=AsyncMock())
parsed = ParsedPayment(payment_id="p2", tg_id=100, amount=750.0, currency="RUB", metadata={"k": "v"})
with (
patch("services.payments.pipeline.async_session_maker", _sessionmaker_returning(session)),
patch(
"services.payments.pipeline.get_payment_by_payment_id",
new=AsyncMock(return_value=None),
),
patch("services.payments.pipeline.update_payment_status", new=AsyncMock()) as upd_mock,
patch("services.payments.pipeline.add_payment", new=AsyncMock()) as add_mock,
patch("services.payments.pipeline.update_balance", new=AsyncMock()) as balance_mock,
patch(
"services.payments.pipeline.send_payment_success_notification",
new=AsyncMock(),
),
patch("services.payments.pipeline.invalidate_payment_cache", new=AsyncMock()),
):
result = await process_success_payment("kassai", parsed)
self.assertTrue(result.ok)
upd_mock.assert_not_awaited()
add_mock.assert_awaited_once()
add_kwargs = add_mock.await_args.kwargs
self.assertEqual(add_kwargs["tg_id"], 100)
self.assertEqual(add_kwargs["amount"], 750.0)
self.assertEqual(add_kwargs["payment_system"], "kassai")
self.assertEqual(add_kwargs["status"], "success")
self.assertEqual(add_kwargs["payment_id"], "p2")
self.assertEqual(add_kwargs["metadata"], {"k": "v"})
balance_mock.assert_awaited_once_with(session, 100, 750.0)
async def test_update_status_failure_returns_error(self):
"""update_payment_status вернул False → PipelineResult(ok=False)."""
session = SimpleNamespace(commit=AsyncMock(), rollback=AsyncMock())
parsed = ParsedPayment(payment_id="p3", tg_id=1, amount=1.0)
with (
patch("services.payments.pipeline.async_session_maker", _sessionmaker_returning(session)),
patch(
"services.payments.pipeline.get_payment_by_payment_id",
new=AsyncMock(return_value={"id": 5, "tg_id": 1, "status": "pending"}),
),
patch(
"services.payments.pipeline.update_payment_status",
new=AsyncMock(return_value=False),
),
patch("services.payments.pipeline.update_balance", new=AsyncMock()) as balance_mock,
patch("services.payments.pipeline.send_payment_success_notification", new=AsyncMock()),
patch("services.payments.pipeline.invalidate_payment_cache", new=AsyncMock()),
):
result = await process_success_payment("yoomoney", parsed)
self.assertFalse(result.ok)
self.assertIsNotNone(result.error)
balance_mock.assert_not_awaited()
session.commit.assert_not_awaited()
async def test_credit_amount_override(self):
"""cryptobot: parsed.amount=USDT-сумма, credit_amount_override=RUB."""
session = SimpleNamespace(commit=AsyncMock())
parsed = ParsedPayment(payment_id="p4", tg_id=42, amount=10.0, currency="USDT")
with (
patch("services.payments.pipeline.async_session_maker", _sessionmaker_returning(session)),
patch(
"services.payments.pipeline.get_payment_by_payment_id",
new=AsyncMock(return_value={"id": 9, "tg_id": 42, "status": "pending"}),
),
patch(
"services.payments.pipeline.update_payment_status",
new=AsyncMock(return_value=True),
),
patch("services.payments.pipeline.update_balance", new=AsyncMock()) as balance_mock,
patch("services.payments.pipeline.send_payment_success_notification", new=AsyncMock()) as notify_mock,
patch("services.payments.pipeline.invalidate_payment_cache", new=AsyncMock()),
patch(
"services.payments.pipeline.select",
return_value=SimpleNamespace(where=lambda *a, **k: SimpleNamespace(limit=lambda n: None)),
),
):
fake_payment_obj = SimpleNamespace(currency=None, original_amount=None)
session.execute = AsyncMock(return_value=SimpleNamespace(scalar_one_or_none=lambda: fake_payment_obj))
result = await process_success_payment(
"CRYPTOBOT",
parsed,
credit_amount_override=900.0,
update_currency="USDT",
update_original_amount=10.0,
)
self.assertTrue(result.ok)
balance_mock.assert_awaited_once_with(session, 42, 900.0)
notify_mock.assert_awaited_once_with(42, 900.0, session)
self.assertEqual(fake_payment_obj.currency, "USDT")
self.assertEqual(fake_payment_obj.original_amount, 10.0)
async def test_metadata_patch_passed_to_update(self):
"""metadata_patch пробрасывается в update_payment_status."""
session = SimpleNamespace(commit=AsyncMock())
parsed = ParsedPayment(payment_id="p5", tg_id=42, amount=500.0)
with (
patch("services.payments.pipeline.async_session_maker", _sessionmaker_returning(session)),
patch(
"services.payments.pipeline.get_payment_by_payment_id",
new=AsyncMock(return_value={"id": 11, "tg_id": 42, "status": "pending"}),
),
patch(
"services.payments.pipeline.update_payment_status",
new=AsyncMock(return_value=True),
) as upd_mock,
patch("services.payments.pipeline.update_balance", new=AsyncMock()),
patch("services.payments.pipeline.send_payment_success_notification", new=AsyncMock()),
patch("services.payments.pipeline.invalidate_payment_cache", new=AsyncMock()),
):
await process_success_payment("CRYPTOBOT", parsed, metadata_patch={"fx": {"rate": 90.5}})
upd_mock.assert_awaited_once()
self.assertEqual(upd_mock.await_args.kwargs["metadata_patch"], {"fx": {"rate": 90.5}})
class ProcessCancelledPaymentTests(unittest.IsolatedAsyncioTestCase):
async def test_idempotent_when_already_cancelled(self):
session = SimpleNamespace(commit=AsyncMock())
parsed = ParsedPayment(payment_id="p1", tg_id=42, amount=500.0)
with (
patch("services.payments.pipeline.async_session_maker", _sessionmaker_returning(session)),
patch(
"services.payments.pipeline.get_payment_by_payment_id",
new=AsyncMock(return_value={"id": 1, "status": "cancelled"}),
),
patch("services.payments.pipeline.update_payment_status", new=AsyncMock()) as upd,
patch("services.payments.pipeline.add_payment", new=AsyncMock()),
patch("services.payments.pipeline.invalidate_payment_cache", new=AsyncMock()),
):
result = await process_cancelled_payment("yookassa", parsed)
self.assertTrue(result.ok)
self.assertTrue(result.already_processed)
upd.assert_not_awaited()
session.commit.assert_not_awaited()
async def test_pending_to_cancelled(self):
session = SimpleNamespace(commit=AsyncMock())
parsed = ParsedPayment(payment_id="p1", tg_id=42, amount=500.0)
with (
patch("services.payments.pipeline.async_session_maker", _sessionmaker_returning(session)),
patch(
"services.payments.pipeline.get_payment_by_payment_id",
new=AsyncMock(return_value={"id": 1, "status": "pending"}),
),
patch(
"services.payments.pipeline.update_payment_status",
new=AsyncMock(return_value=True),
) as upd,
patch("services.payments.pipeline.invalidate_payment_cache", new=AsyncMock()),
):
result = await process_cancelled_payment("yookassa", parsed)
self.assertTrue(result.ok)
upd.assert_awaited_once()
session.commit.assert_awaited_once()
async def test_fresh_cancelled_uses_add_payment(self):
session = SimpleNamespace(commit=AsyncMock())
parsed = ParsedPayment(payment_id="p1", tg_id=42, amount=0.0)
with (
patch("services.payments.pipeline.async_session_maker", _sessionmaker_returning(session)),
patch(
"services.payments.pipeline.get_payment_by_payment_id",
new=AsyncMock(return_value=None),
),
patch("services.payments.pipeline.add_payment", new=AsyncMock()) as add,
patch("services.payments.pipeline.invalidate_payment_cache", new=AsyncMock()),
):
result = await process_cancelled_payment("heleket", parsed, new_status="failed")
self.assertTrue(result.ok)
add.assert_awaited_once()
self.assertEqual(add.await_args.kwargs["status"], "failed")
session.commit.assert_awaited_once()
if __name__ == "__main__":
unittest.main()