refactor: project architecture refactor, container splitting

This commit is contained in:
3252a8
2026-05-17 00:01:28 +03:00
parent e0b5218037
commit 30fb774d93
367 changed files with 2609 additions and 864 deletions
+38 -3
View File
@@ -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,
+36
View File
@@ -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)
+72 -1
View File
@@ -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
View File
@@ -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")