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,341 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import asyncio
|
|
3
|
+
import discord
|
|
4
|
+
from datetime import datetime, timezone, timedelta
|
|
5
|
+
from securex import SecureX
|
|
6
|
+
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
|
7
|
+
|
|
8
|
+
class MockAuditLog:
|
|
9
|
+
def __init__(self, entries):
|
|
10
|
+
self.entries = entries
|
|
11
|
+
def __aiter__(self):
|
|
12
|
+
return self
|
|
13
|
+
async def __anext__(self):
|
|
14
|
+
if not self.entries:
|
|
15
|
+
raise StopAsyncIteration
|
|
16
|
+
return self.entries.pop(0)
|
|
17
|
+
|
|
18
|
+
@pytest.mark.asyncio
|
|
19
|
+
class TestHandlersExhaustive:
|
|
20
|
+
"""Exhaustive tests for securex/handlers/ to reach 100% coverage"""
|
|
21
|
+
|
|
22
|
+
async def test_channel_handler_full(self):
|
|
23
|
+
"""Test ChannelHandler branches for 100% coverage"""
|
|
24
|
+
bot = Mock(spec=discord.Client)
|
|
25
|
+
bot.user = Mock(id=123)
|
|
26
|
+
sx = SecureX(bot)
|
|
27
|
+
handler = sx.channel_handler
|
|
28
|
+
|
|
29
|
+
guild = Mock(spec=discord.Guild)
|
|
30
|
+
guild.id = 777
|
|
31
|
+
guild.owner_id = 111
|
|
32
|
+
|
|
33
|
+
# 1. _is_authorized branches
|
|
34
|
+
sx.whitelist.is_whitelisted = AsyncMock(return_value=True)
|
|
35
|
+
assert await handler._is_authorized(guild, 111) is True # Owner
|
|
36
|
+
assert await handler._is_authorized(guild, 123) is True # Bot
|
|
37
|
+
assert await handler._is_authorized(guild, 888) is True # Whitelisted
|
|
38
|
+
|
|
39
|
+
sx.whitelist.is_whitelisted.return_value = False
|
|
40
|
+
assert await handler._is_authorized(guild, 999) is False # Not whitelisted
|
|
41
|
+
|
|
42
|
+
# 2. on_channel_delete branches
|
|
43
|
+
channel = Mock(spec=discord.TextChannel)
|
|
44
|
+
channel.guild = guild
|
|
45
|
+
channel.id = 555
|
|
46
|
+
channel.name = "test-channel"
|
|
47
|
+
|
|
48
|
+
with patch.object(sx.backup_manager, 'restore_channel', new_callable=AsyncMock) as mock_restore:
|
|
49
|
+
# A. Authorized deletion (Owner)
|
|
50
|
+
entry_auth = Mock()
|
|
51
|
+
entry_auth.target.id = 555
|
|
52
|
+
entry_auth.user = Mock()
|
|
53
|
+
entry_auth.user.id = 111 # Owner
|
|
54
|
+
entry_auth.user.name = "Owner"
|
|
55
|
+
entry_auth.created_at = datetime.now(timezone.utc)
|
|
56
|
+
guild.audit_logs.return_value = MockAuditLog([entry_auth])
|
|
57
|
+
await handler.on_channel_delete(channel)
|
|
58
|
+
mock_restore.assert_not_called()
|
|
59
|
+
|
|
60
|
+
# B. Authorized deletion (Bot)
|
|
61
|
+
entry_bot = Mock()
|
|
62
|
+
entry_bot.target.id = 555
|
|
63
|
+
entry_bot.user = Mock()
|
|
64
|
+
entry_bot.user.id = 123 # Bot
|
|
65
|
+
entry_bot.created_at = datetime.now(timezone.utc)
|
|
66
|
+
guild.audit_logs.return_value = MockAuditLog([entry_bot])
|
|
67
|
+
await handler.on_channel_delete(channel)
|
|
68
|
+
mock_restore.assert_not_called()
|
|
69
|
+
|
|
70
|
+
# C. Unauthorized deletion (Success)
|
|
71
|
+
entry_unauth = Mock()
|
|
72
|
+
entry_unauth.target.id = 555
|
|
73
|
+
entry_unauth.user = Mock()
|
|
74
|
+
entry_unauth.user.id = 999
|
|
75
|
+
entry_unauth.user.name = "Violator"
|
|
76
|
+
entry_unauth.created_at = datetime.now(timezone.utc)
|
|
77
|
+
guild.audit_logs.return_value = MockAuditLog([entry_unauth])
|
|
78
|
+
mock_restore.return_value = Mock()
|
|
79
|
+
await handler.on_channel_delete(channel)
|
|
80
|
+
mock_restore.assert_called_once()
|
|
81
|
+
mock_restore.reset_mock()
|
|
82
|
+
|
|
83
|
+
# D. Category channel restoration
|
|
84
|
+
category = Mock(spec=discord.CategoryChannel)
|
|
85
|
+
category.guild = guild
|
|
86
|
+
category.id = 666
|
|
87
|
+
category.name = "Test Category"
|
|
88
|
+
entry_cat = Mock()
|
|
89
|
+
entry_cat.target.id = 666
|
|
90
|
+
entry_cat.user = Mock()
|
|
91
|
+
entry_cat.user.id = 999
|
|
92
|
+
entry_cat.created_at = datetime.now(timezone.utc)
|
|
93
|
+
guild.audit_logs.return_value = MockAuditLog([entry_cat])
|
|
94
|
+
new_cat = Mock()
|
|
95
|
+
mock_restore.return_value = new_cat
|
|
96
|
+
with patch.object(sx.backup_manager, 'restore_category_children', new_callable=AsyncMock) as mock_cat:
|
|
97
|
+
await handler.on_channel_delete(category)
|
|
98
|
+
mock_cat.assert_called_once()
|
|
99
|
+
|
|
100
|
+
# E. Too old entry
|
|
101
|
+
entry_old = Mock()
|
|
102
|
+
entry_old.created_at = datetime.now(timezone.utc) - timedelta(seconds=60)
|
|
103
|
+
guild.audit_logs.return_value = MockAuditLog([entry_old])
|
|
104
|
+
await handler.on_channel_delete(channel)
|
|
105
|
+
|
|
106
|
+
# F. Entry not found
|
|
107
|
+
guild.audit_logs.return_value = MockAuditLog([])
|
|
108
|
+
await handler.on_channel_delete(channel)
|
|
109
|
+
|
|
110
|
+
# G. Exception path
|
|
111
|
+
guild.audit_logs.side_effect = Exception("Audit Log Error")
|
|
112
|
+
await handler.on_channel_delete(channel)
|
|
113
|
+
|
|
114
|
+
# 3. on_channel_update branches
|
|
115
|
+
before, after = Mock(), Mock()
|
|
116
|
+
after.guild = guild
|
|
117
|
+
after.id = 555
|
|
118
|
+
|
|
119
|
+
# No change
|
|
120
|
+
before.overwrites = {"a": 1}
|
|
121
|
+
after.overwrites = {"a": 1}
|
|
122
|
+
before.position = 1
|
|
123
|
+
after.position = 1
|
|
124
|
+
guild.audit_logs.side_effect = None
|
|
125
|
+
await handler.on_channel_update(before, after)
|
|
126
|
+
|
|
127
|
+
# Unauthorized update
|
|
128
|
+
after.overwrites = {"a": 2}
|
|
129
|
+
entry_upd = Mock()
|
|
130
|
+
entry_upd.action = discord.AuditLogAction.channel_update
|
|
131
|
+
entry_upd.target.id = 555
|
|
132
|
+
entry_upd.user = Mock()
|
|
133
|
+
entry_upd.user.id = 999
|
|
134
|
+
entry_upd.created_at = datetime.now(timezone.utc)
|
|
135
|
+
guild.audit_logs.return_value = MockAuditLog([entry_upd])
|
|
136
|
+
with patch.object(sx.backup_manager, 'restore_channel_permissions', new_callable=AsyncMock) as mock_upd:
|
|
137
|
+
await handler.on_channel_update(before, after)
|
|
138
|
+
mock_upd.assert_called_once()
|
|
139
|
+
|
|
140
|
+
# Too old in update
|
|
141
|
+
entry_upd.created_at = datetime.now(timezone.utc) - timedelta(seconds=60)
|
|
142
|
+
guild.audit_logs.return_value = MockAuditLog([entry_upd])
|
|
143
|
+
await handler.on_channel_update(before, after)
|
|
144
|
+
|
|
145
|
+
# Exception update
|
|
146
|
+
guild.audit_logs.side_effect = Exception("Audit Log Error")
|
|
147
|
+
await handler.on_channel_update(before, after)
|
|
148
|
+
|
|
149
|
+
async def test_role_handler_full(self):
|
|
150
|
+
"""Test RoleHandler branches for 100% coverage"""
|
|
151
|
+
bot = Mock(spec=discord.Client)
|
|
152
|
+
bot.user = Mock(id=123)
|
|
153
|
+
sx = SecureX(bot)
|
|
154
|
+
handler = sx.role_handler
|
|
155
|
+
|
|
156
|
+
guild = Mock(spec=discord.Guild)
|
|
157
|
+
guild.id = 777
|
|
158
|
+
guild.owner_id = 111
|
|
159
|
+
|
|
160
|
+
# 1. _is_authorized
|
|
161
|
+
sx.whitelist.is_whitelisted = AsyncMock(return_value=False)
|
|
162
|
+
assert await handler._is_authorized(guild, 111) is True # Owner
|
|
163
|
+
assert await handler._is_authorized(guild, 123) is True # Bot
|
|
164
|
+
|
|
165
|
+
# 2. on_role_delete
|
|
166
|
+
role = Mock(spec=discord.Role)
|
|
167
|
+
role.guild = guild
|
|
168
|
+
role.id = 444
|
|
169
|
+
role.name = "test-role"
|
|
170
|
+
|
|
171
|
+
entry = Mock()
|
|
172
|
+
entry.target.id = 444
|
|
173
|
+
entry.user = Mock()
|
|
174
|
+
entry.user.id = 999
|
|
175
|
+
entry.created_at = datetime.now(timezone.utc)
|
|
176
|
+
guild.audit_logs.return_value = MockAuditLog([entry])
|
|
177
|
+
|
|
178
|
+
with patch.object(sx.backup_manager, 'restore_role', new_callable=AsyncMock) as mock_rest:
|
|
179
|
+
mock_rest.return_value = True
|
|
180
|
+
await handler.on_role_delete(role)
|
|
181
|
+
mock_rest.assert_called_once()
|
|
182
|
+
|
|
183
|
+
# too old
|
|
184
|
+
entry.created_at = datetime.now(timezone.utc) - timedelta(seconds=60)
|
|
185
|
+
guild.audit_logs.return_value = MockAuditLog([entry])
|
|
186
|
+
await handler.on_role_delete(role)
|
|
187
|
+
|
|
188
|
+
# Exception
|
|
189
|
+
guild.audit_logs.side_effect = Exception("Audit error")
|
|
190
|
+
await handler.on_role_delete(role)
|
|
191
|
+
|
|
192
|
+
# 3. on_role_update
|
|
193
|
+
before, after = Mock(), Mock()
|
|
194
|
+
after.guild = guild
|
|
195
|
+
after.id = 444
|
|
196
|
+
after.name = "Role"
|
|
197
|
+
before.position = 1
|
|
198
|
+
after.position = 1
|
|
199
|
+
before.permissions = discord.Permissions(0)
|
|
200
|
+
after.permissions = discord.Permissions(8)
|
|
201
|
+
|
|
202
|
+
entry_upd = Mock()
|
|
203
|
+
entry_upd.action = discord.AuditLogAction.role_update
|
|
204
|
+
entry_upd.target.id = 444
|
|
205
|
+
entry_upd.user = Mock()
|
|
206
|
+
entry_upd.user.id = 999
|
|
207
|
+
entry_upd.created_at = datetime.now(timezone.utc)
|
|
208
|
+
guild.audit_logs.side_effect = None
|
|
209
|
+
guild.audit_logs.return_value = MockAuditLog([entry_upd])
|
|
210
|
+
|
|
211
|
+
with patch.object(sx.backup_manager, 'restore_role_permissions', new_callable=AsyncMock) as mock_upd:
|
|
212
|
+
mock_upd.return_value = True
|
|
213
|
+
await handler.on_role_update(before, after)
|
|
214
|
+
mock_upd.assert_called_once()
|
|
215
|
+
|
|
216
|
+
# too old
|
|
217
|
+
entry_upd.created_at = datetime.now(timezone.utc) - timedelta(seconds=60)
|
|
218
|
+
guild.audit_logs.return_value = MockAuditLog([entry_upd])
|
|
219
|
+
await handler.on_role_update(before, after)
|
|
220
|
+
|
|
221
|
+
# No positional/perm change
|
|
222
|
+
after.permissions = before.permissions
|
|
223
|
+
await handler.on_role_update(before, after)
|
|
224
|
+
|
|
225
|
+
# Exception in update - make audit_logs raise during iteration
|
|
226
|
+
after.permissions = discord.Permissions(8) # Change permissions again
|
|
227
|
+
|
|
228
|
+
class ExceptionIterator:
|
|
229
|
+
def __init__(self):
|
|
230
|
+
pass
|
|
231
|
+
def __aiter__(self):
|
|
232
|
+
return self
|
|
233
|
+
async def __anext__(self):
|
|
234
|
+
raise Exception("Audit log iteration error")
|
|
235
|
+
|
|
236
|
+
guild.audit_logs.side_effect = None
|
|
237
|
+
guild.audit_logs.return_value = ExceptionIterator()
|
|
238
|
+
await handler.on_role_update(before, after)
|
|
239
|
+
|
|
240
|
+
async def test_member_handler_full(self):
|
|
241
|
+
"""Test MemberHandler branches for 100% coverage"""
|
|
242
|
+
bot = Mock(spec=discord.Client)
|
|
243
|
+
bot.user = Mock(id=123)
|
|
244
|
+
sx = SecureX(bot)
|
|
245
|
+
handler = sx.member_handler
|
|
246
|
+
|
|
247
|
+
guild = Mock(spec=discord.Guild)
|
|
248
|
+
guild.id = 777
|
|
249
|
+
guild.owner_id = 111
|
|
250
|
+
|
|
251
|
+
# _is_authorized
|
|
252
|
+
sx.whitelist.is_whitelisted = AsyncMock(return_value=False)
|
|
253
|
+
assert await handler._is_authorized(guild, 111) is True
|
|
254
|
+
assert await handler._is_authorized(guild, 123) is True
|
|
255
|
+
|
|
256
|
+
user = Mock(spec=discord.User)
|
|
257
|
+
user.id = 222
|
|
258
|
+
user.name = "Victim"
|
|
259
|
+
|
|
260
|
+
# on_member_ban
|
|
261
|
+
entry = Mock()
|
|
262
|
+
entry.target.id = 222
|
|
263
|
+
entry.user = Mock()
|
|
264
|
+
entry.user.id = 999
|
|
265
|
+
entry.created_at = datetime.now(timezone.utc)
|
|
266
|
+
guild.audit_logs.return_value = MockAuditLog([entry])
|
|
267
|
+
guild.unban = AsyncMock()
|
|
268
|
+
|
|
269
|
+
await handler.on_member_ban(guild, user)
|
|
270
|
+
guild.unban.assert_called_once()
|
|
271
|
+
|
|
272
|
+
# entry not found (line 68 coverage)
|
|
273
|
+
guild.audit_logs.return_value = MockAuditLog([])
|
|
274
|
+
await handler.on_member_ban(guild, user)
|
|
275
|
+
|
|
276
|
+
# too old
|
|
277
|
+
entry.created_at = datetime.now(timezone.utc) - timedelta(seconds=60)
|
|
278
|
+
guild.audit_logs.return_value = MockAuditLog([entry])
|
|
279
|
+
await handler.on_member_ban(guild, user)
|
|
280
|
+
|
|
281
|
+
# unban failure (Forbidden)
|
|
282
|
+
entry.created_at = datetime.now(timezone.utc)
|
|
283
|
+
guild.audit_logs.return_value = MockAuditLog([entry])
|
|
284
|
+
guild.unban.side_effect = discord.Forbidden(Mock(), "test")
|
|
285
|
+
await handler.on_member_ban(guild, user)
|
|
286
|
+
|
|
287
|
+
# Exception
|
|
288
|
+
guild.audit_logs.side_effect = Exception("audit error")
|
|
289
|
+
await handler.on_member_ban(guild, user)
|
|
290
|
+
|
|
291
|
+
# on_member_update
|
|
292
|
+
before, after = Mock(), Mock()
|
|
293
|
+
after.guild = guild
|
|
294
|
+
after.id = 222
|
|
295
|
+
after.name = "Victim"
|
|
296
|
+
|
|
297
|
+
role_def = Mock(spec=discord.Role)
|
|
298
|
+
role_def.is_default = Mock(return_value=True) # Line 90 branch
|
|
299
|
+
|
|
300
|
+
role_dang = Mock(spec=discord.Role)
|
|
301
|
+
role_dang.is_default = Mock(return_value=False)
|
|
302
|
+
role_dang.permissions = discord.Permissions(administrator=True)
|
|
303
|
+
role_dang.name = "AdminRole"
|
|
304
|
+
|
|
305
|
+
before.roles = []
|
|
306
|
+
after.roles = [role_def, role_dang]
|
|
307
|
+
|
|
308
|
+
entry_upd = Mock()
|
|
309
|
+
entry_upd.action = discord.AuditLogAction.member_role_update
|
|
310
|
+
entry_upd.target.id = 222
|
|
311
|
+
entry_upd.user = Mock()
|
|
312
|
+
entry_upd.user.id = 999
|
|
313
|
+
entry_upd.created_at = datetime.now(timezone.utc)
|
|
314
|
+
guild.audit_logs.side_effect = None
|
|
315
|
+
guild.audit_logs.return_value = MockAuditLog([entry_upd])
|
|
316
|
+
|
|
317
|
+
after.remove_roles = AsyncMock()
|
|
318
|
+
await handler.on_member_update(before, after)
|
|
319
|
+
after.remove_roles.assert_called_once_with(role_dang, reason="SecureX: Removed dangerous roles (unauthorized update)")
|
|
320
|
+
|
|
321
|
+
# remove_roles failure (Exception path line 106)
|
|
322
|
+
after.remove_roles.reset_mock()
|
|
323
|
+
after.remove_roles.side_effect = discord.HTTPException(Mock(status=400), "Fail")
|
|
324
|
+
guild.audit_logs.return_value = MockAuditLog([entry_upd])
|
|
325
|
+
await handler.on_member_update(before, after)
|
|
326
|
+
|
|
327
|
+
# too old update
|
|
328
|
+
entry_upd.created_at = datetime.now(timezone.utc) - timedelta(seconds=60)
|
|
329
|
+
guild.audit_logs.return_value = MockAuditLog([entry_upd])
|
|
330
|
+
await handler.on_member_update(before, after)
|
|
331
|
+
|
|
332
|
+
# Exception in member update
|
|
333
|
+
guild.audit_logs.side_effect = Exception("member error")
|
|
334
|
+
await handler.on_member_update(before, after)
|
|
335
|
+
|
|
336
|
+
# Test roles unchanged (covers line 68)
|
|
337
|
+
before.roles = [role_def, role_dang]
|
|
338
|
+
after.roles = [role_def, role_dang] # Same roles
|
|
339
|
+
guild.audit_logs.side_effect = None
|
|
340
|
+
await handler.on_member_update(before, after)
|
|
341
|
+
|