dc-securex 2.29.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dc_securex-2.29.7.dist-info/METADATA +1028 -0
- dc_securex-2.29.7.dist-info/RECORD +28 -0
- dc_securex-2.29.7.dist-info/WHEEL +5 -0
- dc_securex-2.29.7.dist-info/licenses/LICENSE +21 -0
- dc_securex-2.29.7.dist-info/top_level.txt +2 -0
- securex/__init__.py +18 -0
- securex/backup/__init__.py +5 -0
- securex/backup/manager.py +792 -0
- securex/client.py +329 -0
- securex/handlers/__init__.py +8 -0
- securex/handlers/channel.py +97 -0
- securex/handlers/member.py +110 -0
- securex/handlers/role.py +74 -0
- securex/models.py +156 -0
- securex/utils/__init__.py +5 -0
- securex/utils/punishment.py +124 -0
- securex/utils/whitelist.py +129 -0
- securex/workers/__init__.py +8 -0
- securex/workers/action_worker.py +115 -0
- securex/workers/cleanup_worker.py +94 -0
- securex/workers/guild_worker.py +186 -0
- securex/workers/log_worker.py +88 -0
- tests/__init__.py +9 -0
- tests/test_coverage_client.py +210 -0
- tests/test_coverage_handlers.py +341 -0
- tests/test_coverage_manager.py +504 -0
- tests/test_coverage_utils.py +177 -0
- tests/test_coverage_workers.py +297 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Guild Restoration Worker - Restores server name and vanity URL only
|
|
3
|
+
Triggered by GuildHandler when unauthorized changes are detected
|
|
4
|
+
Supports per-guild user tokens with file storage and memory caching
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
import discord
|
|
8
|
+
import aiohttp
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Dict, Any, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GuildWorker:
|
|
16
|
+
"""Background worker for guild settings restoration"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, sdk):
|
|
19
|
+
self.sdk = sdk
|
|
20
|
+
self.bot = sdk.bot
|
|
21
|
+
self._worker_task = None
|
|
22
|
+
|
|
23
|
+
# Per-guild user tokens
|
|
24
|
+
self._user_tokens: Dict[int, str] = {} # guild_id -> user_token (cache)
|
|
25
|
+
self._tokens_file = sdk.backup_dir / "user_tokens.json"
|
|
26
|
+
|
|
27
|
+
async def _load_tokens(self):
|
|
28
|
+
"""Load user tokens from file into cache"""
|
|
29
|
+
try:
|
|
30
|
+
if self._tokens_file.exists():
|
|
31
|
+
with open(self._tokens_file, 'r') as f:
|
|
32
|
+
data = json.load(f)
|
|
33
|
+
# Convert string keys back to int
|
|
34
|
+
self._user_tokens = {int(k): v for k, v in data.items()}
|
|
35
|
+
print(f"✅ Loaded user tokens for {len(self._user_tokens)} guild(s)")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"Error loading user tokens: {e}")
|
|
38
|
+
|
|
39
|
+
async def _save_tokens(self):
|
|
40
|
+
"""Save user tokens to file"""
|
|
41
|
+
try:
|
|
42
|
+
# Convert int keys to string for JSON
|
|
43
|
+
data = {str(k): v for k, v in self._user_tokens.items()}
|
|
44
|
+
with open(self._tokens_file, 'w') as f:
|
|
45
|
+
json.dump(data, f, indent=2)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
print(f"Error saving user tokens: {e}")
|
|
48
|
+
|
|
49
|
+
async def set_user_token(self, guild_id: int, token: str):
|
|
50
|
+
"""Set user token for a specific guild"""
|
|
51
|
+
self._user_tokens[guild_id] = token
|
|
52
|
+
await self._save_tokens()
|
|
53
|
+
print(f"✅ User token set for guild {guild_id}")
|
|
54
|
+
|
|
55
|
+
def get_user_token(self, guild_id: int) -> Optional[str]:
|
|
56
|
+
"""Get user token for a guild (from cache)"""
|
|
57
|
+
return self._user_tokens.get(guild_id)
|
|
58
|
+
|
|
59
|
+
async def remove_user_token(self, guild_id: int):
|
|
60
|
+
"""Remove user token for a guild"""
|
|
61
|
+
if guild_id in self._user_tokens:
|
|
62
|
+
del self._user_tokens[guild_id]
|
|
63
|
+
await self._save_tokens()
|
|
64
|
+
print(f"❌ User token removed for guild {guild_id}")
|
|
65
|
+
|
|
66
|
+
async def start(self):
|
|
67
|
+
"""Start the guild restoration worker"""
|
|
68
|
+
# Load tokens first
|
|
69
|
+
await self._load_tokens()
|
|
70
|
+
|
|
71
|
+
if self._worker_task is None or self._worker_task.done():
|
|
72
|
+
self._worker_task = asyncio.create_task(self._restoration_loop())
|
|
73
|
+
print("⚡ Guild Restoration Worker started")
|
|
74
|
+
|
|
75
|
+
async def stop(self):
|
|
76
|
+
"""Stop the worker"""
|
|
77
|
+
if self._worker_task:
|
|
78
|
+
self._worker_task.cancel()
|
|
79
|
+
self._worker_task = None
|
|
80
|
+
|
|
81
|
+
async def _restoration_loop(self):
|
|
82
|
+
"""Main worker loop - processes guild_update entries from queue"""
|
|
83
|
+
while True:
|
|
84
|
+
try:
|
|
85
|
+
entry = await self.sdk.guild_queue.get()
|
|
86
|
+
|
|
87
|
+
# Only process guild_update actions
|
|
88
|
+
if entry.action != discord.AuditLogAction.guild_update:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
# Extract changes from entry
|
|
92
|
+
guild = entry.guild
|
|
93
|
+
executor = entry.user
|
|
94
|
+
|
|
95
|
+
# Skip if bot did it
|
|
96
|
+
if executor.id == self.bot.user.id:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Check authorization
|
|
100
|
+
if executor.id == guild.owner_id:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
whitelist_set = self.sdk.whitelist_cache.get(guild.id, set())
|
|
104
|
+
if executor.id in whitelist_set:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# Unauthorized change - restore from backup
|
|
108
|
+
print(f"🔍 Unauthorized guild update by {executor.name}")
|
|
109
|
+
await self._restore_guild_settings(guild)
|
|
110
|
+
|
|
111
|
+
except asyncio.CancelledError:
|
|
112
|
+
break
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f"Error in guild restoration worker: {e}")
|
|
115
|
+
|
|
116
|
+
async def _restore_vanity_via_api(self, guild_id: int, vanity_code: str) -> bool:
|
|
117
|
+
"""Restore vanity URL using user token and Discord API"""
|
|
118
|
+
user_token = self.get_user_token(guild_id)
|
|
119
|
+
|
|
120
|
+
if not user_token:
|
|
121
|
+
print(f"⚠️ No user token set for guild {guild_id}! Cannot restore vanity URL.")
|
|
122
|
+
print(f" Use: await sx.guild_worker.set_user_token({guild_id}, 'USER_TOKEN')")
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
url = f"https://discord.com/api/v9/guilds/{guild_id}/vanity-url"
|
|
127
|
+
headers = {
|
|
128
|
+
"authorization": user_token,
|
|
129
|
+
"content-type": "application/json"
|
|
130
|
+
}
|
|
131
|
+
payload = {"code": vanity_code}
|
|
132
|
+
|
|
133
|
+
async with aiohttp.ClientSession() as session:
|
|
134
|
+
async with session.patch(url, headers=headers, json=payload) as response:
|
|
135
|
+
if response.status == 200:
|
|
136
|
+
return True
|
|
137
|
+
else:
|
|
138
|
+
error_text = await response.text()
|
|
139
|
+
print(f"⚠️ Failed to restore vanity: {response.status} - {error_text}")
|
|
140
|
+
return False
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print(f"Error restoring vanity via API: {e}")
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
async def _restore_guild_settings(self, guild):
|
|
146
|
+
"""Restore guild settings (name and vanity) from backup"""
|
|
147
|
+
try:
|
|
148
|
+
print(f"🔧 [DEBUG] Starting restoration for guild: {guild.name}")
|
|
149
|
+
start_time = datetime.now()
|
|
150
|
+
|
|
151
|
+
backup_data = await self.sdk.backup_manager.get_guild_settings(guild.id)
|
|
152
|
+
print(f"📦 [DEBUG] Backup data retrieved: {backup_data}")
|
|
153
|
+
if not backup_data:
|
|
154
|
+
print(f"⚠️ No backup found for guild {guild.name}")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
restore_params = {}
|
|
158
|
+
restored_items = []
|
|
159
|
+
|
|
160
|
+
# Restore vanity URL if we have it in backup
|
|
161
|
+
if backup_data.get("vanity_url_code"):
|
|
162
|
+
old_vanity = backup_data["vanity_url_code"]
|
|
163
|
+
vanity_success = await self._restore_vanity_via_api(guild.id, old_vanity)
|
|
164
|
+
if vanity_success:
|
|
165
|
+
restored_items.append(f"vanity (discord.gg/{old_vanity})")
|
|
166
|
+
|
|
167
|
+
# Restore server name if different from backup
|
|
168
|
+
if backup_data.get("name") and backup_data["name"] != guild.name:
|
|
169
|
+
restore_params["name"] = backup_data["name"]
|
|
170
|
+
restored_items.append(f"name ({backup_data['name']})")
|
|
171
|
+
|
|
172
|
+
if restore_params:
|
|
173
|
+
await guild.edit(**restore_params, reason="SecureX: Restoring unauthorized guild changes")
|
|
174
|
+
|
|
175
|
+
if restored_items:
|
|
176
|
+
elapsed = (datetime.now() - start_time).total_seconds() * 1000
|
|
177
|
+
items_str = ", ".join(restored_items)
|
|
178
|
+
print(f"⚡ Restored guild settings in {elapsed:.0f}ms: {items_str}")
|
|
179
|
+
|
|
180
|
+
except discord.errors.HTTPException as e:
|
|
181
|
+
if e.code == 50035:
|
|
182
|
+
print(f"⚠️ Some settings could not be restored: {e}")
|
|
183
|
+
else:
|
|
184
|
+
print(f"Error restoring guild settings: {e}")
|
|
185
|
+
except Exception as e:
|
|
186
|
+
print(f"Error in guild restoration: {e}")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Log Worker - Logs threats and fires callbacks
|
|
3
|
+
"""
|
|
4
|
+
import discord
|
|
5
|
+
import asyncio
|
|
6
|
+
from ..models import ThreatEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LogWorker:
|
|
10
|
+
"""Worker that logs threats and triggers callbacks"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, sdk):
|
|
13
|
+
self.sdk = sdk
|
|
14
|
+
self.bot = sdk.bot
|
|
15
|
+
self.log_queue = asyncio.Queue()
|
|
16
|
+
self._worker_task = None
|
|
17
|
+
|
|
18
|
+
self.action_map = {
|
|
19
|
+
discord.AuditLogAction.bot_add: "bot_add",
|
|
20
|
+
discord.AuditLogAction.channel_create: "channel_create",
|
|
21
|
+
discord.AuditLogAction.channel_delete: "channel_delete",
|
|
22
|
+
discord.AuditLogAction.channel_update: "channel_update",
|
|
23
|
+
discord.AuditLogAction.role_create: "role_create",
|
|
24
|
+
discord.AuditLogAction.role_delete: "role_delete",
|
|
25
|
+
discord.AuditLogAction.role_update: "role_update",
|
|
26
|
+
discord.AuditLogAction.kick: "member_kick",
|
|
27
|
+
discord.AuditLogAction.ban: "member_ban",
|
|
28
|
+
discord.AuditLogAction.unban: "member_unban",
|
|
29
|
+
discord.AuditLogAction.member_update: "member_update",
|
|
30
|
+
discord.AuditLogAction.webhook_create: "webhook_create",
|
|
31
|
+
discord.AuditLogAction.guild_update: "guild_update",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async def start(self):
|
|
35
|
+
"""Start the log worker"""
|
|
36
|
+
if self._worker_task is None or self._worker_task.done():
|
|
37
|
+
self._worker_task = asyncio.create_task(self._worker_loop())
|
|
38
|
+
print("📝 Log Worker started")
|
|
39
|
+
|
|
40
|
+
async def stop(self):
|
|
41
|
+
"""Stop the worker"""
|
|
42
|
+
if self._worker_task:
|
|
43
|
+
self._worker_task.cancel()
|
|
44
|
+
self._worker_task = None
|
|
45
|
+
|
|
46
|
+
async def _worker_loop(self):
|
|
47
|
+
"""Main worker loop"""
|
|
48
|
+
while True:
|
|
49
|
+
try:
|
|
50
|
+
entry = await self.log_queue.get()
|
|
51
|
+
await self._process_entry(entry)
|
|
52
|
+
except asyncio.CancelledError:
|
|
53
|
+
break
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"Error in log worker: {e}")
|
|
56
|
+
|
|
57
|
+
async def _process_entry(self, entry):
|
|
58
|
+
"""Process log entry and fire callbacks"""
|
|
59
|
+
try:
|
|
60
|
+
guild = entry.guild
|
|
61
|
+
executor = entry.user
|
|
62
|
+
|
|
63
|
+
if executor.id == self.bot.user.id or executor.id == guild.owner_id:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
violation_type = self.action_map.get(entry.action)
|
|
67
|
+
if not violation_type:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
guild_punishments = self.sdk.punishment_cache.get(guild.id, self.sdk.punishments)
|
|
71
|
+
punishment_action = guild_punishments.get(violation_type, "none")
|
|
72
|
+
|
|
73
|
+
threat_event = ThreatEvent(
|
|
74
|
+
guild_id=guild.id,
|
|
75
|
+
actor_id=executor.id,
|
|
76
|
+
type=violation_type,
|
|
77
|
+
target_id=getattr(entry.target, 'id', None),
|
|
78
|
+
target_name=getattr(entry.target, 'name', 'Unknown'),
|
|
79
|
+
prevented=True,
|
|
80
|
+
restored=True,
|
|
81
|
+
punishment_action=punishment_action,
|
|
82
|
+
timestamp=entry.created_at
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
await self.sdk._trigger_callbacks('threat_detected', threat_event)
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"Error processing log entry: {e}")
|
tests/__init__.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import asyncio
|
|
3
|
+
import discord
|
|
4
|
+
from securex import SecureX
|
|
5
|
+
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
class TestClientExhaustive:
|
|
9
|
+
"""Exhaustive tests for securex/client.py to reach 100% coverage"""
|
|
10
|
+
|
|
11
|
+
async def test_audit_log_listener_success(self):
|
|
12
|
+
"""Test on_audit_log_entry_create when everything is normal"""
|
|
13
|
+
bot = Mock(spec=discord.Client)
|
|
14
|
+
sx = SecureX(bot)
|
|
15
|
+
|
|
16
|
+
# Mock queues
|
|
17
|
+
sx.action_queue = Mock(spec=asyncio.Queue)
|
|
18
|
+
sx.action_queue.put = AsyncMock()
|
|
19
|
+
sx.log_queue = Mock(spec=asyncio.Queue)
|
|
20
|
+
sx.log_queue.put = AsyncMock()
|
|
21
|
+
sx.cleanup_queue = Mock(spec=asyncio.Queue)
|
|
22
|
+
sx.cleanup_queue.put = AsyncMock()
|
|
23
|
+
sx.guild_queue = Mock(spec=asyncio.Queue)
|
|
24
|
+
sx.guild_queue.put = AsyncMock()
|
|
25
|
+
|
|
26
|
+
# Get the listener
|
|
27
|
+
listeners = bot.event.call_args_list
|
|
28
|
+
listener_func = None
|
|
29
|
+
for call in listeners:
|
|
30
|
+
if call[0][0].__name__ == 'on_audit_log_entry_create':
|
|
31
|
+
listener_func = call[0][0]
|
|
32
|
+
break
|
|
33
|
+
|
|
34
|
+
entry = Mock(spec=discord.AuditLogEntry)
|
|
35
|
+
await listener_func(entry)
|
|
36
|
+
|
|
37
|
+
sx.action_queue.put.assert_called_once_with(entry)
|
|
38
|
+
sx.log_queue.put.assert_called_once_with(entry)
|
|
39
|
+
sx.cleanup_queue.put.assert_called_once_with(entry)
|
|
40
|
+
sx.guild_queue.put.assert_called_once_with(entry)
|
|
41
|
+
|
|
42
|
+
async def test_audit_log_listener_queue_full(self):
|
|
43
|
+
"""Test on_audit_log_entry_create when queues are full"""
|
|
44
|
+
bot = Mock(spec=discord.Client)
|
|
45
|
+
sx = SecureX(bot)
|
|
46
|
+
|
|
47
|
+
# Mock queues to raise QueueFull
|
|
48
|
+
sx.action_queue = Mock(spec=asyncio.Queue)
|
|
49
|
+
sx.action_queue.put = AsyncMock(side_effect=asyncio.QueueFull)
|
|
50
|
+
|
|
51
|
+
# Get the listener
|
|
52
|
+
listeners = bot.event.call_args_list
|
|
53
|
+
listener_func = None
|
|
54
|
+
for call in listeners:
|
|
55
|
+
if call[0][0].__name__ == 'on_audit_log_entry_create':
|
|
56
|
+
listener_func = call[0][0]
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
assert listener_func is not None
|
|
60
|
+
|
|
61
|
+
entry = Mock(spec=discord.AuditLogEntry)
|
|
62
|
+
# Should catch QueueFull and print warning
|
|
63
|
+
await listener_func(entry)
|
|
64
|
+
|
|
65
|
+
async def test_registered_events(self):
|
|
66
|
+
"""Test all registered Discord event listeners in client.py"""
|
|
67
|
+
bot = Mock(spec=discord.Client)
|
|
68
|
+
sx = SecureX(bot)
|
|
69
|
+
|
|
70
|
+
event_mappers = {}
|
|
71
|
+
for call in bot.event.call_args_list:
|
|
72
|
+
func = call[0][0]
|
|
73
|
+
event_mappers[func.__name__] = func
|
|
74
|
+
|
|
75
|
+
# 1. on_guild_channel_delete
|
|
76
|
+
channel = Mock()
|
|
77
|
+
sx.channel_handler.on_channel_delete = AsyncMock()
|
|
78
|
+
await event_mappers['on_guild_channel_delete'](channel)
|
|
79
|
+
sx.channel_handler.on_channel_delete.assert_called_once_with(channel)
|
|
80
|
+
|
|
81
|
+
# 2. on_guild_channel_update
|
|
82
|
+
before, after = Mock(), Mock()
|
|
83
|
+
sx.channel_handler.on_channel_update = AsyncMock()
|
|
84
|
+
await event_mappers['on_guild_channel_update'](before, after)
|
|
85
|
+
sx.channel_handler.on_channel_update.assert_called_once_with(before, after)
|
|
86
|
+
|
|
87
|
+
# 3. on_guild_role_delete
|
|
88
|
+
role = Mock()
|
|
89
|
+
sx.role_handler.on_role_delete = AsyncMock()
|
|
90
|
+
await event_mappers['on_guild_role_delete'](role)
|
|
91
|
+
sx.role_handler.on_role_delete.assert_called_once_with(role)
|
|
92
|
+
|
|
93
|
+
# 4. on_guild_role_update
|
|
94
|
+
before_r, after_r = Mock(), Mock()
|
|
95
|
+
sx.role_handler.on_role_update = AsyncMock()
|
|
96
|
+
await event_mappers['on_guild_role_update'](before_r, after_r)
|
|
97
|
+
sx.role_handler.on_role_update.assert_called_once_with(before_r, after_r)
|
|
98
|
+
|
|
99
|
+
# 5. on_member_ban
|
|
100
|
+
guild, user = Mock(), Mock()
|
|
101
|
+
sx.member_handler.on_member_ban = AsyncMock()
|
|
102
|
+
await event_mappers['on_member_ban'](guild, user)
|
|
103
|
+
sx.member_handler.on_member_ban.assert_called_once_with(guild, user)
|
|
104
|
+
|
|
105
|
+
# 6. on_member_update
|
|
106
|
+
before_m, after_m = Mock(), Mock()
|
|
107
|
+
sx.member_handler.on_member_update = AsyncMock()
|
|
108
|
+
await event_mappers['on_member_update'](before_m, after_m)
|
|
109
|
+
sx.member_handler.on_member_update.assert_called_once_with(before_m, after_m)
|
|
110
|
+
|
|
111
|
+
async def test_callback_decorators_and_triggering(self):
|
|
112
|
+
"""Test @sx.on decorators and _trigger_callbacks logic"""
|
|
113
|
+
bot = Mock()
|
|
114
|
+
sx = SecureX(bot)
|
|
115
|
+
|
|
116
|
+
# Test async callback
|
|
117
|
+
mock_async_cb = AsyncMock()
|
|
118
|
+
sx.on('threat_detected')(mock_async_cb)
|
|
119
|
+
|
|
120
|
+
# Test sync callback
|
|
121
|
+
mock_sync_cb = MagicMock()
|
|
122
|
+
sx.on('threat_detected')(mock_sync_cb)
|
|
123
|
+
|
|
124
|
+
await sx._trigger_callbacks('threat_detected', "arg1", extra="data")
|
|
125
|
+
|
|
126
|
+
mock_async_cb.assert_called_once_with("arg1", extra="data")
|
|
127
|
+
mock_sync_cb.assert_called_once_with("arg1", extra="data")
|
|
128
|
+
|
|
129
|
+
# Test callback with exception
|
|
130
|
+
fail_cb = Mock(side_effect=Exception("Callback failed"))
|
|
131
|
+
sx.on('threat_detected')(fail_cb)
|
|
132
|
+
# Should not raise exception
|
|
133
|
+
await sx._trigger_callbacks('threat_detected')
|
|
134
|
+
|
|
135
|
+
# Test triggering non-existent event
|
|
136
|
+
await sx._trigger_callbacks('non_existent')
|
|
137
|
+
|
|
138
|
+
# Test convenience properties
|
|
139
|
+
sx.on_threat_detected(lambda x: x)
|
|
140
|
+
sx.on_backup_completed(lambda x: x)
|
|
141
|
+
sx.on_restore_completed(lambda x: x)
|
|
142
|
+
sx.on_whitelist_changed(lambda x: x)
|
|
143
|
+
|
|
144
|
+
assert len(sx._callbacks['threat_detected']) == 4 # 2 from before + 1 fail + 1 lambda
|
|
145
|
+
assert len(sx._callbacks['backup_completed']) == 1
|
|
146
|
+
|
|
147
|
+
async def test_enable_full(self):
|
|
148
|
+
"""Test sx.enable with full set of parameters for max coverage"""
|
|
149
|
+
bot = Mock()
|
|
150
|
+
sx = SecureX(bot)
|
|
151
|
+
|
|
152
|
+
sx.whitelist.preload_all = AsyncMock()
|
|
153
|
+
sx.whitelist.get_all = AsyncMock(return_value=[1, 2])
|
|
154
|
+
sx.whitelist.add = AsyncMock()
|
|
155
|
+
sx.backup_manager.preload_all = AsyncMock()
|
|
156
|
+
sx.backup_manager.create_backup = AsyncMock(return_value=Mock(success=True, channel_count=5, role_count=10))
|
|
157
|
+
|
|
158
|
+
# Mock start_auto_backup and start_auto_refresh manually
|
|
159
|
+
sx.backup_manager.start_auto_backup = MagicMock()
|
|
160
|
+
sx.backup_manager.start_auto_refresh = MagicMock()
|
|
161
|
+
|
|
162
|
+
await sx.enable(
|
|
163
|
+
guild_id=123,
|
|
164
|
+
whitelist=[3],
|
|
165
|
+
auto_backup=True,
|
|
166
|
+
punishments={"channel_delete": "kick"},
|
|
167
|
+
notify_user=True
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
assert sx.whitelist_cache[123] == {1, 2, 3}
|
|
171
|
+
assert sx.punishment_cache[123]["channel_delete"] == "kick"
|
|
172
|
+
sx.backup_manager.start_auto_backup.assert_called_once()
|
|
173
|
+
sx.backup_manager.start_auto_refresh.assert_called_once()
|
|
174
|
+
|
|
175
|
+
async def test_enable_minimal(self):
|
|
176
|
+
"""Test sx.enable with minimal parameters"""
|
|
177
|
+
bot = Mock()
|
|
178
|
+
sx = SecureX(bot)
|
|
179
|
+
|
|
180
|
+
# Mocking to avoid real side effects
|
|
181
|
+
sx.whitelist.preload_all = AsyncMock()
|
|
182
|
+
sx.backup_manager.preload_all = AsyncMock()
|
|
183
|
+
sx.backup_manager.start_auto_backup = MagicMock()
|
|
184
|
+
sx.backup_manager.start_auto_refresh = MagicMock()
|
|
185
|
+
|
|
186
|
+
await sx.enable(auto_backup=False)
|
|
187
|
+
|
|
188
|
+
# These should NOT be called if auto_backup is False
|
|
189
|
+
sx.backup_manager.start_auto_backup.assert_not_called()
|
|
190
|
+
sx.backup_manager.start_auto_refresh.assert_not_called()
|
|
191
|
+
|
|
192
|
+
async def test_proxy_methods(self):
|
|
193
|
+
"""Test create_backup, restore_channel, and restore_role proxy methods"""
|
|
194
|
+
bot = Mock()
|
|
195
|
+
sx = SecureX(bot)
|
|
196
|
+
|
|
197
|
+
# restore_channel
|
|
198
|
+
sx.backup_manager.restore_channel = AsyncMock(return_value="new_channel")
|
|
199
|
+
res = await sx.restore_channel(Mock(), Mock())
|
|
200
|
+
assert res == "new_channel"
|
|
201
|
+
|
|
202
|
+
# restore_role
|
|
203
|
+
sx.backup_manager.restore_role = AsyncMock(return_value=True)
|
|
204
|
+
res = await sx.restore_role(Mock(), 999)
|
|
205
|
+
assert res is True
|
|
206
|
+
|
|
207
|
+
# create_backup
|
|
208
|
+
sx.backup_manager.create_backup = AsyncMock(return_value="backup_info")
|
|
209
|
+
res = await sx.create_backup(123)
|
|
210
|
+
assert res == "backup_info"
|