Files
remnawave-minishop/tests/test_subscription_lifecycle_notifications.py
T
3252a8 101119911a refactor(webapp): remove emoji web app logo option
Drop the emoji-logo feature (and its font picker) from the Web App. Only
an uploaded/linked image logo and favicon remain; when no logo is set,
the default project logo is shown. Existing emoji-logo overrides are
ignored — the keys are gone from the manifest, so the override service
skips them and the app falls back to the default logo.

- Remove WEBAPP_LOGO_USE_EMOJI / WEBAPP_LOGO_EMOJI / WEBAPP_LOGO_EMOJI_FONT
  settings, validators, manifest entries and override/runtime plumbing.
- Strip the animated-emoji fetch/cache subsystem, the /webapp-emoji route
  and emoji branches from logo/favicon resolution; leftover emoji cache
  files are now purged on appearance save.
- Simplify BrandMark to an image-only component and drop the emoji UI
  from the admin Appearance section.
- Regenerate the demo settings manifest and clean docs, locales, nginx
  and demo data of emoji-logo references.
2026-06-03 10:55:52 +03:00

395 lines
13 KiB
Python

import asyncio
from datetime import datetime, timezone
from types import SimpleNamespace
from urllib.parse import parse_qs, urlsplit
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_intro_direct": "Direct email notice",
"email_subscription_lifecycle_intro_mirrored": "Mirrored Telegram 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_URL="",
email_auth_configured=True,
**overrides,
)
def _subscription(**overrides):
data = {
"subscription_id": 42,
"user_id": 123,
"end_date": datetime(2026, 6, 1, tzinfo=timezone.utc),
"tariff_key": "standard",
"provider": "yookassa",
"status_from_panel": "ACTIVE",
}
data.update(overrides)
return SimpleNamespace(**data)
def _user(**overrides):
data = {
"user_id": 123,
"telegram_id": 555,
"email": "user@example.test",
"language_code": "ru",
"first_name": "Ada",
}
data.update(overrides)
return SimpleNamespace(**data)
def test_send_stage_records_telegram_and_email_channel_keys(monkeypatch):
recorded = []
status_changes = []
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)
async def fake_mark_status(session, user_id, status, *, telegram_id=None, checked_at=None):
status_changes.append(
{
"user_id": user_id,
"status": status,
"telegram_id": telegram_id,
"checked_at": checked_at,
}
)
monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)
monkeypatch.setattr(lifecycle, "mark_telegram_notifications_status", fake_mark_status)
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 "Mirrored Telegram notice" in email_service.messages[0]["content"].html
assert recorded == ["before_3d:telegram", "before_3d:email"]
assert status_changes == [
{
"user_id": 123,
"status": lifecycle.TELEGRAM_NOTIFICATIONS_ENABLED,
"telegram_id": 555,
"checked_at": status_changes[0]["checked_at"],
}
]
assert status_changes[0]["checked_at"] is not None
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_unstarted_telegram_failure_marks_status_without_recording_delivery(monkeypatch):
recorded = []
status_changes = []
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)
async def fake_mark_status(session, user_id, status, *, telegram_id=None, checked_at=None):
status_changes.append(
{
"user_id": user_id,
"status": status,
"telegram_id": telegram_id,
"checked_at": checked_at,
}
)
monkeypatch.setattr(lifecycle.subscription_dal, "has_subscription_notification", fake_has)
monkeypatch.setattr(lifecycle.subscription_dal, "record_subscription_notification", fake_record)
monkeypatch.setattr(lifecycle, "mark_telegram_notifications_status", fake_mark_status)
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 == []
assert status_changes == [
{
"user_id": 123,
"status": lifecycle.TELEGRAM_NOTIFICATIONS_NEEDS_START,
"telegram_id": None,
"checked_at": None,
}
]
def test_email_only_user_gets_email_name_direct_copy_and_renewal_login_link(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,
)
user = _user(user_id=-8758169927032035, telegram_id=None, first_name=None)
async def run():
return await service.send_stage(
object(),
_subscription(user_id=user.user_id),
SubscriptionNotificationStage(
key="expired",
message_key="subscription_72h_notification",
days_left=0,
),
user=user,
telegram_markup="markup",
)
delivery = asyncio.run(run())
assert delivery.telegram_sent is False
assert delivery.email_sent is True
assert bot.messages == []
content = email_service.messages[0]["content"]
assert "Hi user@example.test, expires on 2026-06-01" in content.text
assert "User -8758169927032035" not in content.text
assert "Direct email notice" in content.html
assert "Mirrored Telegram notice" not in content.html
dashboard_line = next(
line for line in content.text.splitlines() if line.startswith("Dashboard: ")
)
url = dashboard_line.removeprefix("Dashboard: ")
parsed = urlsplit(url)
query = parse_qs(parsed.query)
assert parsed.scheme == "https"
assert parsed.netloc == "app.example.test"
assert query["login"] == ["email_code"]
assert query["login_email"] == ["user@example.test"]
assert query["after_login"] == ["renew"]
assert query["renew"] == ["1"]
assert query["renew_tariff"] == ["standard"]
assert recorded == ["expired:email"]
def test_trial_subscription_renewal_link_omits_tariff_for_default_fallback(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)
email_service = FakeEmailService()
service = SubscriptionLifecycleNotificationService(
_settings(),
FakeBot(),
FakeI18n(),
email_service=email_service,
)
user = _user(user_id=-1001, telegram_id=None, first_name=None)
async def run():
return await service.send_stage(
object(),
_subscription(
user_id=user.user_id,
tariff_key="trial-tariff",
provider="trial",
status_from_panel="TRIAL",
),
SubscriptionNotificationStage(
key="expired",
message_key="subscription_72h_notification",
days_left=0,
),
user=user,
)
asyncio.run(run())
content = email_service.messages[0]["content"]
dashboard_line = next(
line for line in content.text.splitlines() if line.startswith("Dashboard: ")
)
query = parse_qs(urlsplit(dashboard_line.removeprefix("Dashboard: ")).query)
assert "renew_tariff" not in query