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,504 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import asyncio
|
|
3
|
+
import discord
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone, timedelta
|
|
7
|
+
from securex import SecureX
|
|
8
|
+
from securex.backup.manager import BackupManager
|
|
9
|
+
from securex.models import BackupInfo
|
|
10
|
+
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
|
11
|
+
from types import SimpleNamespace
|
|
12
|
+
|
|
13
|
+
@pytest.mark.asyncio
|
|
14
|
+
class TestManagerFinalBoss:
|
|
15
|
+
"""Comprehensive manager tests for maximum coverage"""
|
|
16
|
+
|
|
17
|
+
async def test_comprehensive_coverage(self, tmp_path):
|
|
18
|
+
"""Complete test covering all BackupManager functionality"""
|
|
19
|
+
bot = MagicMock()
|
|
20
|
+
bot.is_closed.return_value = True
|
|
21
|
+
bot.wait_until_ready = AsyncMock()
|
|
22
|
+
bot.user = MagicMock(id=123, name="Bot")
|
|
23
|
+
|
|
24
|
+
sx = SecureX(bot, backup_dir=str(tmp_path))
|
|
25
|
+
manager = sx.backup_manager
|
|
26
|
+
|
|
27
|
+
def make_role(id, name, pos=0, is_def=False):
|
|
28
|
+
r = MagicMock(spec=discord.Role)
|
|
29
|
+
r.id = id; r.name = name; r.position = pos
|
|
30
|
+
r.is_default.return_value = is_def
|
|
31
|
+
r.managed = False; r.hoist = True; r.mentionable = True
|
|
32
|
+
r.color = SimpleNamespace(value=0xFF)
|
|
33
|
+
r.permissions = SimpleNamespace(value=8)
|
|
34
|
+
r.__hash__.return_value = id
|
|
35
|
+
r.__eq__ = lambda s, o: getattr(o, 'id', None) == id
|
|
36
|
+
r.edit = AsyncMock()
|
|
37
|
+
return r
|
|
38
|
+
|
|
39
|
+
def make_chan(id, name, type, cat=None):
|
|
40
|
+
c = MagicMock(spec=discord.abc.GuildChannel)
|
|
41
|
+
if type == discord.ChannelType.text:
|
|
42
|
+
c = MagicMock(spec=discord.TextChannel)
|
|
43
|
+
c.topic = "t"; c.nsfw = False; c.slowmode_delay = 0
|
|
44
|
+
elif type == discord.ChannelType.voice:
|
|
45
|
+
c = MagicMock(spec=discord.VoiceChannel)
|
|
46
|
+
c.bitrate = 64000; c.user_limit = 0
|
|
47
|
+
elif type == discord.ChannelType.category:
|
|
48
|
+
c = MagicMock(spec=discord.CategoryChannel)
|
|
49
|
+
elif type == discord.ChannelType.stage_voice:
|
|
50
|
+
c = MagicMock(spec=discord.StageChannel)
|
|
51
|
+
c.bitrate = 64000
|
|
52
|
+
c.id = id; c.name = name; c.type = type; c.position = 0; c.category = cat
|
|
53
|
+
c.overwrites = {}
|
|
54
|
+
c.__hash__.return_value = id
|
|
55
|
+
c.__eq__ = lambda s, o: getattr(o, 'id', None) == id
|
|
56
|
+
c.edit = AsyncMock()
|
|
57
|
+
c.set_permissions = AsyncMock()
|
|
58
|
+
return c
|
|
59
|
+
|
|
60
|
+
guild = MagicMock(spec=discord.Guild)
|
|
61
|
+
guild.id = 777; guild.name = "G"; guild.owner_id = 999
|
|
62
|
+
guild.icon = None; guild.banner = None; guild.description = None; guild.vanity_url_code = "v"
|
|
63
|
+
bot.get_guild.return_value = guild
|
|
64
|
+
bot.guilds = [guild]
|
|
65
|
+
|
|
66
|
+
r1 = make_role(1, "R1", 1)
|
|
67
|
+
everyone = make_role(777, "@everyone", 0, True)
|
|
68
|
+
guild.roles = [everyone, r1]
|
|
69
|
+
|
|
70
|
+
cat1 = make_chan(10, "C1", discord.ChannelType.category)
|
|
71
|
+
txt1 = make_chan(11, "T1", discord.ChannelType.text, cat1)
|
|
72
|
+
voi1 = make_chan(12, "V1", discord.ChannelType.voice, cat1)
|
|
73
|
+
stg1 = make_chan(13, "S1", discord.ChannelType.stage_voice, cat1)
|
|
74
|
+
txt1.overwrites = {r1: discord.PermissionOverwrite(administrator=True)}
|
|
75
|
+
guild.channels = [cat1, txt1, voi1, stg1]
|
|
76
|
+
|
|
77
|
+
with patch("asyncio.sleep", AsyncMock()), patch("aiohttp.ClientSession.get") as mock_get:
|
|
78
|
+
mock_resp = MagicMock(); mock_resp.read = AsyncMock(return_value=b"i"); mock_resp.status = 200
|
|
79
|
+
mock_get.return_value.__aenter__.return_value = mock_resp
|
|
80
|
+
|
|
81
|
+
# === CORE FLOW TESTS ===
|
|
82
|
+
await manager.create_backup(guild.id)
|
|
83
|
+
|
|
84
|
+
await manager.restore_channel(guild, make_chan(999, "X", discord.ChannelType.text))
|
|
85
|
+
manager._channel_cache.pop(guild.id, None)
|
|
86
|
+
guild.create_text_channel = AsyncMock(return_value=make_chan(111, "T", discord.ChannelType.text))
|
|
87
|
+
guild.create_voice_channel = AsyncMock(return_value=make_chan(112, "V", discord.ChannelType.voice))
|
|
88
|
+
guild.create_stage_channel = AsyncMock(return_value=make_chan(113, "S", discord.ChannelType.stage_voice))
|
|
89
|
+
guild.create_category = AsyncMock(return_value=make_chan(114, "C", discord.ChannelType.category))
|
|
90
|
+
guild.get_channel.return_value = None
|
|
91
|
+
for cid, ctype in [(10, discord.ChannelType.category), (11, discord.ChannelType.text), (12, discord.ChannelType.voice), (13, discord.ChannelType.stage_voice)]:
|
|
92
|
+
await manager.restore_channel(guild, make_chan(cid, "N", ctype))
|
|
93
|
+
guild.create_text_channel.return_value.edit.side_effect = discord.Forbidden(Mock(), "F")
|
|
94
|
+
await manager.restore_channel(guild, make_chan(11, "N", discord.ChannelType.text))
|
|
95
|
+
guild.create_text_channel.return_value.edit.side_effect = None
|
|
96
|
+
|
|
97
|
+
# Category children restoration
|
|
98
|
+
with patch.object(manager, 'backup_dir', tmp_path / "empty"):
|
|
99
|
+
await manager.restore_category_children(guild, 10, make_chan(105, "NC", discord.ChannelType.category))
|
|
100
|
+
await manager.restore_category_children(guild, 99, make_chan(105, "NC", discord.ChannelType.category))
|
|
101
|
+
cat_new = make_chan(105, "NC", discord.ChannelType.category)
|
|
102
|
+
existing_same = make_chan(11, "E", discord.ChannelType.text, cat_new)
|
|
103
|
+
guild.get_channel.side_effect = lambda cid: existing_same if cid == 11 else None
|
|
104
|
+
await manager.restore_category_children(guild, 10, cat_new)
|
|
105
|
+
cat_wrong = make_chan(999, "W", discord.ChannelType.category)
|
|
106
|
+
existing_diff = make_chan(11, "E", discord.ChannelType.text, cat_wrong)
|
|
107
|
+
guild.get_channel.side_effect = lambda cid: existing_diff if cid == 11 else None
|
|
108
|
+
await manager.restore_category_children(guild, 10, cat_new)
|
|
109
|
+
member1 = MagicMock(spec=discord.Member, id=888)
|
|
110
|
+
member1.__hash__.return_value = 888
|
|
111
|
+
guild.get_member.return_value = member1
|
|
112
|
+
backup_file = manager.backup_dir / f"channels_{guild.id}.json"
|
|
113
|
+
with open(backup_file, 'r') as f:
|
|
114
|
+
data = json.load(f)
|
|
115
|
+
data["channels"][1]["permissions"]["888"] = {"type": "member", "allow": 1, "deny": 0}
|
|
116
|
+
with open(backup_file, 'w') as f:
|
|
117
|
+
json.dump(data, f)
|
|
118
|
+
manager._channel_cache.pop(guild.id, None)
|
|
119
|
+
guild.get_channel.side_effect = None
|
|
120
|
+
guild.get_channel.return_value = None
|
|
121
|
+
await manager.restore_category_children(guild, 10, cat_new)
|
|
122
|
+
guild.get_role.return_value = None
|
|
123
|
+
guild.get_member.return_value = None
|
|
124
|
+
await manager.restore_category_children(guild, 10, cat_new)
|
|
125
|
+
guild.get_role.return_value = r1
|
|
126
|
+
|
|
127
|
+
# Channel permissions
|
|
128
|
+
guild.get_channel.return_value = make_chan(11, "T", discord.ChannelType.text)
|
|
129
|
+
guild.get_channel.return_value.set_permissions = AsyncMock()
|
|
130
|
+
await manager.restore_channel_permissions(guild, 11)
|
|
131
|
+
guild.get_channel.return_value.set_permissions.side_effect = Exception("E")
|
|
132
|
+
await manager.restore_channel_permissions(guild, 11)
|
|
133
|
+
|
|
134
|
+
# Role restoration
|
|
135
|
+
await manager.restore_role(guild, 999)
|
|
136
|
+
guild.create_role = AsyncMock(return_value=make_role(101, "R", 5))
|
|
137
|
+
guild.edit_role_positions = AsyncMock(side_effect=discord.Forbidden(Mock(), "F"))
|
|
138
|
+
await manager.restore_role(guild, 1)
|
|
139
|
+
guild.edit_role_positions.side_effect = Exception("E")
|
|
140
|
+
await manager.restore_role(guild, 1)
|
|
141
|
+
guild.edit_role_positions.side_effect = None
|
|
142
|
+
|
|
143
|
+
# Role permissions
|
|
144
|
+
guild.get_role.return_value = None
|
|
145
|
+
await manager.restore_role_permissions(guild, 1)
|
|
146
|
+
guild.get_role.return_value = r1
|
|
147
|
+
await manager.restore_role_permissions(guild, 1)
|
|
148
|
+
r1.edit.side_effect = Exception("E")
|
|
149
|
+
await manager.restore_role_permissions(guild, 1)
|
|
150
|
+
|
|
151
|
+
# Loops
|
|
152
|
+
bot.is_closed.side_effect = [False, True]
|
|
153
|
+
with patch.object(manager, 'create_backup', side_effect=Exception("L")):
|
|
154
|
+
with patch.object(bot.loop, 'create_task') as mock_t:
|
|
155
|
+
manager.start_auto_backup()
|
|
156
|
+
await mock_t.call_args[0][0]
|
|
157
|
+
with patch.object(manager, 'preload_all', AsyncMock()):
|
|
158
|
+
with patch("asyncio.sleep", side_effect=[None, asyncio.CancelledError]):
|
|
159
|
+
try:
|
|
160
|
+
await manager._auto_refresh_loop()
|
|
161
|
+
except asyncio.CancelledError:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Misc operations
|
|
165
|
+
await manager.update_guild_vanity(777, "NEW")
|
|
166
|
+
await manager.get_guild_settings(777)
|
|
167
|
+
with patch("builtins.open", side_effect=Exception("E")):
|
|
168
|
+
await manager._save_all_guild_settings()
|
|
169
|
+
bot.get_guild.return_value = None
|
|
170
|
+
await manager.create_backup(666)
|
|
171
|
+
(tmp_path / "channels_999.json").write_text("invalid")
|
|
172
|
+
await manager.preload_all()
|
|
173
|
+
manager._guild_cache = {}
|
|
174
|
+
await manager.update_guild_vanity(555, "V")
|
|
175
|
+
|
|
176
|
+
# === EDGE CASE TESTS ===
|
|
177
|
+
bot.guilds = []
|
|
178
|
+
|
|
179
|
+
# Lock management
|
|
180
|
+
lock1 = manager._get_guild_lock(999)
|
|
181
|
+
lock2 = manager._get_guild_lock(999)
|
|
182
|
+
assert lock1 is lock2
|
|
183
|
+
lock3 = manager._get_guild_lock(888)
|
|
184
|
+
assert lock3 is not lock1
|
|
185
|
+
|
|
186
|
+
# Exception paths
|
|
187
|
+
with patch.object(manager, '_backup_channels', side_effect=Exception("TEST")):
|
|
188
|
+
result = await manager.create_backup(777)
|
|
189
|
+
assert result.success is False
|
|
190
|
+
|
|
191
|
+
# Auto-refresh task management
|
|
192
|
+
manager._refresh_task = asyncio.create_task(asyncio.sleep(0))
|
|
193
|
+
await asyncio.sleep(0.01)
|
|
194
|
+
manager.start_auto_refresh()
|
|
195
|
+
manager._refresh_task = None
|
|
196
|
+
manager.start_auto_refresh()
|
|
197
|
+
assert manager._refresh_task is not None
|
|
198
|
+
manager._refresh_task.cancel()
|
|
199
|
+
try:
|
|
200
|
+
await manager._refresh_task
|
|
201
|
+
except asyncio.CancelledError:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
# Refresh loop exception handling
|
|
205
|
+
bot.guilds = [MagicMock(id=777)]
|
|
206
|
+
with patch.object(manager, '_update_guild_cache', AsyncMock(side_effect=Exception("E"))):
|
|
207
|
+
with patch("asyncio.sleep", side_effect=[None, asyncio.CancelledError]):
|
|
208
|
+
try:
|
|
209
|
+
await manager._auto_refresh_loop()
|
|
210
|
+
except asyncio.CancelledError:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
# Guild settings loading
|
|
214
|
+
(tmp_path / "guild_settings.json").write_text("invalid json")
|
|
215
|
+
await manager._load_all_guild_settings()
|
|
216
|
+
(tmp_path / "guild_settings.json").write_text('{"777": {"guild_id": 777, "vanity_url_code": "test"}}')
|
|
217
|
+
await manager._load_all_guild_settings()
|
|
218
|
+
assert 777 in manager._guild_cache
|
|
219
|
+
|
|
220
|
+
# Vanity URL operations
|
|
221
|
+
manager._guild_cache = {}
|
|
222
|
+
vanity = await manager.get_guild_vanity(777)
|
|
223
|
+
assert vanity == "test"
|
|
224
|
+
vanity = await manager.get_guild_vanity(999)
|
|
225
|
+
assert vanity is None
|
|
226
|
+
with patch.object(manager, '_load_all_guild_settings', AsyncMock(side_effect=Exception("E"))):
|
|
227
|
+
manager._guild_cache = {}
|
|
228
|
+
vanity = await manager.get_guild_vanity(777)
|
|
229
|
+
assert vanity is None
|
|
230
|
+
manager._guild_cache[888] = {"vanity_url_code": "cached"}
|
|
231
|
+
vanity = await manager.get_guild_vanity(888)
|
|
232
|
+
assert vanity == "cached"
|
|
233
|
+
|
|
234
|
+
# Guild settings operations
|
|
235
|
+
manager._guild_cache[777] = {"guild_id": 777, "name": "Test"}
|
|
236
|
+
settings = await manager.get_guild_settings(777)
|
|
237
|
+
assert settings["name"] == "Test"
|
|
238
|
+
manager._guild_cache = {}
|
|
239
|
+
settings = await manager.get_guild_settings(777)
|
|
240
|
+
assert settings["guild_id"] == 777
|
|
241
|
+
settings = await manager.get_guild_settings(999)
|
|
242
|
+
assert settings is None
|
|
243
|
+
with patch.object(manager, '_load_all_guild_settings', AsyncMock(side_effect=Exception("E"))):
|
|
244
|
+
manager._guild_cache = {}
|
|
245
|
+
settings = await manager.get_guild_settings(777)
|
|
246
|
+
|
|
247
|
+
# Exception handling
|
|
248
|
+
with patch.object(manager, '_save_all_guild_settings', AsyncMock(side_effect=Exception("E"))):
|
|
249
|
+
await manager.update_guild_vanity(888, "code")
|
|
250
|
+
guild2 = MagicMock(spec=discord.Guild)
|
|
251
|
+
guild2.id = 999; guild2.vanity_url_code = None; guild2.name = "Test"
|
|
252
|
+
guild2.icon = None; guild2.banner = None; guild2.description = None
|
|
253
|
+
with patch.object(manager, '_save_all_guild_settings', AsyncMock(side_effect=Exception("E"))):
|
|
254
|
+
result = await manager._backup_guild_settings(guild2)
|
|
255
|
+
assert result == {} or result.get("guild_id") == 999
|
|
256
|
+
with patch("builtins.open", side_effect=Exception("E")):
|
|
257
|
+
await manager._update_guild_cache(777)
|
|
258
|
+
manager._cache_loaded = True
|
|
259
|
+
await manager.preload_all()
|
|
260
|
+
|
|
261
|
+
# Permission restoration edge cases
|
|
262
|
+
guild2.get_channel = MagicMock(return_value=None)
|
|
263
|
+
guild2.get_role = MagicMock(return_value=None)
|
|
264
|
+
result = await manager.restore_channel_permissions(guild2, 123)
|
|
265
|
+
assert result is False
|
|
266
|
+
(tmp_path / "channels_999.json").write_text('{"channels": [{"id": 456, "permissions": {}}]}')
|
|
267
|
+
result = await manager.restore_channel_permissions(guild2, 123)
|
|
268
|
+
assert result is False
|
|
269
|
+
(tmp_path / "channels_999.json").write_text('{"channels": [{"id": 123, "permissions": {}}]}')
|
|
270
|
+
result = await manager.restore_channel_permissions(guild2, 123)
|
|
271
|
+
assert result is False
|
|
272
|
+
result = await manager.restore_role_permissions(guild2, 123)
|
|
273
|
+
assert result is False
|
|
274
|
+
(tmp_path / "roles_999.json").write_text('{"roles": [{"id": 456, "permissions": 8}]}')
|
|
275
|
+
result = await manager.restore_role_permissions(guild2, 123)
|
|
276
|
+
assert result is False
|
|
277
|
+
(tmp_path / "roles_999.json").write_text('{"roles": [{"id": 123, "permissions": 8, "position": 5}]}')
|
|
278
|
+
result = await manager.restore_role_permissions(guild2, 123)
|
|
279
|
+
assert result is False
|
|
280
|
+
role = make_role(123, "TestRole")
|
|
281
|
+
guild2.get_role.return_value = role
|
|
282
|
+
result = await manager.restore_role_permissions(guild2, 123)
|
|
283
|
+
assert result is True
|
|
284
|
+
role.edit.side_effect = Exception("E")
|
|
285
|
+
result = await manager.restore_role_permissions(guild2, 123)
|
|
286
|
+
assert result is False
|
|
287
|
+
|
|
288
|
+
# Successful permission restoration
|
|
289
|
+
perm_backup = {"channels": [{"id": 123, "permissions": {"456": {"type": "role", "allow": 8, "deny": 0}}}]}
|
|
290
|
+
(tmp_path / "channels_999.json").write_text(json.dumps(perm_backup))
|
|
291
|
+
chan = make_chan(123, "Test", discord.ChannelType.text)
|
|
292
|
+
guild2.get_channel.return_value = chan
|
|
293
|
+
test_role = make_role(456, "Role")
|
|
294
|
+
guild2.get_role.return_value = test_role
|
|
295
|
+
result = await manager.restore_channel_permissions(guild2, 123)
|
|
296
|
+
assert result is True
|
|
297
|
+
|
|
298
|
+
async def test_missing_coverage_edge_cases(self, tmp_path):
|
|
299
|
+
"""Test all remaining edge cases to hit 100% coverage"""
|
|
300
|
+
bot = MagicMock()
|
|
301
|
+
bot.is_closed.return_value = True
|
|
302
|
+
bot.wait_until_ready = AsyncMock()
|
|
303
|
+
bot.user = MagicMock(id=123, name="Bot")
|
|
304
|
+
bot.guilds = []
|
|
305
|
+
|
|
306
|
+
sx = SecureX(bot, backup_dir=str(tmp_path))
|
|
307
|
+
manager = sx.backup_manager
|
|
308
|
+
|
|
309
|
+
# Test _get_guild_lock creates new locks
|
|
310
|
+
lock1 = manager._get_guild_lock(999)
|
|
311
|
+
lock2 = manager._get_guild_lock(999)
|
|
312
|
+
assert lock1 is lock2
|
|
313
|
+
lock3 = manager._get_guild_lock(888)
|
|
314
|
+
assert lock3 is not lock1
|
|
315
|
+
|
|
316
|
+
# Test create_backup exception path
|
|
317
|
+
with patch.object(manager, '_backup_channels', side_effect=Exception("TEST")):
|
|
318
|
+
result = await manager.create_backup(777)
|
|
319
|
+
assert result.success is False
|
|
320
|
+
|
|
321
|
+
# Test start_auto_refresh when task already exists
|
|
322
|
+
manager._refresh_task = asyncio.create_task(asyncio.sleep(0))
|
|
323
|
+
await asyncio.sleep(0.01)
|
|
324
|
+
manager.start_auto_refresh()
|
|
325
|
+
|
|
326
|
+
# Test start_auto_refresh when task is None
|
|
327
|
+
manager._refresh_task = None
|
|
328
|
+
manager.start_auto_refresh()
|
|
329
|
+
assert manager._refresh_task is not None
|
|
330
|
+
manager._refresh_task.cancel()
|
|
331
|
+
try:
|
|
332
|
+
await manager._refresh_task
|
|
333
|
+
except asyncio.CancelledError:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
# Test _auto_refresh_loop exception handling
|
|
337
|
+
bot.guilds = [MagicMock(id=777)]
|
|
338
|
+
with patch.object(manager, '_update_guild_cache', AsyncMock(side_effect=Exception("E"))):
|
|
339
|
+
with patch("asyncio.sleep", side_effect=[None, asyncio.CancelledError]):
|
|
340
|
+
try:
|
|
341
|
+
await manager._auto_refresh_loop()
|
|
342
|
+
except asyncio.CancelledError:
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
# Test _load_all_guild_settings with invalid JSON
|
|
346
|
+
(tmp_path / "guild_settings.json").write_text("invalid json")
|
|
347
|
+
await manager._load_all_guild_settings()
|
|
348
|
+
|
|
349
|
+
# Test _load_all_guild_settings when file exists with valid data
|
|
350
|
+
(tmp_path / "guild_settings.json").write_text('{"777": {"guild_id": 777, "vanity_url_code": "test"}}')
|
|
351
|
+
await manager._load_all_guild_settings()
|
|
352
|
+
assert 777 in manager._guild_cache
|
|
353
|
+
|
|
354
|
+
# Test get_guild_vanity when not in cache but in file
|
|
355
|
+
manager._guild_cache = {}
|
|
356
|
+
vanity = await manager.get_guild_vanity(777)
|
|
357
|
+
assert vanity == "test"
|
|
358
|
+
|
|
359
|
+
# Test get_guild_vanity when not found anywhere
|
|
360
|
+
vanity = await manager.get_guild_vanity(999)
|
|
361
|
+
assert vanity is None
|
|
362
|
+
|
|
363
|
+
# Test get_guild_vanity exception handling
|
|
364
|
+
with patch.object(manager, '_load_all_guild_settings', AsyncMock(side_effect=Exception("E"))):
|
|
365
|
+
manager._guild_cache = {}
|
|
366
|
+
vanity = await manager.get_guild_vanity(777)
|
|
367
|
+
assert vanity is None
|
|
368
|
+
|
|
369
|
+
# Test get_guild_settings when already in cache
|
|
370
|
+
manager._guild_cache[777] = {"guild_id": 777, "name": "Test"}
|
|
371
|
+
settings = await manager.get_guild_settings(777)
|
|
372
|
+
assert settings["name"] == "Test"
|
|
373
|
+
|
|
374
|
+
# Test get_guild_settings when not in cache but in file
|
|
375
|
+
manager._guild_cache = {}
|
|
376
|
+
settings = await manager.get_guild_settings(777)
|
|
377
|
+
assert settings["guild_id"] == 777
|
|
378
|
+
|
|
379
|
+
# Test get_guild_settings when not found
|
|
380
|
+
settings = await manager.get_guild_settings(999)
|
|
381
|
+
assert settings is None
|
|
382
|
+
|
|
383
|
+
# Test get_guild_settings exception handling
|
|
384
|
+
with patch.object(manager, '_load_all_guild_settings', AsyncMock(side_effect=Exception("E"))):
|
|
385
|
+
manager._guild_cache = {}
|
|
386
|
+
settings = await manager.get_guild_settings(777)
|
|
387
|
+
|
|
388
|
+
# Test update_guild_vanity exception handling
|
|
389
|
+
with patch.object(manager, '_save_all_guild_settings', AsyncMock(side_effect=Exception("E"))):
|
|
390
|
+
await manager.update_guild_vanity(888, "code")
|
|
391
|
+
|
|
392
|
+
# Test _backup_guild_settings exception handling
|
|
393
|
+
guild = MagicMock(spec=discord.Guild)
|
|
394
|
+
guild.id = 999
|
|
395
|
+
guild.vanity_url_code = None
|
|
396
|
+
guild.name = "Test"
|
|
397
|
+
guild.icon = None
|
|
398
|
+
guild.banner = None
|
|
399
|
+
guild.description = None
|
|
400
|
+
|
|
401
|
+
with patch.object(manager, '_save_all_guild_settings', AsyncMock(side_effect=Exception("E"))):
|
|
402
|
+
result = await manager._backup_guild_settings(guild)
|
|
403
|
+
assert result == {} or result.get("guild_id") == 999
|
|
404
|
+
|
|
405
|
+
# Test _update_guild_cache exception handling
|
|
406
|
+
with patch("builtins.open", side_effect=Exception("E")):
|
|
407
|
+
await manager._update_guild_cache(777)
|
|
408
|
+
|
|
409
|
+
# Test preload_all when already loaded
|
|
410
|
+
manager._cache_loaded = True
|
|
411
|
+
await manager.preload_all()
|
|
412
|
+
|
|
413
|
+
# Test restore_channel_permissions with missing backup file
|
|
414
|
+
guild = MagicMock(spec=discord.Guild)
|
|
415
|
+
guild.id = 999
|
|
416
|
+
result = await manager.restore_channel_permissions(guild, 123)
|
|
417
|
+
assert result is False
|
|
418
|
+
|
|
419
|
+
# Test restore_channel_permissions with missing channel data
|
|
420
|
+
(tmp_path / "channels_999.json").write_text('{"channels": [{"id": 456, "permissions": {}}]}')
|
|
421
|
+
result = await manager.restore_channel_permissions(guild, 123)
|
|
422
|
+
assert result is False
|
|
423
|
+
|
|
424
|
+
# Test restore_channel_permissions with missing channel
|
|
425
|
+
(tmp_path / "channels_999.json").write_text('{"channels": [{"id": 123, "permissions": {}}]}')
|
|
426
|
+
guild.get_channel.return_value = None
|
|
427
|
+
result = await manager.restore_channel_permissions(guild, 123)
|
|
428
|
+
assert result is False
|
|
429
|
+
|
|
430
|
+
# Test restore_role_permissions with missing backup file
|
|
431
|
+
result = await manager.restore_role_permissions(guild, 123)
|
|
432
|
+
assert result is False
|
|
433
|
+
|
|
434
|
+
# Test restore_role_permissions with missing role data
|
|
435
|
+
(tmp_path / "roles_999.json").write_text('{"roles": [{"id": 456, "permissions": 8}]}')
|
|
436
|
+
result = await manager.restore_role_permissions(guild, 123)
|
|
437
|
+
assert result is False
|
|
438
|
+
|
|
439
|
+
# Test restore_role_permissions with missing role
|
|
440
|
+
(tmp_path / "roles_999.json").write_text('{"roles": [{"id": 123, "permissions": 8, "position": 5}]}')
|
|
441
|
+
guild.get_role.return_value = None
|
|
442
|
+
result = await manager.restore_role_permissions(guild, 123)
|
|
443
|
+
assert result is False
|
|
444
|
+
|
|
445
|
+
# Test restore_role_permissions successful path
|
|
446
|
+
role = MagicMock(spec=discord.Role)
|
|
447
|
+
role.edit = AsyncMock()
|
|
448
|
+
guild.get_role.return_value = role
|
|
449
|
+
result = await manager.restore_role_permissions(guild, 123)
|
|
450
|
+
assert result is True
|
|
451
|
+
|
|
452
|
+
# Test restore_role_permissions exception handling
|
|
453
|
+
role.edit.side_effect = Exception("E")
|
|
454
|
+
result = await manager.restore_role_permissions(guild, 123)
|
|
455
|
+
assert result is False
|
|
456
|
+
|
|
457
|
+
# Helper functions for additional tests
|
|
458
|
+
def make_chan(id, name, type, cat=None):
|
|
459
|
+
c = MagicMock(spec=discord.abc.GuildChannel)
|
|
460
|
+
if type == discord.ChannelType.text:
|
|
461
|
+
c = MagicMock(spec=discord.TextChannel)
|
|
462
|
+
c.topic = "t"; c.nsfw = False; c.slowmode_delay = 0
|
|
463
|
+
c.id = id; c.name = name; c.type = type; c.position = 0; c.category = cat
|
|
464
|
+
c.overwrites = {}
|
|
465
|
+
c.__hash__.return_value = id
|
|
466
|
+
c.edit = AsyncMock()
|
|
467
|
+
c.set_permissions = AsyncMock()
|
|
468
|
+
return c
|
|
469
|
+
|
|
470
|
+
def make_role(id, name):
|
|
471
|
+
r = MagicMock(spec=discord.Role)
|
|
472
|
+
r.id = id; r.name = name
|
|
473
|
+
r.__hash__.return_value = id
|
|
474
|
+
return r
|
|
475
|
+
|
|
476
|
+
guild2 = MagicMock(spec=discord.Guild)
|
|
477
|
+
guild2.id = 999
|
|
478
|
+
guild2.get_channel = MagicMock(return_value=None)
|
|
479
|
+
guild2.get_role = MagicMock(return_value=None)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# Test restore_channel_permissions success path
|
|
483
|
+
perm_backup = {
|
|
484
|
+
"channels": [{
|
|
485
|
+
"id": 123,
|
|
486
|
+
"permissions": {
|
|
487
|
+
"456": {"type": "role", "allow": 8, "deny": 0}
|
|
488
|
+
}
|
|
489
|
+
}]
|
|
490
|
+
}
|
|
491
|
+
(tmp_path / f"channels_{guild.id}.json").write_text(json.dumps(perm_backup))
|
|
492
|
+
|
|
493
|
+
chan = make_chan(123, "Test", discord.ChannelType.text)
|
|
494
|
+
guild.get_channel.return_value = chan
|
|
495
|
+
role = make_role(456, "Role")
|
|
496
|
+
guild.get_role.return_value = role
|
|
497
|
+
|
|
498
|
+
result = await manager.restore_channel_permissions(guild, 123)
|
|
499
|
+
assert result is True
|
|
500
|
+
|
|
501
|
+
# Test get_guild_vanity when in cache from start
|
|
502
|
+
manager._guild_cache[888] = {"vanity_url_code": "cached"}
|
|
503
|
+
vanity = await manager.get_guild_vanity(888)
|
|
504
|
+
assert vanity == "cached"
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import asyncio
|
|
3
|
+
import discord
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from securex.models import ThreatEvent, BackupInfo, RestoreResult, WhitelistChange, UserToken
|
|
8
|
+
from securex.utils.punishment import PunishmentExecutor
|
|
9
|
+
from securex.utils.whitelist import WhitelistManager
|
|
10
|
+
from unittest.mock import Mock, AsyncMock, patch
|
|
11
|
+
|
|
12
|
+
@pytest.mark.asyncio
|
|
13
|
+
class TestUtilsExhaustive:
|
|
14
|
+
"""Exhaustive tests for models, punishment, and whitelist to reach 100% coverage"""
|
|
15
|
+
|
|
16
|
+
async def test_models_full(self):
|
|
17
|
+
"""Test all models serialization"""
|
|
18
|
+
# ThreatEvent
|
|
19
|
+
te = ThreatEvent(
|
|
20
|
+
type="test", guild_id=1, actor_id=2, target_id=3, target_name="n",
|
|
21
|
+
prevented=True, restored=True, timestamp=datetime.now(timezone.utc)
|
|
22
|
+
)
|
|
23
|
+
d = te.to_dict()
|
|
24
|
+
assert d['type'] == "test"
|
|
25
|
+
assert isinstance(d['timestamp'], str)
|
|
26
|
+
assert te.to_json().startswith('{"type": "test"')
|
|
27
|
+
|
|
28
|
+
# BackupInfo
|
|
29
|
+
bi = BackupInfo(guild_id=1, timestamp=datetime.now(timezone.utc), channel_count=5, role_count=10, backup_path="/p")
|
|
30
|
+
bd = bi.to_dict()
|
|
31
|
+
assert bd['channel_count'] == 5
|
|
32
|
+
|
|
33
|
+
# RestoreResult
|
|
34
|
+
rr = RestoreResult(success=True, items_restored=5, items_failed=0)
|
|
35
|
+
assert rr.to_dict()['success'] is True
|
|
36
|
+
|
|
37
|
+
# WhitelistChange
|
|
38
|
+
wc = WhitelistChange(guild_id=1, user_id=2, action="added")
|
|
39
|
+
assert wc.to_dict()['action'] == "added"
|
|
40
|
+
|
|
41
|
+
# UserToken
|
|
42
|
+
ut = UserToken(guild_id=123, token="test_token", set_by=999, description="Test")
|
|
43
|
+
assert ut.guild_id == 123
|
|
44
|
+
assert ut.token == "test_token"
|
|
45
|
+
assert ut.last_used is None
|
|
46
|
+
ut.mark_used()
|
|
47
|
+
assert ut.last_used is not None
|
|
48
|
+
ud = ut.to_dict()
|
|
49
|
+
assert ud['guild_id'] == 123
|
|
50
|
+
assert isinstance(ud['timestamp'], str)
|
|
51
|
+
|
|
52
|
+
async def test_punishment_full(self):
|
|
53
|
+
"""Test all punishment branches"""
|
|
54
|
+
bot = Mock()
|
|
55
|
+
executor = PunishmentExecutor(bot)
|
|
56
|
+
guild = Mock()
|
|
57
|
+
user = Mock(id=123)
|
|
58
|
+
sdk = Mock()
|
|
59
|
+
sdk.timeout_duration = 60
|
|
60
|
+
sdk.notify_punished_user = True
|
|
61
|
+
|
|
62
|
+
# sdk is None
|
|
63
|
+
assert await executor.punish(guild, user, "type", sdk=None) == "none"
|
|
64
|
+
|
|
65
|
+
# action is none
|
|
66
|
+
sdk.punishments = {"type": "none"}
|
|
67
|
+
assert await executor.punish(guild, user, "type", sdk=sdk) == "none"
|
|
68
|
+
|
|
69
|
+
# Not a member
|
|
70
|
+
guild.get_member.return_value = None
|
|
71
|
+
sdk.punishments = {"type": "kick"}
|
|
72
|
+
assert await executor.punish(guild, user, "type", sdk=sdk) == "kick"
|
|
73
|
+
|
|
74
|
+
# Owner bypass
|
|
75
|
+
member = Mock(id=123)
|
|
76
|
+
guild.owner_id = 123
|
|
77
|
+
guild.get_member.return_value = member
|
|
78
|
+
assert await executor.punish(guild, user, "type", sdk=sdk) == "kick"
|
|
79
|
+
|
|
80
|
+
# Warn
|
|
81
|
+
guild.owner_id = 999
|
|
82
|
+
sdk.punishments = {"type": "warn"}
|
|
83
|
+
assert await executor.punish(guild, user, "type", sdk=sdk) == "warn"
|
|
84
|
+
|
|
85
|
+
# Timeout
|
|
86
|
+
sdk.punishments = {"type": "timeout"}
|
|
87
|
+
member.timeout = AsyncMock()
|
|
88
|
+
member.send = AsyncMock()
|
|
89
|
+
assert await executor.punish(guild, user, "type", sdk=sdk) == "timeout"
|
|
90
|
+
member.timeout.assert_called_once()
|
|
91
|
+
|
|
92
|
+
# Kick
|
|
93
|
+
sdk.punishments = {"type": "kick"}
|
|
94
|
+
member.kick = AsyncMock()
|
|
95
|
+
assert await executor.punish(guild, user, "type", sdk=sdk) == "kick"
|
|
96
|
+
|
|
97
|
+
# Ban
|
|
98
|
+
sdk.punishments = {"type": "ban"}
|
|
99
|
+
member.ban = AsyncMock()
|
|
100
|
+
assert await executor.punish(guild, user, "type", sdk=sdk) == "ban"
|
|
101
|
+
|
|
102
|
+
# Forbidden error
|
|
103
|
+
member.ban.side_effect = discord.Forbidden(Mock(), "fail")
|
|
104
|
+
await executor.punish(guild, user, "type", sdk=sdk)
|
|
105
|
+
|
|
106
|
+
# General Exception
|
|
107
|
+
member.ban.side_effect = Exception("err")
|
|
108
|
+
await executor.punish(guild, user, "type", sdk=sdk)
|
|
109
|
+
|
|
110
|
+
# Notify user branches
|
|
111
|
+
member.send.side_effect = discord.Forbidden(Mock(), "fail")
|
|
112
|
+
await executor._notify_user(member, "type", "ban", "details", sdk)
|
|
113
|
+
member.send.side_effect = Exception("err")
|
|
114
|
+
await executor._notify_user(member, "type", "ban", "details", sdk)
|
|
115
|
+
|
|
116
|
+
async def test_whitelist_full(self, tmp_path):
|
|
117
|
+
"""Test whitelist manager branches"""
|
|
118
|
+
sdk = Mock()
|
|
119
|
+
sdk.bot = Mock()
|
|
120
|
+
sdk._trigger_callbacks = AsyncMock()
|
|
121
|
+
|
|
122
|
+
# Patch data_dir to tmp_path
|
|
123
|
+
with patch("securex.utils.whitelist.Path", return_value=tmp_path):
|
|
124
|
+
# We need to be careful with Path inheritance.
|
|
125
|
+
# Better to just set manager.data_dir directly if possible, but __init__ sets it.
|
|
126
|
+
# Let's patch WhitelistManager's Path in its module.
|
|
127
|
+
with patch("securex.utils.whitelist.Path") as mock_path:
|
|
128
|
+
mock_path.return_value = tmp_path
|
|
129
|
+
manager = WhitelistManager(sdk)
|
|
130
|
+
manager.data_dir = tmp_path # Ensure it's set
|
|
131
|
+
|
|
132
|
+
# add/is_whitelisted
|
|
133
|
+
await manager.add(777, 123)
|
|
134
|
+
assert await manager.is_whitelisted(777, 123) is True
|
|
135
|
+
assert await manager.is_whitelisted(777, 999) is False
|
|
136
|
+
|
|
137
|
+
# remove
|
|
138
|
+
await manager.remove(777, 123)
|
|
139
|
+
assert await manager.is_whitelisted(777, 123) is False
|
|
140
|
+
|
|
141
|
+
# get_all
|
|
142
|
+
await manager.add(777, 456)
|
|
143
|
+
assert 456 in await manager.get_all(777)
|
|
144
|
+
|
|
145
|
+
# Preload (success)
|
|
146
|
+
(tmp_path / "888.json").write_text(json.dumps({"users": [1, 2]}))
|
|
147
|
+
manager._cache_loaded = False
|
|
148
|
+
await manager.preload_all()
|
|
149
|
+
assert await manager.is_whitelisted(888, 1) is True
|
|
150
|
+
|
|
151
|
+
# Preload (already loaded)
|
|
152
|
+
await manager.preload_all()
|
|
153
|
+
|
|
154
|
+
# Preload (error)
|
|
155
|
+
(tmp_path / "bad.json").write_text("invalid")
|
|
156
|
+
manager._cache_loaded = False
|
|
157
|
+
await manager.preload_all()
|
|
158
|
+
|
|
159
|
+
# clear
|
|
160
|
+
await manager.clear(777)
|
|
161
|
+
assert len(await manager.get_all(777)) == 0
|
|
162
|
+
|
|
163
|
+
# Load fallback in remove
|
|
164
|
+
(tmp_path / "998.json").write_text(json.dumps({"users": [8]}))
|
|
165
|
+
manager._whitelists.pop(998, None)
|
|
166
|
+
await manager.remove(998, 8)
|
|
167
|
+
assert 8 not in await manager.get_all(998)
|
|
168
|
+
|
|
169
|
+
# Load fallback in get_all
|
|
170
|
+
(tmp_path / "997.json").write_text(json.dumps({"users": [7]}))
|
|
171
|
+
manager._whitelists.pop(997, None)
|
|
172
|
+
assert 7 in await manager.get_all(997)
|
|
173
|
+
|
|
174
|
+
# Load fallback in is_whitelisted
|
|
175
|
+
(tmp_path / "999.json").write_text(json.dumps({"users": [9]}))
|
|
176
|
+
manager._whitelists.pop(999, None)
|
|
177
|
+
assert await manager.is_whitelisted(999, 9) is True
|