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.
@@ -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