Files
remnawave-minishop/tests/test_auto_renew_wiring.py
T
2026-05-18 10:29:00 +03:00

284 lines
9.8 KiB
Python

"""Regression tests for the auto-renew code path in ``RenewalMixin``.
Prior to the fix, ``charge_subscription_renewal`` did::
try:
from .yookassa_service import YooKassaService # local import to avoid cycles
yk: YooKassaService = self.yookassa_service
except Exception:
yk = None
There is no ``yookassa_service`` module inside ``subscription_service_impl``
(the real implementation lives in ``bot.payment_providers.yookassa``), so the import
always raised ``ModuleNotFoundError``, ``yk`` became ``None``, and every
auto-renew silently logged ``YooKassa unavailable for auto-renew`` and
returned False — even though ``build_core_services`` had wired a real
``yookassa_service`` onto the subscription service via ``setattr``.
These tests pin the working contract end-to-end so the regression cannot
return.
"""
import unittest
from types import SimpleNamespace
from typing import Any, Dict, List, Optional
from unittest.mock import patch
from bot.services.subscription_service_impl.renewal import RenewalMixin
class _FakeYooKassaService:
"""Stand-in for the real ``YooKassaService``."""
def __init__(self, configured: bool = True, response: Optional[Dict[str, Any]] = None) -> None:
self.configured = configured
self.calls: List[Dict[str, Any]] = []
self._response = response if response is not None else {
"id": "pay-1", "status": "pending"
}
async def create_payment(self, **kwargs):
self.calls.append(kwargs)
return self._response
class _FakePaymentMethod:
def __init__(self, pm_id: str = "pm-42") -> None:
self.provider_payment_method_id = pm_id
class _FakeSub(SimpleNamespace):
pass
def _make_mixin(*, yk: Optional[_FakeYooKassaService], price_for_months: Optional[float] = 100.0):
mixin = RenewalMixin()
mixin.settings = SimpleNamespace(
traffic_sale_mode=False,
yookassa_autopayments_active=True,
subscription_options={1: price_for_months} if price_for_months else {},
)
if yk is not None:
mixin.yookassa_service = yk # type: ignore[attr-defined]
return mixin
async def _stub_default_pm(session, user_id):
return _FakePaymentMethod()
async def _no_default_pm(session, user_id):
return None
class ChargeRenewalShortCircuitTests(unittest.IsolatedAsyncioTestCase):
"""Negative paths that should return *without* hitting YooKassa."""
async def test_skips_when_traffic_sale_mode_enabled(self):
mixin = _make_mixin(yk=None)
mixin.settings.traffic_sale_mode = True
ok = await mixin.charge_subscription_renewal(session=None, sub=_FakeSub())
self.assertTrue(ok)
async def test_skips_when_auto_renew_disabled(self):
mixin = _make_mixin(yk=None)
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(auto_renew_enabled=False),
)
self.assertTrue(ok)
async def test_skips_when_autopayments_globally_disabled(self):
mixin = _make_mixin(yk=None)
mixin.settings.yookassa_autopayments_active = False
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(auto_renew_enabled=True, provider="yookassa"),
)
self.assertTrue(ok)
async def test_skips_for_non_yookassa_provider(self):
mixin = _make_mixin(yk=None)
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(auto_renew_enabled=True, provider="freekassa"),
)
self.assertTrue(ok)
class ChargeRenewalFailureTests(unittest.IsolatedAsyncioTestCase):
async def test_returns_false_when_no_saved_payment_method(self):
mixin = _make_mixin(yk=_FakeYooKassaService())
with patch(
"db.dal.user_billing_dal.get_user_default_payment_method",
_no_default_pm,
):
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(
auto_renew_enabled=True,
provider="yookassa",
user_id=1,
subscription_id=10,
duration_months=1,
),
)
self.assertFalse(ok)
async def test_returns_false_when_yookassa_service_missing(self):
# Reproduces the historic bug: build_core_services did not attach
# ``yookassa_service`` for some reason → auto-renew must report False.
mixin = _make_mixin(yk=None)
with patch(
"db.dal.user_billing_dal.get_user_default_payment_method",
_stub_default_pm,
):
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(
auto_renew_enabled=True,
provider="yookassa",
user_id=1,
subscription_id=10,
duration_months=1,
),
)
self.assertFalse(ok)
async def test_returns_false_when_yookassa_not_configured(self):
mixin = _make_mixin(yk=_FakeYooKassaService(configured=False))
with patch(
"db.dal.user_billing_dal.get_user_default_payment_method",
_stub_default_pm,
):
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(
auto_renew_enabled=True,
provider="yookassa",
user_id=1,
subscription_id=10,
duration_months=1,
),
)
self.assertFalse(ok)
async def test_returns_false_when_legacy_price_for_months_missing(self):
mixin = _make_mixin(yk=_FakeYooKassaService(), price_for_months=None)
with patch(
"db.dal.user_billing_dal.get_user_default_payment_method",
_stub_default_pm,
):
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(
auto_renew_enabled=True,
provider="yookassa",
user_id=1,
subscription_id=10,
duration_months=1,
),
)
self.assertFalse(ok)
async def test_returns_false_when_yookassa_response_unrecognized_status(self):
yk = _FakeYooKassaService(response={"id": "p", "status": "failed"})
mixin = _make_mixin(yk=yk)
with patch(
"db.dal.user_billing_dal.get_user_default_payment_method",
_stub_default_pm,
):
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(
auto_renew_enabled=True,
provider="yookassa",
user_id=1,
subscription_id=10,
duration_months=1,
),
)
self.assertFalse(ok)
class ChargeRenewalHappyPathTests(unittest.IsolatedAsyncioTestCase):
async def test_initiates_payment_with_saved_method(self):
yk = _FakeYooKassaService(response={"id": "auto-pay-7", "status": "pending"})
mixin = _make_mixin(yk=yk, price_for_months=399.0)
with patch(
"db.dal.user_billing_dal.get_user_default_payment_method",
_stub_default_pm,
):
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(
auto_renew_enabled=True,
provider="yookassa",
user_id=77,
subscription_id=555,
duration_months=1,
),
)
self.assertTrue(ok)
self.assertEqual(len(yk.calls), 1)
call = yk.calls[0]
self.assertEqual(call["amount"], 399.0)
self.assertEqual(call["currency"], "RUB")
self.assertEqual(call["payment_method_id"], "pm-42")
self.assertEqual(call["save_payment_method"], False)
self.assertEqual(call["capture"], True)
meta = call["metadata"]
self.assertEqual(meta["user_id"], "77")
self.assertEqual(meta["auto_renew_for_subscription_id"], "555")
self.assertEqual(meta["subscription_months"], "1")
async def test_accepts_waiting_for_capture_status(self):
yk = _FakeYooKassaService(response={"id": "p", "status": "waiting_for_capture"})
mixin = _make_mixin(yk=yk)
with patch(
"db.dal.user_billing_dal.get_user_default_payment_method",
_stub_default_pm,
):
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(
auto_renew_enabled=True,
provider="yookassa",
user_id=1,
subscription_id=2,
duration_months=1,
),
)
self.assertTrue(ok)
async def test_defaults_to_one_month_when_duration_missing(self):
yk = _FakeYooKassaService()
mixin = _make_mixin(yk=yk, price_for_months=99.0)
with patch(
"db.dal.user_billing_dal.get_user_default_payment_method",
_stub_default_pm,
):
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(
auto_renew_enabled=True,
provider="yookassa",
user_id=1,
subscription_id=2,
duration_months=None,
),
)
self.assertTrue(ok)
self.assertEqual(yk.calls[0]["metadata"]["subscription_months"], "1")
self.assertEqual(yk.calls[0]["amount"], 99.0)
if __name__ == "__main__": # pragma: no cover
unittest.main()