Merge branch 'dev' into feature/multicurrency
This commit is contained in:
@@ -133,6 +133,59 @@ class AdminGrantTopupTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIsNone(await service.admin_grant_topup(AsyncMock(), 1, 0))
|
||||
self.assertIsNone(await service.admin_grant_topup(AsyncMock(), 1, -10))
|
||||
|
||||
async def test_regular_unlimited_override_syncs_zero_panel_limit(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
panel_service = AsyncMock(spec=PanelApiService)
|
||||
panel_service.update_user_details_on_panel = AsyncMock(return_value={"response": {}})
|
||||
service = SubscriptionService(settings, panel_service)
|
||||
|
||||
db_user = SimpleNamespace(
|
||||
user_id=42,
|
||||
first_name="Tester",
|
||||
last_name=None,
|
||||
username="tester",
|
||||
language_code="ru",
|
||||
panel_user_uuid="panel-uuid",
|
||||
email=None,
|
||||
telegram_id=42,
|
||||
)
|
||||
sub = SimpleNamespace(
|
||||
subscription_id=7,
|
||||
user_id=42,
|
||||
panel_user_uuid="panel-uuid",
|
||||
end_date=datetime.now(timezone.utc) + timedelta(days=10),
|
||||
tariff_key="standard",
|
||||
tier_baseline_bytes=100 * (1024**3),
|
||||
topup_balance_bytes=0,
|
||||
traffic_limit_bytes=105 * (1024**3),
|
||||
traffic_used_bytes=2 * (1024**5),
|
||||
regular_bonus_bytes=0,
|
||||
regular_unlimited_override=True,
|
||||
is_throttled=True,
|
||||
hwid_device_limit=3,
|
||||
extra_hwid_devices=0,
|
||||
premium_is_limited=False,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"bot.services.subscription_service.user_dal.get_user_by_id",
|
||||
new=AsyncMock(return_value=db_user),
|
||||
),
|
||||
patch(
|
||||
"bot.services.subscription_service.subscription_dal.get_active_subscription_by_user_id",
|
||||
new=AsyncMock(return_value=sub),
|
||||
),
|
||||
):
|
||||
await service.sync_main_traffic_limit_to_panel(AsyncMock(), 42)
|
||||
|
||||
self.assertEqual(sub.traffic_limit_bytes, 0)
|
||||
self.assertFalse(sub.is_throttled)
|
||||
panel_service.update_user_details_on_panel.assert_awaited_once()
|
||||
panel_payload = panel_service.update_user_details_on_panel.await_args.args[1]
|
||||
self.assertEqual(panel_payload["trafficLimitBytes"], 0)
|
||||
|
||||
async def test_premium_grant_clears_limited_state_when_balance_covers_overuse(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(premium=True), tmpdir)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from bot.services import message_audit
|
||||
|
||||
|
||||
class MessageAuditTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_log_user_message_delivery_adds_targeted_log(self):
|
||||
calls = []
|
||||
sent_at = datetime(2026, 5, 31, tzinfo=timezone.utc)
|
||||
|
||||
async def fake_create(_session, payload):
|
||||
calls.append(payload)
|
||||
|
||||
original = message_audit.message_log_dal.create_message_log_no_commit
|
||||
message_audit.message_log_dal.create_message_log_no_commit = fake_create
|
||||
self.addCleanup(
|
||||
self._restore_create_message_log,
|
||||
original,
|
||||
)
|
||||
|
||||
await message_audit.log_user_message_delivery(
|
||||
object(),
|
||||
target_user_id=42,
|
||||
event_type="telegram_traffic_warning_sent",
|
||||
channel="telegram",
|
||||
recipient="100500",
|
||||
content="kind=regular level=90",
|
||||
timestamp=sent_at,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[
|
||||
{
|
||||
"user_id": None,
|
||||
"event_type": "telegram_traffic_warning_sent",
|
||||
"content": "channel=telegram | recipient=100500 | kind=regular level=90",
|
||||
"is_admin_event": False,
|
||||
"target_user_id": 42,
|
||||
"timestamp": sent_at,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
def _restore_create_message_log(self, original):
|
||||
message_audit.message_log_dal.create_message_log_no_commit = original
|
||||
@@ -46,6 +46,23 @@ class MiniAppUrlTests(unittest.TestCase):
|
||||
subscription_mini_app_topup_url(s, "regular"),
|
||||
"https://app.example.com/webapp?topup=regular",
|
||||
)
|
||||
self.assertEqual(
|
||||
subscription_mini_app_topup_url(s, "premium"),
|
||||
"https://app.example.com/webapp?topup=premium",
|
||||
)
|
||||
|
||||
def test_subscription_mini_app_topup_url_preserves_existing_query(self):
|
||||
s = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="x",
|
||||
POSTGRES_USER="u",
|
||||
POSTGRES_PASSWORD="p",
|
||||
SUBSCRIPTION_MINI_APP_URL="https://app.example.com/webapp?lang=ru",
|
||||
)
|
||||
self.assertEqual(
|
||||
subscription_mini_app_topup_url(s, "premium"),
|
||||
"https://app.example.com/webapp?lang=ru&topup=premium",
|
||||
)
|
||||
|
||||
def test_subscription_mini_app_path_url(self):
|
||||
s = Settings(
|
||||
|
||||
@@ -96,7 +96,7 @@ class SubscriptionServiceCalculationTests(unittest.TestCase):
|
||||
["fallback-a", "fallback-b"],
|
||||
)
|
||||
|
||||
def test_main_traffic_limit_includes_topup_bonus_and_unlimited_floor(self):
|
||||
def test_main_traffic_limit_includes_topup_bonus_and_unlimited_zero(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
service = _make_service(settings)
|
||||
@@ -117,7 +117,7 @@ class SubscriptionServiceCalculationTests(unittest.TestCase):
|
||||
regular_unlimited_override=True,
|
||||
traffic_used_bytes=2 * (1024**5),
|
||||
)
|
||||
self.assertEqual(unlimited_limit, 2 * (1024**5) + 512 * GIB)
|
||||
self.assertEqual(unlimited_limit, 0)
|
||||
|
||||
def test_premium_effective_limit_ignores_negative_balances(self):
|
||||
self.assertEqual(
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_support_ticket_closed_email_uses_user_language():
|
||||
assert '<html lang="ru"' in content.html
|
||||
assert "Ticket #7 was closed" not in content.text
|
||||
assert "Тема: Проблема с подключением" in content.text
|
||||
assert "Открыть в Mini App" in content.html
|
||||
assert "Открыть обращение" in content.html
|
||||
|
||||
|
||||
def test_support_admin_email_localizes_snapshot_rows_for_recipient():
|
||||
|
||||
@@ -34,6 +34,31 @@ def _tariffs_config_payload() -> dict:
|
||||
|
||||
|
||||
class TariffWorkerTests(unittest.IsolatedAsyncioTestCase):
|
||||
def test_topup_webapp_button_labels_do_not_mention_mini_app(self):
|
||||
class I18n:
|
||||
def gettext(self, _lang, key, **_kwargs):
|
||||
return {
|
||||
"traffic_warn_btn_topup_webapp_regular": "Top up traffic",
|
||||
"traffic_warn_btn_topup_webapp_premium": "Top up premium traffic",
|
||||
}.get(key, key)
|
||||
|
||||
worker = TariffTrafficWorker(
|
||||
settings=SimpleNamespace(SUBSCRIPTION_MINI_APP_URL="https://app.example.com"),
|
||||
session_factory=SimpleNamespace(),
|
||||
panel_service=SimpleNamespace(),
|
||||
subscription_service=SimpleNamespace(),
|
||||
bot=SimpleNamespace(),
|
||||
i18n=I18n(),
|
||||
)
|
||||
|
||||
regular = worker._traffic_topup_markup("en", "regular").inline_keyboard[0][0]
|
||||
premium = worker._traffic_topup_markup("en", "premium").inline_keyboard[0][0]
|
||||
|
||||
self.assertEqual(regular.text, "Top up traffic")
|
||||
self.assertEqual(regular.web_app.url, "https://app.example.com?topup=regular")
|
||||
self.assertEqual(premium.text, "Top up premium traffic")
|
||||
self.assertEqual(premium.web_app.url, "https://app.example.com?topup=premium")
|
||||
|
||||
async def test_db_tick_retries_deadlock_once(self):
|
||||
class FakeSession:
|
||||
def __init__(self):
|
||||
|
||||
@@ -17,6 +17,9 @@ class FakeResult:
|
||||
def scalar_one_or_none(self):
|
||||
return self._scalar_value
|
||||
|
||||
def scalar_one(self):
|
||||
return self._scalar_value
|
||||
|
||||
def scalars(self):
|
||||
return self
|
||||
|
||||
@@ -71,6 +74,53 @@ class UserDalStatisticsTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIn("TRIAL", sql)
|
||||
|
||||
|
||||
class UserDalReferralTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_get_users_referred_by_filters_with_pagination(self):
|
||||
invited = [SimpleNamespace(user_id=1001), SimpleNamespace(user_id=1002)]
|
||||
session = SimpleNamespace(execute=AsyncMock(return_value=FakeResult(invited)))
|
||||
|
||||
result = await user_dal.get_users_referred_by(session, 42, limit=2, offset=10)
|
||||
|
||||
self.assertEqual(result, invited)
|
||||
stmt = session.execute.await_args.args[0]
|
||||
sql = str(
|
||||
stmt.compile(
|
||||
dialect=postgresql.dialect(),
|
||||
compile_kwargs={"literal_binds": True},
|
||||
)
|
||||
).upper()
|
||||
self.assertIn("REFERRED_BY_ID = 42", sql)
|
||||
self.assertIn("ORDER BY", sql)
|
||||
self.assertIn("LIMIT 2", sql)
|
||||
self.assertIn("OFFSET 10", sql)
|
||||
|
||||
async def test_count_users_referred_by_counts_matching_rows(self):
|
||||
session = SimpleNamespace(execute=AsyncMock(return_value=FakeResult(3)))
|
||||
|
||||
result = await user_dal.count_users_referred_by(session, 42)
|
||||
|
||||
self.assertEqual(result, 3)
|
||||
stmt = session.execute.await_args.args[0]
|
||||
sql = str(
|
||||
stmt.compile(
|
||||
dialect=postgresql.dialect(),
|
||||
compile_kwargs={"literal_binds": True},
|
||||
)
|
||||
).upper()
|
||||
self.assertIn("COUNT", sql)
|
||||
self.assertIn("REFERRED_BY_ID = 42", sql)
|
||||
|
||||
async def test_get_referrer_for_user_uses_referred_by_id(self):
|
||||
referrer = SimpleNamespace(user_id=7)
|
||||
session = SimpleNamespace(execute=AsyncMock(return_value=FakeResult(referrer)))
|
||||
|
||||
result = await user_dal.get_referrer_for_user(
|
||||
session, SimpleNamespace(user_id=42, referred_by_id=7)
|
||||
)
|
||||
|
||||
self.assertIs(result, referrer)
|
||||
|
||||
|
||||
class UserDalMergeTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_get_panel_user_uuids_for_user_includes_subscription_fallbacks_once(self):
|
||||
user = SimpleNamespace(user_id=42, panel_user_uuid="panel-main")
|
||||
|
||||
@@ -156,6 +156,7 @@ class WebAppRouteContractTests(unittest.TestCase):
|
||||
("GET", "/api/admin/stats"): "admin_stats_route",
|
||||
("GET", "/api/admin/users"): "admin_users_list_route",
|
||||
("GET", "/api/admin/users/{user_id}"): "admin_user_detail_route",
|
||||
("GET", "/api/admin/users/{user_id}/referrals"): "admin_user_referrals_route",
|
||||
("GET", "/api/admin/users/{user_id}/avatar"): "admin_user_avatar_route",
|
||||
("POST", "/api/admin/users/{user_id}/ban"): "admin_user_ban_route",
|
||||
("POST", "/api/admin/users/{user_id}/message"): "admin_user_message_route",
|
||||
|
||||
Reference in New Issue
Block a user