From d9a23235e5643337b12a89962b31ccd8b909c7bc Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Sun, 24 May 2026 21:36:00 +0300 Subject: [PATCH] fix: stop syncing email in panel descriptions --- backend/bot/app/web/webapp/auth.py | 1 - backend/bot/handlers/admin/sync_admin.py | 32 ++++------ backend/bot/middlewares/profile_sync.py | 1 - .../panel_identity.py | 1 - tests/test_account_linking_panel.py | 14 +++++ tests/test_admin_sync_performance.py | 51 ++++++++++++++++ tests/test_panel_identity_description.py | 20 +++++++ tests/test_profile_sync_middleware.py | 58 +++++++++++++++++++ 8 files changed, 154 insertions(+), 24 deletions(-) create mode 100644 tests/test_panel_identity_description.py diff --git a/backend/bot/app/web/webapp/auth.py b/backend/bot/app/web/webapp/auth.py index 086cc2e..08cfa67 100644 --- a/backend/bot/app/web/webapp/auth.py +++ b/backend/bot/app/web/webapp/auth.py @@ -1014,7 +1014,6 @@ def _telegram_id_for_user(user: User) -> Optional[int]: def _panel_description_for_user(user: User) -> str: lines = [ - user.email or "", user.username or "", user.first_name or "", user.last_name or "", diff --git a/backend/bot/handlers/admin/sync_admin.py b/backend/bot/handlers/admin/sync_admin.py index 49f2d46..a4f96db 100644 --- a/backend/bot/handlers/admin/sync_admin.py +++ b/backend/bot/handlers/admin/sync_admin.py @@ -143,6 +143,15 @@ def _panel_identity_update_payload(user: User, description_text: str) -> dict[st return payload +def _panel_description_for_user(user: User) -> str: + lines = [ + user.username or "", + user.first_name or "", + user.last_name or "", + ] + return "\n".join(line for line in lines if line).strip() + + def _datetime_matches(current: Optional[datetime], desired: datetime) -> bool: if current is None: return False @@ -454,17 +463,7 @@ def _panel_identity_payload_with_expiry( *, expire_at: datetime, ) -> dict[str, Any]: - description_text = "\n".join( - line - for line in [ - user.email or "", - user.username or "", - user.first_name or "", - user.last_name or "", - ] - if line - ) - payload = _panel_identity_update_payload(user, description_text) + payload = _panel_identity_update_payload(user, _panel_description_for_user(user)) payload["expireAt"] = expire_at.isoformat(timespec="milliseconds").replace("+00:00", "Z") if expire_at > datetime.now(timezone.utc): payload["status"] = "ACTIVE" @@ -1013,16 +1012,7 @@ async def _perform_sync_impl( # Ensure panel description contains Telegram fields try: if panel_uuid and existing_user and not is_duplicate_panel_identity: - description_text = "\n".join( - line - for line in [ - existing_user.email or "", - existing_user.username or "", - existing_user.first_name or "", - existing_user.last_name or "", - ] - if line - ) + description_text = _panel_description_for_user(existing_user) # Update description only when it differs from the current one on panel desired_description = description_text.strip() ( diff --git a/backend/bot/middlewares/profile_sync.py b/backend/bot/middlewares/profile_sync.py index db10400..c06786d 100644 --- a/backend/bot/middlewares/profile_sync.py +++ b/backend/bot/middlewares/profile_sync.py @@ -61,7 +61,6 @@ class ProfileSyncMiddleware(BaseMiddleware): if panel_service and db_user.panel_user_uuid: description_text = "\n".join( [ - db_user.email or "", username_for_display(tg_user.username, with_at=False) if sanitized_username is not None else "", diff --git a/backend/bot/services/subscription_service_impl/panel_identity.py b/backend/bot/services/subscription_service_impl/panel_identity.py index c71a7c2..90a9a0f 100644 --- a/backend/bot/services/subscription_service_impl/panel_identity.py +++ b/backend/bot/services/subscription_service_impl/panel_identity.py @@ -58,7 +58,6 @@ class PanelIdentityMixin: def _panel_description_for_user(self, db_user: User) -> str: lines = [ - db_user.email or "", db_user.username or "", db_user.first_name or "", db_user.last_name or "", diff --git a/tests/test_account_linking_panel.py b/tests/test_account_linking_panel.py index e5ed2df..8d1c136 100644 --- a/tests/test_account_linking_panel.py +++ b/tests/test_account_linking_panel.py @@ -8,6 +8,7 @@ from bot.app.web import subscription_webapp # noqa: F401 from bot.app.web.webapp import account as account_routes from bot.app.web.webapp.auth import ( _link_telegram_to_user, + _panel_description_for_user, _sync_merged_panel_identity_for_user, _sync_panel_identity_for_user, ) @@ -72,6 +73,19 @@ class AccountLinkingPanelTests(unittest.IsolatedAsyncioTestCase): self.assertTrue(result) panel_service.update_user_details_on_panel.assert_awaited_once() + _, payload = panel_service.update_user_details_on_panel.await_args.args[:2] + self.assertEqual(payload["description"], "alice") + self.assertEqual(payload["email"], "linked@example.com") + + def test_panel_description_for_user_excludes_email(self): + user = SimpleNamespace( + email="linked@example.com", + username="alice", + first_name="Alice", + last_name=None, + ) + + self.assertEqual(_panel_description_for_user(user), "alice\nAlice") async def test_merged_panel_identity_deletes_source_before_updating_target(self): calls = [] diff --git a/tests/test_admin_sync_performance.py b/tests/test_admin_sync_performance.py index d592da8..a6b398a 100644 --- a/tests/test_admin_sync_performance.py +++ b/tests/test_admin_sync_performance.py @@ -7,8 +7,10 @@ from bot.handlers.admin.sync_admin import ( _absorb_duplicate_panel_identity, _coerce_panel_telegram_id, _description_matches, + _panel_description_for_user, _panel_identity_matches_user, _panel_identity_needs_full_fetch, + _panel_identity_payload_with_expiry, _panel_identity_view_for_comparison, _should_update_lifetime_used_traffic, _subscription_update_delta, @@ -39,6 +41,55 @@ def test_panel_telegram_id_is_coerced_to_int(): assert _coerce_panel_telegram_id("") is None +def test_panel_description_for_user_excludes_email(): + user = SimpleNamespace( + email="linked@example.com", + username="alice", + first_name="Alice", + last_name="Smith", + ) + + assert _panel_description_for_user(user) == "alice\nAlice\nSmith" + + +def test_panel_identity_match_accepts_list_description_without_email(): + user = SimpleNamespace( + email="linked@example.com", + telegram_id=42, + username="alice", + first_name="Alice", + last_name=None, + ) + panel_user = { + "description": "alice\nAlice", + "email": "linked@example.com", + "telegramId": 42, + } + + assert _panel_identity_matches_user( + panel_user, + user, + _panel_description_for_user(user), + ) + + +def test_panel_identity_payload_with_expiry_keeps_email_out_of_description(): + expire_at = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc) + user = SimpleNamespace( + email="linked@example.com", + telegram_id=42, + username="alice", + first_name="Alice", + last_name=None, + ) + + payload = _panel_identity_payload_with_expiry(user, expire_at=expire_at) + + assert payload["description"] == "alice\nAlice" + assert payload["email"] == "linked@example.com" + assert payload["telegramId"] == 42 + + def test_panel_identity_match_treats_missing_list_email_as_unknown(): user = SimpleNamespace( email="linked@example.com", diff --git a/tests/test_panel_identity_description.py b/tests/test_panel_identity_description.py new file mode 100644 index 0000000..bda1e10 --- /dev/null +++ b/tests/test_panel_identity_description.py @@ -0,0 +1,20 @@ +from types import SimpleNamespace + +from bot.services.subscription_service_impl.panel_identity import PanelIdentityMixin + + +def test_subscription_panel_description_excludes_email(): + user = SimpleNamespace( + email="linked@example.com", + username="alice", + first_name="Alice", + last_name="Smith", + telegram_id=42, + user_id=42, + ) + + payload = PanelIdentityMixin()._panel_identity_payload_for_user(user) + + assert payload["description"] == "alice\nAlice\nSmith" + assert payload["email"] == "linked@example.com" + assert payload["telegramId"] == 42 diff --git a/tests/test_profile_sync_middleware.py b/tests/test_profile_sync_middleware.py index 33b123a..b930ccd 100644 --- a/tests/test_profile_sync_middleware.py +++ b/tests/test_profile_sync_middleware.py @@ -69,6 +69,64 @@ class ProfileSyncMiddlewareCacheTests(unittest.IsolatedAsyncioTestCase): get_user.assert_awaited_once() self.assertEqual(handler.await_count, 2) + async def test_profile_sync_keeps_email_out_of_panel_description(self): + middleware = ProfileSyncMiddleware() + handler = AsyncMock(return_value="ok") + event = SimpleNamespace() + tg_user = SimpleNamespace( + id=42, + username="alice", + first_name="Alice", + last_name="Smith", + ) + db_user = SimpleNamespace( + user_id=42, + telegram_id=42, + username="oldalice", + first_name="Old", + last_name="Smith", + email="linked@example.com", + panel_user_uuid="panel-42", + ) + panel_service = SimpleNamespace( + update_user_details_on_panel=AsyncMock(return_value={"uuid": "panel-42"}) + ) + cache_store = {} + + async def fake_get(_settings, key): + return cache_store.get(key) + + async def fake_set(_settings, key, value, ttl): + cache_store[key] = value + + data = { + "session": AsyncMock(), + "event_from_user": tg_user, + "settings": self._settings(), + "panel_service": panel_service, + } + with ( + patch.object(profile_sync_module, "cache_get_json", fake_get), + patch.object(profile_sync_module, "cache_set_json", fake_set), + patch.object( + profile_sync_module.user_dal, + "get_user_by_telegram_id", + AsyncMock(return_value=db_user), + ), + patch.object( + profile_sync_module.user_dal, + "update_user", + AsyncMock(return_value=db_user), + ), + ): + result = await middleware(handler, event, data) + + self.assertEqual(result, "ok") + panel_service.update_user_details_on_panel.assert_awaited_once() + _, payload = panel_service.update_user_details_on_panel.await_args.args[:2] + self.assertEqual(payload["description"], "alice\nAlice\nSmith") + self.assertEqual(payload["email"], "linked@example.com") + if __name__ == "__main__": unittest.main()