Files
remnawave-minishop/tests/test_subscription_lifecycle_notifications.py
T

235 lines
7.2 KiB
Python

import asyncio
from datetime import datetime, timezone
from types import SimpleNamespace
from aiogram.exceptions import TelegramBadRequest
from aiogram.methods import SendMessage
from bot.services import subscription_lifecycle_notifications as lifecycle
from bot.services.subscription_lifecycle_notifications import (
SubscriptionLifecycleNotificationService,
SubscriptionNotificationStage,
)
class FakeI18n:
def gettext(self, lang_code, key, **kwargs):
messages = {
"subscription_72h_notification": "Hi {user_name}, expires on {end_date}",
"email_subscription_lifecycle_subject_before_days": "{days} days left",
"email_subscription_lifecycle_subject_before_hours": "{hours} hours left",
"email_subscription_lifecycle_subject_expired": "Expired",
"email_subscription_lifecycle_subject_expired_after": "Expired yesterday",
"email_subscription_lifecycle_subject_autorenew": "Auto-renewal tomorrow",
"email_subscription_lifecycle_intro": "Subscription notice",
"email_subscription_lifecycle_row_end_date": "Active until",
"email_subscription_lifecycle_cta": "Open dashboard",
"email_subscription_lifecycle_text_renew": "Dashboard: {url}",
"email_footer_auto": "Sent by {brand}",
}
return messages.get(key, key).format(**kwargs)
class FakeBot:
def __init__(self):
self.messages = []
async def send_message(self, chat_id, text, reply_markup=None):
self.messages.append(
{
"chat_id": chat_id,
"text": text,
"reply_markup": reply_markup,
}
)
class ChatNotFoundBot:
def __init__(self):
self.calls = []
async def send_message(self, chat_id, text, reply_markup=None):
self.calls.append((chat_id, text, reply_markup))
raise TelegramBadRequest(
method=SendMessage(chat_id=chat_id, text=text),
message="Bad Request: chat not found",
)
class FakeEmailService:
def __init__(self):
self.messages = []
async def send_rendered_email(self, *, email, content):
self.messages.append({"email": email, "content": content})
def _settings(**overrides):
return SimpleNamespace(
DEFAULT_LANGUAGE="ru",
SUBSCRIPTION_EMAIL_NOTIFICATIONS_ENABLED=True,
SUBSCRIPTION_MINI_APP_URL="https://app.example.test/",
WEBAPP_PRIMARY_COLOR="#00fe7a",
WEBAPP_TITLE="Minishop",
WEBAPP_LOGO_USE_EMOJI=False,
WEBAPP_LOGO_URL="",
email_auth_configured=True,
**overrides,
)
def _subscription():
return SimpleNamespace(
subscription_id=42,
user_id=123,
end_date=datetime(2026, 6, 1, tzinfo=timezone.utc),
)
def _user(**overrides):
return SimpleNamespace(
user_id=123,
telegram_id=555,
email="user@example.test",
language_code="ru",
first_name="Ada",
**overrides,
)
def test_send_stage_records_telegram_and_email_channel_keys(monkeypatch):
recorded = []
async def fake_has(session, subscription_id, notification_key):
return notification_key in recorded
async def fake_record(session, subscription_id, notification_key, *, sent_at=None):
recorded.append(notification_key)
monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)
bot = FakeBot()
email_service = FakeEmailService()
service = SubscriptionLifecycleNotificationService(
_settings(),
bot,
FakeI18n(),
email_service=email_service,
)
async def run():
return await service.send_stage(
object(),
_subscription(),
SubscriptionNotificationStage(
key="before_3d",
message_key="subscription_72h_notification",
days_left=3,
),
user=_user(),
telegram_markup="markup",
)
delivery = asyncio.run(run())
assert delivery.telegram_sent is True
assert delivery.email_sent is True
assert bot.messages == [
{
"chat_id": 555,
"text": "Hi Ada, expires on 2026-06-01",
"reply_markup": "markup",
}
]
assert email_service.messages[0]["email"] == "user@example.test"
assert recorded == ["before_3d:telegram", "before_3d:email"]
def test_legacy_stage_key_suppresses_only_telegram(monkeypatch):
recorded = ["before_3d"]
async def fake_has(session, subscription_id, notification_key):
return notification_key in recorded
async def fake_record(session, subscription_id, notification_key, *, sent_at=None):
recorded.append(notification_key)
monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)
bot = FakeBot()
email_service = FakeEmailService()
service = SubscriptionLifecycleNotificationService(
_settings(),
bot,
FakeI18n(),
email_service=email_service,
)
async def run():
return await service.send_stage(
object(),
_subscription(),
SubscriptionNotificationStage(
key="before_3d",
message_key="subscription_72h_notification",
days_left=3,
),
user=_user(),
telegram_markup="markup",
)
delivery = asyncio.run(run())
assert delivery.telegram_sent is False
assert delivery.email_sent is True
assert bot.messages == []
assert email_service.messages[0]["email"] == "user@example.test"
assert recorded == ["before_3d", "before_3d:email"]
def test_terminal_telegram_failure_is_recorded_to_avoid_retry_spam(monkeypatch):
recorded = []
async def fake_has(session, subscription_id, notification_key):
return notification_key in recorded
async def fake_record(session, subscription_id, notification_key, *, sent_at=None):
recorded.append(notification_key)
monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)
bot = ChatNotFoundBot()
settings = _settings()
settings.email_auth_configured = False
service = SubscriptionLifecycleNotificationService(
settings,
bot,
FakeI18n(),
)
user = _user()
user.telegram_id = 777
user.email = ""
async def run():
return await service.send_stage(
object(),
_subscription(),
SubscriptionNotificationStage(
key="before_3d",
message_key="subscription_72h_notification",
days_left=3,
),
user=user,
telegram_markup="markup",
)
delivery = asyncio.run(run())
assert delivery.telegram_sent is False
assert delivery.email_sent is False
assert bot.calls[0][0] == 777
assert recorded == ["before_3d:telegram"]