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,297 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import asyncio
|
|
3
|
+
import discord
|
|
4
|
+
import json
|
|
5
|
+
from securex import SecureX
|
|
6
|
+
from securex.models import ThreatEvent
|
|
7
|
+
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
@pytest.mark.asyncio
|
|
11
|
+
class TestWorkersExhaustive:
|
|
12
|
+
"""Exhaustive tests for securex/workers/ to reach 100% coverage"""
|
|
13
|
+
|
|
14
|
+
async def test_action_worker_full(self):
|
|
15
|
+
"""Test ActionWorker branches for 100% coverage"""
|
|
16
|
+
bot = Mock(spec=discord.Client)
|
|
17
|
+
bot.user = Mock(id=123)
|
|
18
|
+
sx = SecureX(bot)
|
|
19
|
+
worker = sx.action_worker
|
|
20
|
+
|
|
21
|
+
# 1. Test start/stop
|
|
22
|
+
await worker.start()
|
|
23
|
+
await worker.start()
|
|
24
|
+
assert worker._worker_task is not None
|
|
25
|
+
await worker.stop()
|
|
26
|
+
assert worker._worker_task is None
|
|
27
|
+
|
|
28
|
+
# 2. Test _worker_loop
|
|
29
|
+
entry_mock = Mock(spec=discord.AuditLogEntry)
|
|
30
|
+
worker.action_queue = Mock(spec=asyncio.Queue)
|
|
31
|
+
worker.action_queue.get = AsyncMock(side_effect=[entry_mock, Exception("Queue error"), asyncio.CancelledError])
|
|
32
|
+
with patch.object(worker, '_process_entry', new_callable=AsyncMock) as mock_process:
|
|
33
|
+
await worker._worker_loop()
|
|
34
|
+
mock_process.assert_called_once()
|
|
35
|
+
|
|
36
|
+
# 3. Test _process_entry branches
|
|
37
|
+
guild = Mock(spec=discord.Guild)
|
|
38
|
+
guild.id = 777
|
|
39
|
+
guild.owner_id = 111
|
|
40
|
+
|
|
41
|
+
# A. Bot executor
|
|
42
|
+
entry_bot = Mock(user=Mock(id=123))
|
|
43
|
+
await worker._process_entry(entry_bot)
|
|
44
|
+
|
|
45
|
+
# B. Owner executor
|
|
46
|
+
entry_owner = Mock(user=Mock(id=111), guild=guild)
|
|
47
|
+
await worker._process_entry(entry_owner)
|
|
48
|
+
|
|
49
|
+
# C. Whitelisted executor
|
|
50
|
+
entry_wl = Mock(user=Mock(id=888), guild=guild)
|
|
51
|
+
sx.whitelist_cache[777] = {888}
|
|
52
|
+
await worker._process_entry(entry_wl)
|
|
53
|
+
|
|
54
|
+
# D. Unknown violation type
|
|
55
|
+
entry_unknown = Mock(user=Mock(id=999), guild=guild, action=9999)
|
|
56
|
+
await worker._process_entry(entry_unknown)
|
|
57
|
+
|
|
58
|
+
# E. bot_add violation (Success)
|
|
59
|
+
target_bot = Mock(id=555)
|
|
60
|
+
target_bot.name = "MyBot"
|
|
61
|
+
bot_member = Mock(spec=discord.Member)
|
|
62
|
+
bot_member.ban = AsyncMock()
|
|
63
|
+
guild.get_member = Mock(return_value=bot_member)
|
|
64
|
+
entry_bot_add = Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.bot_add, target=target_bot)
|
|
65
|
+
entry_bot_add.user.name = "Violator"
|
|
66
|
+
await worker._process_entry(entry_bot_add)
|
|
67
|
+
bot_member.ban.assert_called_once()
|
|
68
|
+
|
|
69
|
+
# F. bot_add violation (Failure)
|
|
70
|
+
bot_member.ban.side_effect = Exception("Ban failed")
|
|
71
|
+
await worker._process_entry(entry_bot_add)
|
|
72
|
+
|
|
73
|
+
# G. Instant ban types (member_kick)
|
|
74
|
+
entry_kick = Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.kick)
|
|
75
|
+
entry_kick.user.name = "Violator"
|
|
76
|
+
guild.ban = AsyncMock()
|
|
77
|
+
await worker._process_entry(entry_kick)
|
|
78
|
+
guild.ban.assert_called_once()
|
|
79
|
+
|
|
80
|
+
# H. Instant ban types failure
|
|
81
|
+
guild.ban.side_effect = Exception("Guild ban failed")
|
|
82
|
+
await worker._process_entry(entry_kick)
|
|
83
|
+
|
|
84
|
+
# I. General punishment SUCCESS
|
|
85
|
+
sx.punishments["role_create"] = "kick"
|
|
86
|
+
entry_role = Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.role_create, target=Mock(id=444))
|
|
87
|
+
entry_role.target.name = "EvilRole"
|
|
88
|
+
entry_role.user.name = "Violator"
|
|
89
|
+
sx.punisher.punish = AsyncMock(return_value="kick")
|
|
90
|
+
await worker._process_entry(entry_role)
|
|
91
|
+
|
|
92
|
+
# J. General punishment exception
|
|
93
|
+
sx.punisher.punish.side_effect = Exception("Punish failed")
|
|
94
|
+
await worker._process_entry(entry_role)
|
|
95
|
+
|
|
96
|
+
# K. Error path line 114
|
|
97
|
+
with patch.object(worker, 'action_map', side_effect=Exception("Map error")):
|
|
98
|
+
await worker._process_entry(None)
|
|
99
|
+
|
|
100
|
+
async def test_cleanup_worker_full(self):
|
|
101
|
+
"""Test CleanupWorker branches for 100% coverage"""
|
|
102
|
+
bot = Mock(spec=discord.Client)
|
|
103
|
+
bot.user = Mock(id=123)
|
|
104
|
+
sx = SecureX(bot)
|
|
105
|
+
worker = sx.cleanup_worker
|
|
106
|
+
|
|
107
|
+
await worker.start()
|
|
108
|
+
await worker.stop()
|
|
109
|
+
|
|
110
|
+
entry_mock = Mock()
|
|
111
|
+
worker.cleanup_queue = Mock()
|
|
112
|
+
worker.cleanup_queue.get = AsyncMock(side_effect=[entry_mock, Exception("Loop error"), asyncio.CancelledError])
|
|
113
|
+
with patch.object(worker, '_process_entry', new_callable=AsyncMock) as mock_process:
|
|
114
|
+
await worker._worker_loop()
|
|
115
|
+
mock_process.assert_called_once()
|
|
116
|
+
|
|
117
|
+
guild = Mock(spec=discord.Guild)
|
|
118
|
+
guild.id = 777
|
|
119
|
+
guild.owner_id = 111
|
|
120
|
+
|
|
121
|
+
await worker._process_entry(Mock(user=Mock(id=123), guild=guild))
|
|
122
|
+
await worker._process_entry(Mock(user=Mock(id=111), guild=guild))
|
|
123
|
+
sx.whitelist_cache[777] = {888}
|
|
124
|
+
await worker._process_entry(Mock(user=Mock(id=888), guild=guild))
|
|
125
|
+
|
|
126
|
+
await worker._process_entry(Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.ban))
|
|
127
|
+
await worker._process_entry(Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.role_create, target=None))
|
|
128
|
+
|
|
129
|
+
role = Mock()
|
|
130
|
+
role.name = "EvilRole"
|
|
131
|
+
role.delete = AsyncMock()
|
|
132
|
+
guild.get_role = Mock(return_value=role)
|
|
133
|
+
await worker._process_entry(Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.role_create, target=Mock(id=55)))
|
|
134
|
+
role.delete.assert_called_once()
|
|
135
|
+
|
|
136
|
+
channel = Mock()
|
|
137
|
+
channel.name = "EvilChannel"
|
|
138
|
+
channel.delete = AsyncMock()
|
|
139
|
+
guild.get_channel = Mock(return_value=channel)
|
|
140
|
+
await worker._process_entry(Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.channel_create, target=Mock(id=66)))
|
|
141
|
+
channel.delete.assert_called_once()
|
|
142
|
+
|
|
143
|
+
webhook = Mock(id=77)
|
|
144
|
+
webhook.name = "EvilWebhook"
|
|
145
|
+
webhook.delete = AsyncMock()
|
|
146
|
+
guild.webhooks = AsyncMock(return_value=[webhook])
|
|
147
|
+
await worker._process_entry(Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.webhook_create, target=Mock(id=77)))
|
|
148
|
+
webhook.delete.assert_called_once()
|
|
149
|
+
|
|
150
|
+
role.delete.side_effect = discord.Forbidden(Mock(), "test")
|
|
151
|
+
await worker._process_entry(Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.role_create, target=Mock(id=55)))
|
|
152
|
+
|
|
153
|
+
role.delete.side_effect = Exception("Boom")
|
|
154
|
+
await worker._process_entry(Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.role_create, target=Mock(id=55)))
|
|
155
|
+
|
|
156
|
+
# General loop exception line 94
|
|
157
|
+
with patch.object(worker, 'bot', side_effect=Exception("Bot error")):
|
|
158
|
+
await worker._process_entry(None)
|
|
159
|
+
|
|
160
|
+
async def test_log_worker_full(self):
|
|
161
|
+
"""Test LogWorker branches for 100% coverage"""
|
|
162
|
+
bot = Mock(spec=discord.Client)
|
|
163
|
+
bot.user = Mock(id=123)
|
|
164
|
+
sx = SecureX(bot)
|
|
165
|
+
worker = sx.log_worker
|
|
166
|
+
|
|
167
|
+
await worker.start()
|
|
168
|
+
await worker.stop()
|
|
169
|
+
|
|
170
|
+
entry_mock = Mock()
|
|
171
|
+
worker.log_queue = Mock()
|
|
172
|
+
worker.log_queue.get = AsyncMock(side_effect=[entry_mock, Exception("Log error"), asyncio.CancelledError])
|
|
173
|
+
with patch.object(worker, '_process_entry', new_callable=AsyncMock) as mock_process:
|
|
174
|
+
await worker._worker_loop()
|
|
175
|
+
mock_process.assert_called_once()
|
|
176
|
+
|
|
177
|
+
guild = Mock(id=777, owner_id=111)
|
|
178
|
+
await worker._process_entry(Mock(user=Mock(id=123), guild=guild))
|
|
179
|
+
await worker._process_entry(Mock(user=Mock(id=999), guild=guild, action=9999))
|
|
180
|
+
|
|
181
|
+
sx._trigger_callbacks = AsyncMock()
|
|
182
|
+
entry = Mock(user=Mock(id=999), guild=guild, action=discord.AuditLogAction.role_create, target=Mock(id=55), created_at=None)
|
|
183
|
+
await worker._process_entry(entry)
|
|
184
|
+
sx._trigger_callbacks.assert_called_once()
|
|
185
|
+
|
|
186
|
+
sx._trigger_callbacks.side_effect = Exception("Callback error")
|
|
187
|
+
await worker._process_entry(entry)
|
|
188
|
+
|
|
189
|
+
async def test_guild_worker_full(self, tmp_path):
|
|
190
|
+
"""Test GuildWorker branches for 100% coverage"""
|
|
191
|
+
bot = Mock(spec=discord.Client)
|
|
192
|
+
bot.user = Mock(id=123)
|
|
193
|
+
sx = SecureX(bot)
|
|
194
|
+
|
|
195
|
+
tokens_file = tmp_path / "user_tokens.json"
|
|
196
|
+
sx.backup_dir = tmp_path
|
|
197
|
+
|
|
198
|
+
worker = sx.guild_worker
|
|
199
|
+
worker._tokens_file = tokens_file
|
|
200
|
+
|
|
201
|
+
# 0. Successful token load
|
|
202
|
+
tokens_file.write_text(json.dumps({"123": "token123"}))
|
|
203
|
+
await worker._load_tokens()
|
|
204
|
+
assert worker.get_user_token(123) == "token123"
|
|
205
|
+
|
|
206
|
+
# 1. start/stop
|
|
207
|
+
await worker.start()
|
|
208
|
+
await worker.stop()
|
|
209
|
+
|
|
210
|
+
with patch("json.load", side_effect=Exception("JSON error")):
|
|
211
|
+
tokens_file.write_text("{}")
|
|
212
|
+
await worker._load_tokens()
|
|
213
|
+
|
|
214
|
+
with patch("json.dump", side_effect=Exception("Dump error")):
|
|
215
|
+
await worker._save_tokens()
|
|
216
|
+
|
|
217
|
+
await worker.set_user_token(777, "token123")
|
|
218
|
+
assert worker.get_user_token(777) == "token123"
|
|
219
|
+
await worker.remove_user_token(777)
|
|
220
|
+
assert worker.get_user_token(777) is None
|
|
221
|
+
await worker.remove_user_token(999)
|
|
222
|
+
|
|
223
|
+
entry_mock = Mock(action=discord.AuditLogAction.guild_update, user=Mock(id=999))
|
|
224
|
+
entry_mock.changes = [Mock(key="name", before="Old", after="New")]
|
|
225
|
+
worker.sdk.guild_queue = Mock()
|
|
226
|
+
worker.sdk.guild_queue.get = AsyncMock(side_effect=[
|
|
227
|
+
Mock(action=discord.AuditLogAction.ban),
|
|
228
|
+
Mock(action=discord.AuditLogAction.guild_update, user=Mock(id=123)),
|
|
229
|
+
Mock(action=discord.AuditLogAction.guild_update, user=Mock(id=999), changes=[]),
|
|
230
|
+
entry_mock,
|
|
231
|
+
Exception("Loop error"),
|
|
232
|
+
asyncio.CancelledError
|
|
233
|
+
])
|
|
234
|
+
with patch.object(worker, '_restore_guild_settings', new_callable=AsyncMock) as mock_restore:
|
|
235
|
+
await worker._restoration_loop()
|
|
236
|
+
mock_restore.assert_called_once()
|
|
237
|
+
|
|
238
|
+
assert await worker._restore_vanity_via_api(777, "vanity") is False
|
|
239
|
+
await worker.set_user_token(777, "token123")
|
|
240
|
+
|
|
241
|
+
with patch("aiohttp.ClientSession.patch") as mock_patch:
|
|
242
|
+
mock_resp = MagicMock()
|
|
243
|
+
mock_resp.status = 200
|
|
244
|
+
mock_patch.return_value.__aenter__.return_value = mock_resp
|
|
245
|
+
|
|
246
|
+
assert await worker._restore_vanity_via_api(777, "vanity") is True
|
|
247
|
+
|
|
248
|
+
mock_resp.status = 400
|
|
249
|
+
mock_resp.text = AsyncMock(return_value="error")
|
|
250
|
+
assert await worker._restore_vanity_via_api(777, "vanity") is False
|
|
251
|
+
|
|
252
|
+
mock_patch.side_effect = Exception("Network error")
|
|
253
|
+
assert await worker._restore_vanity_via_api(777, "vanity") is False
|
|
254
|
+
|
|
255
|
+
guild = Mock(spec=discord.Guild)
|
|
256
|
+
guild.id = 777
|
|
257
|
+
guild.edit = AsyncMock()
|
|
258
|
+
|
|
259
|
+
sx.backup_manager.get_guild_settings = AsyncMock(return_value=None)
|
|
260
|
+
await worker._restore_guild_settings({"guild": guild, "changes": {}})
|
|
261
|
+
|
|
262
|
+
backup = {
|
|
263
|
+
"name": "Old",
|
|
264
|
+
"vanity_url_code": "old-v",
|
|
265
|
+
"icon": "icon-data",
|
|
266
|
+
"banner": "banner-data",
|
|
267
|
+
"description": "desc"
|
|
268
|
+
}
|
|
269
|
+
sx.backup_manager.get_guild_settings = AsyncMock(return_value=backup)
|
|
270
|
+
|
|
271
|
+
changes = {
|
|
272
|
+
"vanity_url_code": ("old-v", "new-v"),
|
|
273
|
+
"name": ("Old", "New"),
|
|
274
|
+
"icon": ("h1", "h2"),
|
|
275
|
+
"banner": ("b1", "b2"),
|
|
276
|
+
"description": ("d1", "d2")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
with patch.object(worker, '_restore_vanity_via_api', return_value=True):
|
|
280
|
+
await worker._restore_guild_settings({"guild": guild, "changes": changes})
|
|
281
|
+
guild.edit.assert_called_once()
|
|
282
|
+
|
|
283
|
+
# Vanity fail branch
|
|
284
|
+
with patch.object(worker, '_restore_vanity_via_api', return_value=False):
|
|
285
|
+
await worker._restore_guild_settings({"guild": guild, "changes": {"vanity_url_code": "v"}})
|
|
286
|
+
|
|
287
|
+
guild.edit.side_effect = discord.HTTPException(Mock(status=400), "Bad request")
|
|
288
|
+
await worker._restore_guild_settings({"guild": guild, "changes": {"name": "n"}})
|
|
289
|
+
|
|
290
|
+
mock_resp = Mock(status=400)
|
|
291
|
+
ex = discord.HTTPException(mock_resp, "err")
|
|
292
|
+
ex.code = 50035
|
|
293
|
+
guild.edit.side_effect = ex
|
|
294
|
+
await worker._restore_guild_settings({"guild": guild, "changes": {"name": "n"}})
|
|
295
|
+
|
|
296
|
+
guild.edit.side_effect = Exception("Critical fail")
|
|
297
|
+
await worker._restore_guild_settings({"guild": guild, "changes": {"name": "n"}})
|