fix(admin): delete Remnawave user with bot account
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import json
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from bot.app.web.admin_api_impl import users as admin_users
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self):
|
||||
self.committed = False
|
||||
self.rolled_back = False
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
async def commit(self):
|
||||
self.committed = True
|
||||
|
||||
async def rollback(self):
|
||||
self.rolled_back = True
|
||||
|
||||
|
||||
class AdminUserDeleteRouteTests(unittest.IsolatedAsyncioTestCase):
|
||||
def _request(self, session: FakeSession, app_overrides=None):
|
||||
app = {
|
||||
"settings": SimpleNamespace(),
|
||||
"async_session_factory": lambda: session,
|
||||
}
|
||||
app.update(app_overrides or {})
|
||||
return SimpleNamespace(
|
||||
app=app,
|
||||
match_info={"user_id": "42"},
|
||||
)
|
||||
|
||||
async def test_deletes_panel_users_before_bot_db_user(self):
|
||||
session = FakeSession()
|
||||
calls = []
|
||||
|
||||
async def delete_panel_user(panel_uuid, log_response=False):
|
||||
calls.append(("panel", panel_uuid, log_response))
|
||||
return True
|
||||
|
||||
async def delete_db_user(_session, user_id):
|
||||
calls.append(("db", user_id))
|
||||
return True
|
||||
|
||||
panel_service = SimpleNamespace(
|
||||
delete_user_from_panel=AsyncMock(side_effect=delete_panel_user),
|
||||
)
|
||||
request = self._request(session, {"panel_service": panel_service})
|
||||
user = SimpleNamespace(user_id=42, panel_user_uuid="panel-main")
|
||||
|
||||
with (
|
||||
patch.object(admin_users, "_require_admin_user_id", return_value=100),
|
||||
patch.object(admin_users.user_dal, "get_user_by_id", AsyncMock(return_value=user)),
|
||||
patch.object(
|
||||
admin_users.user_dal,
|
||||
"get_panel_user_uuids_for_user",
|
||||
AsyncMock(return_value=["panel-main", "panel-sub"]),
|
||||
) as collect_uuids,
|
||||
patch.object(
|
||||
admin_users.user_dal,
|
||||
"delete_user_and_relations",
|
||||
AsyncMock(side_effect=delete_db_user),
|
||||
),
|
||||
patch.object(
|
||||
admin_users.message_log_dal,
|
||||
"create_message_log_no_commit",
|
||||
AsyncMock(),
|
||||
) as log_mock,
|
||||
patch.object(admin_users, "_invalidate_after_admin_user_mutation", AsyncMock()),
|
||||
):
|
||||
response = await admin_users.admin_user_delete_route(request)
|
||||
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(json.loads(response.text)["ok"], True)
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[
|
||||
("panel", "panel-main", False),
|
||||
("panel", "panel-sub", False),
|
||||
("db", 42),
|
||||
],
|
||||
)
|
||||
collect_uuids.assert_awaited_once_with(session, 42, user=user)
|
||||
log_payload = log_mock.await_args.args[1]
|
||||
self.assertEqual(log_payload["event_type"], "admin_delete_user_webapp")
|
||||
self.assertIn("panel-main,panel-sub", log_payload["content"])
|
||||
self.assertTrue(session.committed)
|
||||
self.assertFalse(session.rolled_back)
|
||||
|
||||
async def test_aborts_when_panel_service_is_unavailable_for_panel_user(self):
|
||||
session = FakeSession()
|
||||
request = self._request(session)
|
||||
user = SimpleNamespace(user_id=42, panel_user_uuid="panel-main")
|
||||
|
||||
with (
|
||||
patch.object(admin_users, "_require_admin_user_id", return_value=100),
|
||||
patch.object(admin_users.user_dal, "get_user_by_id", AsyncMock(return_value=user)),
|
||||
patch.object(
|
||||
admin_users.user_dal,
|
||||
"get_panel_user_uuids_for_user",
|
||||
AsyncMock(return_value=["panel-main"]),
|
||||
),
|
||||
patch.object(
|
||||
admin_users.user_dal,
|
||||
"delete_user_and_relations",
|
||||
AsyncMock(),
|
||||
) as delete_db,
|
||||
):
|
||||
response = await admin_users.admin_user_delete_route(request)
|
||||
|
||||
self.assertEqual(response.status, 503)
|
||||
self.assertEqual(json.loads(response.text)["error"], "panel_service_unavailable")
|
||||
delete_db.assert_not_awaited()
|
||||
self.assertTrue(session.rolled_back)
|
||||
self.assertFalse(session.committed)
|
||||
|
||||
async def test_aborts_when_panel_delete_fails(self):
|
||||
session = FakeSession()
|
||||
panel_service = SimpleNamespace(
|
||||
delete_user_from_panel=AsyncMock(return_value=False),
|
||||
)
|
||||
request = self._request(session, {"panel_service": panel_service})
|
||||
user = SimpleNamespace(user_id=42, panel_user_uuid="panel-main")
|
||||
|
||||
with (
|
||||
patch.object(admin_users, "_require_admin_user_id", return_value=100),
|
||||
patch.object(admin_users.user_dal, "get_user_by_id", AsyncMock(return_value=user)),
|
||||
patch.object(
|
||||
admin_users.user_dal,
|
||||
"get_panel_user_uuids_for_user",
|
||||
AsyncMock(return_value=["panel-main"]),
|
||||
),
|
||||
patch.object(
|
||||
admin_users.user_dal,
|
||||
"delete_user_and_relations",
|
||||
AsyncMock(),
|
||||
) as delete_db,
|
||||
):
|
||||
response = await admin_users.admin_user_delete_route(request)
|
||||
|
||||
self.assertEqual(response.status, 502)
|
||||
self.assertEqual(json.loads(response.text)["error"], "panel_delete_failed")
|
||||
panel_service.delete_user_from_panel.assert_awaited_once_with(
|
||||
"panel-main",
|
||||
log_response=False,
|
||||
)
|
||||
delete_db.assert_not_awaited()
|
||||
self.assertTrue(session.rolled_back)
|
||||
self.assertFalse(session.committed)
|
||||
@@ -29,6 +29,73 @@ class FakeResult:
|
||||
|
||||
|
||||
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")
|
||||
session = SimpleNamespace(
|
||||
execute=AsyncMock(
|
||||
return_value=FakeResult(["panel-main", "panel-sub", "panel-sub", ""])
|
||||
),
|
||||
)
|
||||
|
||||
result = await user_dal.get_panel_user_uuids_for_user(session, 42, user=user)
|
||||
|
||||
self.assertEqual(result, ["panel-main", "panel-sub"])
|
||||
stmt = session.execute.await_args.args[0]
|
||||
sql = str(
|
||||
stmt.compile(
|
||||
dialect=postgresql.dialect(),
|
||||
compile_kwargs={"literal_binds": True},
|
||||
)
|
||||
)
|
||||
self.assertIn("subscriptions", sql)
|
||||
self.assertIn("42", sql)
|
||||
|
||||
async def test_delete_user_and_relations_cleans_dependent_tables_before_parents(self):
|
||||
user = SimpleNamespace(user_id=42)
|
||||
session = SimpleNamespace(
|
||||
execute=AsyncMock(side_effect=lambda stmt: FakeResult()),
|
||||
delete=AsyncMock(),
|
||||
flush=AsyncMock(),
|
||||
)
|
||||
|
||||
with patch("db.dal.user_dal.get_user_by_id", AsyncMock(return_value=user)):
|
||||
deleted = await user_dal.delete_user_and_relations(session, 42)
|
||||
|
||||
self.assertTrue(deleted)
|
||||
|
||||
delete_tables = []
|
||||
update_tables = []
|
||||
for call in session.execute.await_args_list:
|
||||
stmt = call.args[0]
|
||||
if isinstance(stmt, Delete):
|
||||
delete_tables.append(stmt.table.name)
|
||||
elif isinstance(stmt, Update):
|
||||
update_tables.append(stmt.table.name)
|
||||
|
||||
self.assertLess(delete_tables.index("traffic_topups"), delete_tables.index("payments"))
|
||||
self.assertLess(delete_tables.index("traffic_topups"), delete_tables.index("subscriptions"))
|
||||
self.assertLess(
|
||||
delete_tables.index("hwid_device_purchases"),
|
||||
delete_tables.index("payments"),
|
||||
)
|
||||
self.assertLess(delete_tables.index("tariff_changes"), delete_tables.index("payments"))
|
||||
self.assertLess(
|
||||
delete_tables.index("traffic_warnings"),
|
||||
delete_tables.index("subscriptions"),
|
||||
)
|
||||
self.assertLess(
|
||||
delete_tables.index("promo_code_activations"),
|
||||
delete_tables.index("payments"),
|
||||
)
|
||||
self.assertLess(
|
||||
delete_tables.index("support_ticket_messages"),
|
||||
delete_tables.index("support_tickets"),
|
||||
)
|
||||
self.assertIn("support_ticket_messages", update_tables)
|
||||
self.assertIn("email_verification_codes", delete_tables)
|
||||
session.delete.assert_awaited_once_with(user)
|
||||
session.flush.assert_awaited_once()
|
||||
|
||||
async def test_get_user_ids_without_active_subscription_uses_left_join_null_check(self):
|
||||
session = SimpleNamespace(
|
||||
execute=AsyncMock(return_value=FakeResult([2, 3])),
|
||||
|
||||
Reference in New Issue
Block a user