• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

discord-modmail / modmail / 11041384318

25 Sep 2024 09:27PM UTC coverage: 71.46% (-1.5%) from 72.981%
11041384318

Pull #138

github

web-flow
Merge aafb3e91a into df5dbb94a
Pull Request #138: update lint config

569 of 1096 branches covered (51.92%)

4 of 4 new or added lines in 4 files covered. (100.0%)

55 existing lines in 3 files now uncovered.

2559 of 3581 relevant lines covered (71.46%)

0.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.84
/tests/modmail/extensions/utils/test_error_handler.py
1
import inspect
1✔
2
import typing
1✔
3
import unittest.mock
1✔
4

5
import discord
1✔
6
import pytest
1✔
7
from discord.ext import commands
1✔
8

9
from modmail.extensions.utils import error_handler
1✔
10
from modmail.extensions.utils.error_handler import ErrorHandler
1✔
11
from tests import mocks
1✔
12

13

14
# set dev mode for the cog to a truthy value
15
error_handler.ANY_DEV_MODE = 2
1✔
16

17

18
@pytest.fixture
1✔
19
def cog():
1✔
20
    """Pytest fixture for error_handler."""
21
    return ErrorHandler(mocks.MockBot())
1✔
22

23

24
@pytest.fixture
1✔
25
def ctx():
1✔
26
    """Pytest fixture for MockContext."""
27
    return mocks.MockContext()
1✔
28

29

30
@pytest.fixture
1✔
31
def command():
1✔
32
    """Fixture for discord.ext.commands.Command."""
33
    command = unittest.mock.NonCallableMock(spec_set=commands.Command(unittest.mock.AsyncMock(), name="mock"))
1✔
UNCOV
34
    return command
×
35

36

37
@pytest.mark.parametrize("is_cooldown", [True, False])
1✔
38
def test_reset_cooldown(ctx, cog, is_cooldown: bool):
1✔
39
    """Test the cooldown is reset if the command is on a cooldown."""
40
    ctx.command.is_on_cooldown.return_value = bool(is_cooldown)
1✔
41
    cog._reset_command_cooldown(ctx)
1✔
42
    assert 1 == ctx.command.is_on_cooldown.call_count
1✔
43
    assert int(is_cooldown) == ctx.command.reset_cooldown.call_count
1✔
44
    if int(is_cooldown) == 1:
1✔
45
        ctx.command.reset_cooldown.assert_called_once_with(ctx)
1✔
46

47

48
def test_error_embed():
1✔
49
    """Test the error embed method creates the correct embed."""
50
    title = "Something very drastic went very wrong!"
1✔
51
    message = "seven southern seas are ready to collapse."
1✔
52
    embed = ErrorHandler.error_embed(title=title, message=message)
1✔
53

54
    assert embed.title == title
1✔
55
    assert embed.description == message
1✔
56
    assert embed.colour == error_handler.ERROR_COLOUR
1✔
57

58

59
@pytest.mark.parametrize(
1✔
60
    ["exception_or_str", "expected_str"],
61
    [
62
        [commands.NSFWChannelRequired(mocks.MockTextChannel()), "NSFW Channel Required"],
63
        [commands.CommandNotFound(), "Command Not Found"],
64
        ["someWEIrdName", "some WE Ird Name"],
65
    ],
66
)
67
def test_get_title_from_name(exception_or_str: typing.Union[Exception, str], expected_str: str):
1✔
68
    """Test the regex works properly for the title from name."""
69
    result = ErrorHandler.get_title_from_name(exception_or_str)
1✔
70
    assert expected_str == result
1✔
71

72

73
@pytest.mark.parametrize("reset_cooldown", [True, False])
1✔
74
@pytest.mark.parametrize(
1✔
75
    ["error", "title", "description"],
76
    [
77
        [
78
            commands.UserInputError("some interesting information."),
79
            "User Input Error",
80
            "some interesting information.",
81
        ],
82
        [
83
            commands.MissingRequiredArgument(inspect.Parameter("SomethingSpecial", kind=1)),
84
            "Missing Required Argument",
85
            "SomethingSpecial is a required argument that is missing.",
86
        ],
87
        [
88
            commands.GuildNotFound("ImportantGuild"),
89
            "Guild Not Found",
90
            'Guild "ImportantGuild" not found.',
91
        ],
92
        [
93
            commands.BadUnionArgument(
94
                inspect.Parameter("colour", 2),
95
                (commands.InviteConverter, commands.ColourConverter),
96
                [commands.BadBoolArgument("colour"), commands.BadColourArgument("colour")],
97
            ),
98
            "Bad Union Argument",
99
            'Could not convert "colour" into Invite Converter or Colour Converter.',
100
        ],
101
    ],
102
)
103
@pytest.mark.asyncio
1✔
104
async def test_handle_user_input_error(
1✔
105
    cog: ErrorHandler,
106
    ctx: mocks.MockContext,
107
    error: commands.UserInputError,
108
    title: str,
109
    description: str,
110
    reset_cooldown: bool,
111
):
112
    """Test user input errors are handled properly."""
113
    with unittest.mock.patch.object(cog, "_reset_command_cooldown") as mock_cooldown_reset:
1✔
114
        embed = await cog.handle_user_input_error(ctx=ctx, error=error, reset_cooldown=reset_cooldown)
1✔
115

116
    assert title == embed.title
1✔
117
    assert description == embed.description
1✔
118

119
    if reset_cooldown:
1✔
120
        assert 1 == mock_cooldown_reset.call_count
1✔
121

122

123
@pytest.mark.parametrize(
1✔
124
    ["error", "bot_perms", "should_send_channel", "member_perms", "should_send_user", "raise_forbidden"],
125
    [
126
        (
127
            commands.BotMissingPermissions(["manage_guild"]),
128
            discord.Permissions(read_messages=True, send_messages=True, embed_links=True),
129
            1,
130
            discord.Permissions(read_messages=True, send_messages=True, embed_links=True),
131
            0,
132
            False,
133
        ),
134
        (
135
            commands.BotMissingPermissions(["administrator"]),
136
            discord.Permissions(read_messages=True, send_messages=True, manage_guild=True),
137
            1,
138
            discord.Permissions(read_messages=True, send_messages=True, embed_links=True),
139
            0,
140
            False,
141
        ),
142
        (
143
            commands.BotMissingPermissions(["mention_everyone"]),
144
            discord.Permissions(read_messages=True, send_messages=True),
145
            1,
146
            discord.Permissions(read_messages=True, send_messages=True, embed_links=True),
147
            0,
148
            False,
149
        ),
150
        (
151
            commands.BotMissingPermissions(["administrator"]),
152
            discord.Permissions(read_messages=False, send_messages=False),
153
            0,
154
            discord.Permissions(read_messages=False),
155
            0,
156
            False,
157
        ),
158
        (
159
            commands.BotMissingPermissions(["change_nickname"]),
160
            discord.Permissions(read_messages=True, send_messages=True),
161
            1,
162
            discord.Permissions(read_messages=True, send_messages=True, administrator=True),
163
            1,
164
            False,
165
        ),
166
        (
167
            commands.BotMissingPermissions(["administrator"]),
168
            discord.Permissions(manage_threads=True, manage_channels=True),
169
            0,
170
            discord.Permissions(administrator=True),
171
            1,
172
            False,
173
        ),
174
        (
175
            commands.BotMissingPermissions(["change_nickname"]),
176
            discord.Permissions(read_messages=True, send_messages=True),
177
            1,
178
            discord.Permissions(read_messages=True, send_messages=True, administrator=True),
179
            1,
180
            True,
181
        ),
182
        (
183
            commands.BotMissingPermissions(["administrator"]),
184
            discord.Permissions(manage_threads=True, manage_channels=True),
185
            0,
186
            discord.Permissions(administrator=True),
187
            1,
188
            True,
189
        ),
190
    ],
191
)
192
@pytest.mark.asyncio
1✔
193
async def test_handle_bot_missing_perms(
1✔
194
    cog: ErrorHandler,
195
    ctx: mocks.MockContext,
196
    error: commands.BotMissingPermissions,
197
    bot_perms: discord.Permissions,
198
    should_send_channel: int,
199
    member_perms: discord.Permissions,
200
    should_send_user: int,
201
    raise_forbidden: bool,
202
):
203
    """
204
    Test error_handler.handle_bot_missing_perms.
205

206
    There are some cases here where the bot is unable to send messages, and that should be clear.
207
    """
208

209
    def mock_permissions_for(member):
1✔
210
        assert isinstance(member, discord.Member)
1✔
211
        if member is ctx.me:
1✔
212
            return bot_perms
1✔
213
        if member is ctx.author:
1!
214
            return member_perms
1✔
215
        # fail since there is no other kind of user who should be passed here
216
        pytest.fail("An invalid member or role was passed to ctx.channel.permissions_for")
×
217

218
    if raise_forbidden:
1✔
219
        error_to_raise = discord.Forbidden(unittest.mock.MagicMock(status=403), "no.")
1✔
220
        ctx.author.send.side_effect = error_to_raise
1✔
221
        ctx.message.author.send.side_effect = error_to_raise
1✔
222

223
    with unittest.mock.patch.object(ctx.channel, "permissions_for", mock_permissions_for):
1✔
224

225
        await cog.handle_bot_missing_perms(ctx, error)
1✔
226

227
    assert should_send_channel == ctx.send.call_count + ctx.channel.send.call_count
1✔
228

229
    # note: this may break depending on dev-mode and relay mode.
230
    assert should_send_user == ctx.author.send.call_count
1✔
231

232

233
@pytest.mark.parametrize(
1✔
234
    ["error", "expected_title"],
235
    [
236
        [
237
            commands.CheckAnyFailure(
238
                ["Something went wrong"],
239
                [commands.NoPrivateMessage(), commands.PrivateMessageOnly()],
240
            ),
241
            "Something went wrong",
242
        ],
243
        [commands.NoPrivateMessage(), "Server Only"],
244
        [commands.PrivateMessageOnly(), "DMs Only"],
245
        [commands.NotOwner(), "Not Owner"],
246
        [commands.MissingPermissions(["send_message"]), "Missing Permissions"],
247
        [commands.BotMissingPermissions(["send_message"]), None],
248
        [commands.MissingRole(mocks.MockRole().id), "Missing Role"],
249
        [commands.BotMissingRole(mocks.MockRole().id), "Bot Missing Role"],
250
        [commands.MissingAnyRole([mocks.MockRole().id]), "Missing Any Role"],
251
        [commands.BotMissingAnyRole([mocks.MockRole().id]), "Bot Missing Any Role"],
252
        [commands.NSFWChannelRequired(mocks.MockTextChannel()), "NSFW Channel Required"],
253
    ],
254
)
255
@pytest.mark.asyncio
1✔
256
async def test_handle_check_failure(
1✔
257
    cog: ErrorHandler, ctx: mocks.MockContext, error: commands.CheckFailure, expected_title: str
258
):
259
    """
260
    Test check failures.
261

262
    In some cases, this method should result in calling a bot_missing_perms method
263
    because the bot cannot send messages.
264
    """
265
    with unittest.mock.patch.object(cog, "handle_bot_missing_perms"):
1✔
266
        if isinstance(error, commands.BotMissingPermissions):
1✔
267
            assert await cog.handle_check_failure(ctx, error) is None
1✔
268
            return
1✔
269
        embed = await cog.handle_check_failure(ctx, error)
1✔
270

271
        assert embed.title == expected_title
1✔
272

273

274
class TestCommandInvokeError:
1✔
275
    """
276
    Collection of tests for ErrorHandler.handle_command_invoke_error.
277

278
    This serves as nothing more but to group the tests for the single method
279
    """
280

281
    @pytest.mark.asyncio
1✔
282
    async def test_forbidden(self, cog: ErrorHandler, ctx: mocks.MockContext):
1✔
283
        """Test discord.Forbidden errors are not met with an attempt to send a message."""
284
        error = commands.CommandInvokeError(unittest.mock.Mock(spec_set=discord.Forbidden))
1✔
285
        with unittest.mock.patch.object(cog, "handle_bot_missing_perms"):
1✔
286
            result = await cog.handle_command_invoke_error(ctx, error)
1✔
287

288
        assert result is None
1✔
289
        assert 0 == ctx.send.call_count
1✔
290

291
    @pytest.mark.parametrize(
1✔
292
        ["module", "title_words", "message_words", "exclude_words"],
293
        [
294
            [
295
                "modmail.extensions.utils.error_handler",
296
                ["internal", "error"],
297
                ["internally", "wrong", "report", "developers"],
298
                ["plugin"],
299
            ],
300
            [
301
                "modmail.plugins.better_error_handler.main",
302
                ["plugin", "error"],
303
                ["plugin", "wrong", "plugin developers"],
304
                None,
305
            ],
306
        ],
307
    )
308
    @pytest.mark.asyncio
1✔
309
    async def test_error(
1✔
310
        self,
311
        cog: ErrorHandler,
312
        ctx: mocks.MockContext,
313
        command: commands.Command,
314
        module: str,
315
        title_words: list,
316
        message_words: list,
317
        exclude_words: list,
318
    ):
319
        """Test that the proper alerts are shared in the returned embed."""
UNCOV
320
        embed = discord.Embed(description="you failed")
×
UNCOV
321
        error = commands.CommandInvokeError(Exception("lul"))
×
UNCOV
322
        ctx.command = command
×
323

324
        # needs a mock cog for __module__
UNCOV
325
        mock_cog = unittest.mock.NonCallableMock(spec_set=commands.Cog)
×
UNCOV
326
        mock_cog.__module__ = module
×
UNCOV
327
        ctx.command.cog = mock_cog
×
328

UNCOV
329
        def error_embed(title, msg):
×
330
            """Replace cog.error_embed and test that the correct params are passed."""
UNCOV
331
            title = title.lower()
×
UNCOV
332
            for word in title_words:
×
UNCOV
333
                assert word in title
×
334

UNCOV
335
            msg = msg.lower()
×
UNCOV
336
            for word in message_words:
×
UNCOV
337
                assert word in msg
×
338

UNCOV
339
            if exclude_words:
×
UNCOV
340
                for word in exclude_words:
×
UNCOV
341
                    assert word not in title
×
UNCOV
342
                    assert word not in msg
×
343

UNCOV
344
            return embed
×
345

UNCOV
346
        with unittest.mock.patch.object(cog, "error_embed", side_effect=error_embed):
×
UNCOV
347
            result = await cog.handle_command_invoke_error(ctx, error)
×
348

UNCOV
349
        assert result is embed
×
350

351

352
class TestOnCommandError:
1✔
353
    """
354
    Collection of tests for ErrorHandler.on_command_error.
355

356
    This serves as nothing more but to group the tests for the single method
357
    """
358

359
    @pytest.mark.asyncio
1✔
360
    async def test_ignore_already_handled(self, cog: ErrorHandler, ctx: mocks.MockContext):
1✔
361
        """Assert errors handled elsewhere are ignored."""
362
        error = commands.NotOwner()
1✔
363
        error.handled = True
1✔
364
        await cog.on_command_error(ctx, error)
1✔
365

366
    @pytest.mark.asyncio
1✔
367
    async def test_ignore_command_not_found(self, cog: ErrorHandler, ctx: mocks.MockContext):
1✔
368
        """Test the command handler ignores command not found errors."""
369
        await cog.on_command_error(ctx, commands.CommandNotFound())
1✔
370

371
        assert 0 == ctx.send.call_count
1✔
372

373
    @pytest.mark.parametrize(
1✔
374
        ["error", "delegate", "embed"],
375
        [
376
            [
377
                commands.UserInputError("User input the wrong thing I guess, not sure."),
378
                "handle_user_input_error",
379
                discord.Embed(description="si"),
380
            ],
381
            [
382
                commands.CheckFailure("Checks failed, crosses passed."),
383
                "handle_check_failure",
384
                discord.Embed(description="also si"),
385
            ],
386
            [
387
                commands.CheckFailure("Checks failed, crosses passed."),
388
                "handle_check_failure",
389
                None,
390
            ],
391
            [
392
                unittest.mock.NonCallableMock(spec_set=commands.CommandInvokeError),
393
                "handle_command_invoke_error",
394
                discord.Embed(description="<generic response>"),
395
            ],
396
            [
397
                unittest.mock.NonCallableMock(spec_set=commands.CommandInvokeError),
398
                "handle_command_invoke_error",
399
                None,
400
            ],
401
        ],
402
    )
403
    @pytest.mark.asyncio
1✔
404
    async def test_errors_delegated(
1✔
405
        self,
406
        cog: ErrorHandler,
407
        ctx: mocks.MockContext,
408
        error: commands.CommandError,
409
        delegate: str,
410
        embed: typing.Optional[discord.Embed],
411
    ):
412
        """Test that the main error method delegates errors as appropriate to helper methods."""
413
        with unittest.mock.patch.object(cog, delegate) as mock:
1✔
414
            mock.return_value = embed
1✔
415
            await cog.on_command_error(ctx, error)
1✔
416

417
        assert 1 == mock.call_count
1✔
418
        assert unittest.mock.call(ctx, error) == mock.call_args
1✔
419

420
        assert int(bool(embed)) == ctx.send.call_count
1✔
421

422
        if embed is None:
1✔
423
            return
1✔
424

425
        assert unittest.mock.call(embeds=[embed]) == ctx.send.call_args
1✔
426

427
    @pytest.mark.parametrize(
1✔
428
        ["embed", "error", "hidden", "disabled_reason"],
429
        [
430
            [
431
                discord.Embed(description="hey its me your worst error"),
432
                commands.DisabledCommand("disabled command, yert"),
433
                True,
434
                None,
435
            ],
436
            [
437
                discord.Embed(description="hey its me your worst error"),
438
                commands.DisabledCommand("disabled command, yert"),
439
                False,
440
                None,
441
            ],
442
            [
443
                discord.Embed(description="hey its me your worst error"),
444
                commands.DisabledCommand("disabled command, yert"),
445
                False,
446
                "Some message that should show up once the mock is right",
447
            ],
448
        ],
449
    )
450
    @pytest.mark.asyncio
1✔
451
    async def test_disabled_command(
1✔
452
        self,
453
        cog: ErrorHandler,
454
        ctx: mocks.MockContext,
455
        command: commands.Command,
456
        embed: discord.Embed,
457
        error: commands.DisabledCommand,
458
        hidden: bool,
459
        disabled_reason: str,
460
    ):
461
        """Test disabled commands have the right error message."""
462

UNCOV
463
        def error_embed(title: str, message: str):
×
UNCOV
464
            if disabled_reason:
×
UNCOV
465
                assert disabled_reason in message
×
UNCOV
466
            return embed
×
467

UNCOV
468
        ctx.command = command
×
UNCOV
469
        ctx.invoked_with = command.name
×
UNCOV
470
        ctx.command.hidden = hidden
×
UNCOV
471
        ctx.command.extras = dict()
×
UNCOV
472
        should_respond = not hidden
×
UNCOV
473
        if disabled_reason:
×
UNCOV
474
            ctx.command.extras["disabled_reason"] = disabled_reason
×
475

UNCOV
476
        mock = unittest.mock.Mock(side_effect=error_embed)
×
477

UNCOV
478
        with unittest.mock.patch.object(cog, "error_embed", mock):
×
UNCOV
479
            await cog.on_command_error(ctx, error)
×
480

UNCOV
481
        assert int(should_respond) == ctx.send.call_count
×
UNCOV
482
        if should_respond:
×
UNCOV
483
            assert unittest.mock.call(embeds=[embed]) == ctx.send.call_args
×
484

485
    @pytest.mark.asyncio
1✔
486
    async def test_default_embed(self, cog, ctx):
1✔
487
        """Test the default embed calls the right methods the correct number of times."""
488
        embed = discord.Embed(description="I need all of the errors!")
1✔
489
        error = unittest.mock.NonCallableMock(spec_set=commands.ConversionError)
1✔
490

491
        with unittest.mock.patch.object(cog, "error_embed") as mock:
1✔
492
            mock.return_value = embed
1✔
493
            await cog.on_command_error(ctx, error)
1✔
494

495
        assert 1 == ctx.send.call_count
1✔
496
        assert unittest.mock.call(embeds=[embed]) == ctx.send.call_args
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc