refactor: project architecture refactor, container splitting
This commit is contained in:
+38
-3
@@ -1,8 +1,12 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from bot.services import settings_override_service
|
||||
from config.settings import Settings
|
||||
|
||||
|
||||
@@ -67,9 +71,6 @@ class SettingsTests(unittest.TestCase):
|
||||
self.assertTrue(settings.traffic_sale_mode)
|
||||
|
||||
def test_existing_tariffs_config_disables_legacy_traffic_mode(self):
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = Path(tmpdir) / "tariffs.json"
|
||||
path.write_text(
|
||||
@@ -106,6 +107,40 @@ class SettingsTests(unittest.TestCase):
|
||||
self.assertIsNotNone(settings.tariffs_config)
|
||||
self.assertFalse(settings.traffic_sale_mode)
|
||||
|
||||
def test_appearance_backup_roundtrip_preserves_logo_theme_and_favicon_settings(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
)
|
||||
settings.WEBAPP_LOGO_URL = "/webapp-uploaded-logo/logo-1111111111111111.png"
|
||||
settings.WEBAPP_LOGO_USE_EMOJI = False
|
||||
settings.WEBAPP_LOGO_FAVICON_URL = "/webapp-favicon/aaaaaaaaaaaaaaaa/icon-180.png"
|
||||
settings.WEBAPP_FAVICON_USE_CUSTOM = True
|
||||
settings.WEBAPP_FAVICON_URL = "/webapp-favicon/bbbbbbbbbbbbbbbb/icon-180.png"
|
||||
settings.WEBAPP_PRIMARY_COLOR = "#123456"
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backup_path = Path(tmpdir) / "appearance-settings.json"
|
||||
with patch.object(
|
||||
settings_override_service,
|
||||
"APPEARANCE_OVERRIDES_BACKUP_PATH",
|
||||
backup_path,
|
||||
):
|
||||
settings_override_service.write_appearance_backup(settings)
|
||||
restored = settings_override_service._read_appearance_backup()
|
||||
|
||||
self.assertEqual(
|
||||
restored["WEBAPP_LOGO_URL"],
|
||||
"/webapp-uploaded-logo/logo-1111111111111111.png",
|
||||
)
|
||||
self.assertEqual(restored["WEBAPP_PRIMARY_COLOR"], "#123456")
|
||||
self.assertEqual(
|
||||
restored["WEBAPP_FAVICON_URL"],
|
||||
"/webapp-favicon/bbbbbbbbbbbbbbbb/icon-180.png",
|
||||
)
|
||||
|
||||
def test_trial_traffic_strategy_is_available(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import io
|
||||
import os
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from unittest.mock import patch
|
||||
|
||||
from startup_banner import print_startup_banner
|
||||
|
||||
|
||||
class StartupBannerTests(unittest.TestCase):
|
||||
def test_startup_banner_marks_services_without_mojibake(self):
|
||||
for service in ("frontend", "backend", "worker", "migrate"):
|
||||
with self.subTest(service=service):
|
||||
buffer = io.StringIO()
|
||||
|
||||
with (
|
||||
patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"IMAGE_TAG": "test-tag",
|
||||
"POSTGRES_HOST": "postgres",
|
||||
"POSTGRES_DB": "postgres",
|
||||
"REDIS_URL": "redis://redis:6379/0",
|
||||
},
|
||||
clear=False,
|
||||
),
|
||||
redirect_stdout(buffer),
|
||||
):
|
||||
print_startup_banner(service)
|
||||
|
||||
output = buffer.getvalue()
|
||||
self.assertIn(f"container :: {service.upper()}", output)
|
||||
self.assertIn("image tag :: test-tag", output)
|
||||
self.assertIn("remnawave-minishop", output)
|
||||
self.assertIn("███", output)
|
||||
self.assertNotIn("в", output)
|
||||
@@ -3,11 +3,13 @@ import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from config.settings import Settings
|
||||
from db.dal.subscription_dal import _subscription_model_payload
|
||||
|
||||
GIB = 1024**3
|
||||
|
||||
@@ -187,6 +189,59 @@ class SubscriptionServiceCalculationTests(unittest.TestCase):
|
||||
|
||||
|
||||
class SubscriptionServiceActivationDispatchTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_activate_trial_keeps_panel_strategy_out_of_local_subscription_payload(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(
|
||||
_tariffs_config_payload(),
|
||||
tmpdir,
|
||||
TRIAL_ENABLED=True,
|
||||
TRIAL_DURATION_DAYS=3,
|
||||
TRIAL_TRAFFIC_LIMIT_GB=5,
|
||||
TRIAL_TRAFFIC_STRATEGY="WEEK",
|
||||
USER_SQUAD_UUIDS="trial-squad",
|
||||
)
|
||||
service = _make_service(settings)
|
||||
service.has_had_any_subscription = AsyncMock(return_value=False)
|
||||
service._get_or_create_panel_user_link_details = AsyncMock(
|
||||
return_value=("panel-user", "panel-sub", "short", True)
|
||||
)
|
||||
service.panel_service.update_user_details_on_panel = AsyncMock(
|
||||
return_value={"subscriptionUrl": "https://example.test/sub", "shortUuid": "short"}
|
||||
)
|
||||
session = AsyncMock()
|
||||
db_user = SimpleNamespace(
|
||||
user_id=42,
|
||||
telegram_id=42,
|
||||
email=None,
|
||||
username="trial-user",
|
||||
first_name="Trial",
|
||||
last_name="User",
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.trial.user_dal.get_user_by_id",
|
||||
AsyncMock(return_value=db_user),
|
||||
),
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.trial.subscription_dal.deactivate_other_active_subscriptions",
|
||||
AsyncMock(),
|
||||
),
|
||||
patch(
|
||||
"bot.services.subscription_service_impl.trial.subscription_dal.upsert_subscription",
|
||||
AsyncMock(),
|
||||
) as upsert_subscription,
|
||||
):
|
||||
result = await service.activate_trial_subscription(session, user_id=42)
|
||||
|
||||
self.assertTrue(result["activated"])
|
||||
sub_payload = upsert_subscription.await_args.args[1]
|
||||
self.assertNotIn("traffic_limit_strategy", sub_payload)
|
||||
self.assertEqual(sub_payload["traffic_limit_bytes"], 5 * GIB)
|
||||
|
||||
panel_payload = service.panel_service.update_user_details_on_panel.await_args.args[1]
|
||||
self.assertEqual(panel_payload["trafficLimitStrategy"], "WEEK")
|
||||
|
||||
async def test_activate_subscription_dispatches_traffic_sale_mode(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
@@ -288,5 +343,21 @@ class SubscriptionServiceActivationDispatchTests(unittest.IsolatedAsyncioTestCas
|
||||
self.assertEqual(kwargs["payment_db_id"], 12)
|
||||
|
||||
|
||||
class SubscriptionDalPayloadTests(unittest.TestCase):
|
||||
def test_subscription_model_payload_drops_panel_only_keys(self):
|
||||
payload = _subscription_model_payload(
|
||||
{
|
||||
"user_id": 42,
|
||||
"panel_user_uuid": "panel-user",
|
||||
"panel_subscription_uuid": "panel-sub",
|
||||
"end_date": datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||
"traffic_limit_strategy": "WEEK",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(payload["user_id"], 42)
|
||||
self.assertNotIn("traffic_limit_strategy", payload)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
+102
-1
@@ -7,7 +7,7 @@ import unittest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from PIL import Image
|
||||
|
||||
@@ -86,6 +86,8 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertNotIn("https://telegram.org/js/telegram-web-app.js", html)
|
||||
self.assertNotIn("https://fonts.googleapis.com", html)
|
||||
self.assertNotIn('id="logo-preload"', html)
|
||||
self.assertNotIn('href=""', html)
|
||||
self.assertLess(html.index("/subscription_webapp.css"), html.index("WEBAPP_JS_SCRIPT"))
|
||||
|
||||
def test_https_webapp_logo_uses_same_origin_proxy(self):
|
||||
@@ -106,6 +108,23 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
"/webapp-uploaded-logo/logo-abcdef1234567890.png",
|
||||
)
|
||||
|
||||
async def test_webapp_logo_route_serves_configured_uploaded_logo(self):
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_LOGO_USE_EMOJI=False,
|
||||
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-1111111111111111.png",
|
||||
)
|
||||
request = SimpleNamespace(app={"settings": settings})
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
logo_dir = Path(tmpdir)
|
||||
(logo_dir / "logo-1111111111111111.png").write_bytes(b"logo")
|
||||
with patch.object(webapp_assets, "WEBAPP_UPLOADED_LOGO_DIR", logo_dir):
|
||||
response = await webapp_assets.webapp_logo_route(request)
|
||||
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(response.content_type, "image/png")
|
||||
self.assertEqual(response.body, b"logo")
|
||||
|
||||
def test_webapp_logo_is_hidden_when_emoji_logo_is_enabled(self):
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_LOGO_USE_EMOJI=True,
|
||||
@@ -172,6 +191,88 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertTrue((Path(tmpdir) / digest / "apple-touch-icon.png").exists())
|
||||
self.assertTrue((Path(tmpdir) / digest / "favicon.ico").exists())
|
||||
|
||||
def test_prune_unused_appearance_assets_keeps_only_referenced_logo_and_favicons(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
root = Path(tmpdir)
|
||||
uploads = root / "uploads"
|
||||
favicons = root / "favicons"
|
||||
emoji = root / "emoji"
|
||||
uploads.mkdir()
|
||||
favicons.mkdir()
|
||||
emoji.mkdir()
|
||||
(uploads / "logo-1111111111111111.png").write_bytes(b"keep")
|
||||
(uploads / "logo-2222222222222222.png").write_bytes(b"remove")
|
||||
(favicons / "aaaaaaaaaaaaaaaa").mkdir()
|
||||
(favicons / "bbbbbbbbbbbbbbbb").mkdir()
|
||||
(favicons / "cccccccccccccccc").mkdir()
|
||||
(favicons / "aaaaaaaaaaaaaaaa" / "icon-180.png").write_bytes(b"keep")
|
||||
(favicons / "bbbbbbbbbbbbbbbb" / "icon-180.png").write_bytes(b"keep")
|
||||
(favicons / "cccccccccccccccc" / "icon-180.png").write_bytes(b"remove")
|
||||
(emoji / "1f929.512.gif").write_bytes(b"keep")
|
||||
(emoji / "1f929.512.webp").write_bytes(b"keep")
|
||||
(emoji / "1f525.512.gif").write_bytes(b"remove")
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-1111111111111111.png",
|
||||
WEBAPP_FAVICON_URL="/webapp-favicon/aaaaaaaaaaaaaaaa/icon-180.png",
|
||||
WEBAPP_LOGO_FAVICON_URL="/webapp-favicon/bbbbbbbbbbbbbbbb/icon-180.png",
|
||||
WEBAPP_LOGO_USE_EMOJI=True,
|
||||
WEBAPP_LOGO_EMOJI="🤩",
|
||||
WEBAPP_LOGO_EMOJI_FONT="noto-color-animated",
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(admin_themes, "WEBAPP_UPLOADED_LOGO_DIR", uploads),
|
||||
patch.object(admin_themes, "WEBAPP_FAVICON_DIR", favicons),
|
||||
patch.object(admin_themes, "WEBAPP_EMOJI_CACHE_DIR", emoji),
|
||||
):
|
||||
admin_themes.prune_unused_appearance_assets(settings)
|
||||
|
||||
self.assertTrue((uploads / "logo-1111111111111111.png").exists())
|
||||
self.assertFalse((uploads / "logo-2222222222222222.png").exists())
|
||||
self.assertTrue((favicons / "aaaaaaaaaaaaaaaa").exists())
|
||||
self.assertTrue((favicons / "bbbbbbbbbbbbbbbb").exists())
|
||||
self.assertFalse((favicons / "cccccccccccccccc").exists())
|
||||
self.assertTrue((emoji / "1f929.512.gif").exists())
|
||||
self.assertTrue((emoji / "1f929.512.webp").exists())
|
||||
self.assertFalse((emoji / "1f525.512.gif").exists())
|
||||
|
||||
async def test_persist_appearance_upload_writes_overrides_and_clears_caches(self):
|
||||
settings = SimpleNamespace()
|
||||
request = SimpleNamespace(
|
||||
app={
|
||||
"settings": settings,
|
||||
"async_session_factory": object(),
|
||||
"webapp_settings_cache": {"ts": 123.0, "data": {"stale": True}},
|
||||
"webapp_logo_cache": ("url", b"body", "image/png"),
|
||||
}
|
||||
)
|
||||
updates = {
|
||||
"WEBAPP_LOGO_URL": "/webapp-uploaded-logo/logo-1111111111111111.png",
|
||||
"WEBAPP_LOGO_USE_EMOJI": False,
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
admin_themes,
|
||||
"update_overrides",
|
||||
AsyncMock(return_value={"ok": True}),
|
||||
) as update_mock,
|
||||
patch.object(admin_themes, "prune_unused_appearance_assets") as prune_mock,
|
||||
):
|
||||
persisted = await admin_themes._persist_appearance_upload(request, updates, 42)
|
||||
|
||||
self.assertTrue(persisted)
|
||||
update_mock.assert_awaited_once_with(
|
||||
settings,
|
||||
request.app["async_session_factory"],
|
||||
updates=updates,
|
||||
deletes=[],
|
||||
actor_id=42,
|
||||
)
|
||||
self.assertEqual(request.app["webapp_settings_cache"], {"ts": 0.0, "data": {}})
|
||||
self.assertIsNone(request.app["webapp_logo_cache"])
|
||||
prune_mock.assert_called_once_with(settings)
|
||||
|
||||
def test_initial_theme_head_markup_includes_css_and_tokens(self):
|
||||
cfg = builtin_webapp_themes_config("#123456")
|
||||
theme = cfg.theme_by_key("light")
|
||||
|
||||
Reference in New Issue
Block a user