284 lines
9.8 KiB
Python
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 one lives in ``bot.services.yookassa_service``), 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()
|