diff options
| author | 2019-09-22 15:15:27 -0400 | |
|---|---|---|
| committer | 2019-09-22 15:15:27 -0400 | |
| commit | 9e30880df12d085b545ed3d429b2dcea3ea7d544 (patch) | |
| tree | b3409f6be37e20ad8c4f67bcb0fe609647d6f93f /tests/cogs | |
| parent | Make defcon days command turn on defcon, refactor log messaging (diff) | |
| parent | Fix date formatting bug in infraction search (diff) | |
Merge branch 'master' into defcon-fix-django
Diffstat (limited to 'tests/cogs')
| -rw-r--r-- | tests/cogs/test_antispam.py | 30 | ||||
| -rw-r--r-- | tests/cogs/test_information.py | 163 | ||||
| -rw-r--r-- | tests/cogs/test_security.py | 54 | ||||
| -rw-r--r-- | tests/cogs/test_token_remover.py | 133 | 
4 files changed, 380 insertions, 0 deletions
| diff --git a/tests/cogs/test_antispam.py b/tests/cogs/test_antispam.py new file mode 100644 index 000000000..67900b275 --- /dev/null +++ b/tests/cogs/test_antispam.py @@ -0,0 +1,30 @@ +import pytest + +from bot.cogs import antispam + + +def test_default_antispam_config_is_valid(): +    validation_errors = antispam.validate_config() +    assert not validation_errors + + +    ('config', 'expected'), +    ( +        ( +            {'invalid-rule': {}}, +            {'invalid-rule': "`invalid-rule` is not recognized as an antispam rule."} +        ), +        ( +            {'burst': {'interval': 10}}, +            {'burst': "Key `max` is required but not set for rule `burst`"} +        ), +        ( +            {'burst': {'max': 10}}, +            {'burst': "Key `interval` is required but not set for rule `burst`"} +        ) +    ) +) +def test_invalid_antispam_config_returns_validation_errors(config, expected): +    validation_errors = antispam.validate_config(config) +    assert validation_errors == expected diff --git a/tests/cogs/test_information.py b/tests/cogs/test_information.py new file mode 100644 index 000000000..85b2d092e --- /dev/null +++ b/tests/cogs/test_information.py @@ -0,0 +1,163 @@ +import asyncio +import logging +import textwrap +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pytest +from discord import ( +    CategoryChannel, +    Colour, +    TextChannel, +    VoiceChannel, +) + +from bot.cogs import information +from bot.constants import Emojis +from bot.decorators import InChannelCheckFailure +from tests.helpers import AsyncMock + + +def cog(simple_bot): +    return information.Information(simple_bot) + + +def role(name: str, id_: int): +    r = MagicMock() +    r.name = name +    r.id = id_ +    r.mention = f'&{name}' +    return r + + +def member(status: str): +    m = MagicMock() +    m.status = status +    return m + + +def ctx(moderator_role, simple_ctx): +    simple_ctx.author.roles = [moderator_role] +    simple_ctx.guild.created_at = datetime(2001, 1, 1) +    simple_ctx.send = AsyncMock() +    return simple_ctx + + +def test_roles_info_command(cog, ctx): +    everyone_role = MagicMock() +    everyone_role.name = '@everyone'  # should be excluded in the output +    ctx.author.roles.append(everyone_role) +    ctx.guild.roles = ctx.author.roles + +    cog.roles_info.can_run = AsyncMock() +    cog.roles_info.can_run.return_value = True + +    coroutine = cog.roles_info.callback(cog, ctx) + +    assert asyncio.run(coroutine) is None  # no rval +    ctx.send.assert_called_once() +    _, kwargs = ctx.send.call_args +    embed = kwargs.pop('embed') +    assert embed.title == "Role information" +    assert embed.colour == Colour.blurple() +    assert embed.description == f"`{ctx.guild.roles[0].id}` - {ctx.guild.roles[0].mention}\n" +    assert embed.footer.text == "Total roles: 1" + + +# There is no argument passed in here that we can use to test, +# so the return value would change constantly. +@patch('bot.cogs.information.time_since') +def test_server_info_command(time_since_patch, cog, ctx, moderator_role): +    time_since_patch.return_value = '2 days ago' + +    ctx.guild.created_at = datetime(2001, 1, 1) +    ctx.guild.features = ('lemons', 'apples') +    ctx.guild.region = 'The Moon' +    ctx.guild.roles = [moderator_role] +    ctx.guild.channels = [ +        TextChannel( +            state={}, +            guild=ctx.guild, +            data={'id': 42, 'name': 'lemons-offering', 'position': 22, 'type': 'text'} +        ), +        CategoryChannel( +            state={}, +            guild=ctx.guild, +            data={'id': 5125, 'name': 'the-lemon-collection', 'position': 22, 'type': 'category'} +        ), +        VoiceChannel( +            state={}, +            guild=ctx.guild, +            data={'id': 15290, 'name': 'listen-to-lemon', 'position': 22, 'type': 'voice'} +        ) +    ] +    ctx.guild.members = [ +        member('online'), member('online'), +        member('idle'), +        member('dnd'), member('dnd'), member('dnd'), member('dnd'), +        member('offline'), member('offline'), member('offline') +    ] +    ctx.guild.member_count = 1_234 +    ctx.guild.icon_url = 'a-lemon.png' + +    coroutine = cog.server_info.callback(cog, ctx) +    assert asyncio.run(coroutine) is None  # no rval + +    time_since_patch.assert_called_once_with(ctx.guild.created_at, precision='days') +    _, kwargs = ctx.send.call_args +    embed = kwargs.pop('embed') +    assert embed.colour == Colour.blurple() +    assert embed.description == textwrap.dedent(f""" +        **Server information** +        Created: {time_since_patch.return_value} +        Voice region: {ctx.guild.region} +        Features: {', '.join(ctx.guild.features)} + +        **Counts** +        Members: {ctx.guild.member_count:,} +        Roles: {len(ctx.guild.roles)} +        Text: 1 +        Voice: 1 +        Channel categories: 1 + +        **Members** +        {Emojis.status_online} 2 +        {Emojis.status_idle} 1 +        {Emojis.status_dnd} 4 +        {Emojis.status_offline} 3 +        """) +    assert embed.thumbnail.url == 'a-lemon.png' + + +def test_user_info_on_other_users_from_non_moderator(ctx, cog): +    ctx.author = MagicMock() +    ctx.author.__eq__.return_value = False +    ctx.author.roles = [] +    coroutine = cog.user_info.callback(cog, ctx, user='scragly')  # skip checks, pass args + +    assert asyncio.run(coroutine) is None  # no rval +    ctx.send.assert_called_once_with( +        "You may not use this command on users other than yourself." +    ) + + +def test_user_info_in_wrong_channel_from_non_moderator(ctx, cog): +    ctx.author = MagicMock() +    ctx.author.__eq__.return_value = False +    ctx.author.roles = [] + +    coroutine = cog.user_info.callback(cog, ctx) +    message = 'Sorry, but you may only use this command within <#267659945086812160>.' +    with pytest.raises(InChannelCheckFailure, match=message): +        assert asyncio.run(coroutine) is None  # no rval + + +def test_setup(simple_bot, caplog): +    information.setup(simple_bot) +    simple_bot.add_cog.assert_called_once() +    [record] = caplog.records + +    assert record.message == "Cog loaded: Information" +    assert record.levelno == logging.INFO diff --git a/tests/cogs/test_security.py b/tests/cogs/test_security.py new file mode 100644 index 000000000..1efb460fe --- /dev/null +++ b/tests/cogs/test_security.py @@ -0,0 +1,54 @@ +import logging +from unittest.mock import MagicMock + +import pytest +from discord.ext.commands import NoPrivateMessage + +from bot.cogs import security + + +def cog(): +    bot = MagicMock() +    return security.Security(bot) + + +def context(): +    return MagicMock() + + +def test_check_additions(cog): +    cog.bot.check.assert_any_call(cog.check_on_guild) +    cog.bot.check.assert_any_call(cog.check_not_bot) + + +def test_check_not_bot_for_humans(cog, context): +    context.author.bot = False +    assert cog.check_not_bot(context) + + +def test_check_not_bot_for_robots(cog, context): +    context.author.bot = True +    assert not cog.check_not_bot(context) + + +def test_check_on_guild_outside_of_guild(cog, context): +    context.guild = None + +    with pytest.raises(NoPrivateMessage, match="This command cannot be used in private messages."): +        cog.check_on_guild(context) + + +def test_check_on_guild_on_guild(cog, context): +    context.guild = "lemon's lemonade stand" +    assert cog.check_on_guild(context) + + +def test_security_cog_load(caplog): +    bot = MagicMock() +    security.setup(bot) +    bot.add_cog.assert_called_once() +    [record] = caplog.records +    assert record.message == "Cog loaded: Security" +    assert record.levelno == logging.INFO diff --git a/tests/cogs/test_token_remover.py b/tests/cogs/test_token_remover.py new file mode 100644 index 000000000..9d46b3a05 --- /dev/null +++ b/tests/cogs/test_token_remover.py @@ -0,0 +1,133 @@ +import asyncio +from unittest.mock import MagicMock + +import pytest +from discord import Colour + +from bot.cogs.token_remover import ( +    DELETION_MESSAGE_TEMPLATE, +    TokenRemover, +    setup as setup_cog, +) +from bot.constants import Channels, Colours, Event, Icons +from tests.helpers import AsyncMock + + +def token_remover(): +    bot = MagicMock() +    bot.get_cog.return_value = MagicMock() +    bot.get_cog.return_value.send_log_message = AsyncMock() +    return TokenRemover(bot=bot) + + +def message(): +    message = MagicMock() +    message.author.__str__.return_value = 'lemon' +    message.author.bot = False +    message.author.avatar_url_as.return_value = 'picture-lemon.png' +    message.author.id = 42 +    message.author.mention = '@lemon' +    message.channel.send = AsyncMock() +    message.channel.mention = '#lemonade-stand' +    message.content = '' +    message.delete = AsyncMock() +    message.id = 555 +    return message + + +    ('content', 'expected'), +    ( +        ('MTIz', True),  # 123 +        ('YWJj', False),  # abc +    ) +) +def test_is_valid_user_id(content: str, expected: bool): +    assert TokenRemover.is_valid_user_id(content) is expected + + +    ('content', 'expected'), +    ( +        ('DN9r_A', True),  # stolen from dapi, thanks to the author of the 'token' tag! +        ('MTIz', False),  # 123 +    ) +) +def test_is_valid_timestamp(content: str, expected: bool): +    assert TokenRemover.is_valid_timestamp(content) is expected + + +def test_mod_log_property(token_remover): +    token_remover.bot.get_cog.return_value = 'lemon' +    assert token_remover.mod_log == 'lemon' +    token_remover.bot.get_cog.assert_called_once_with('ModLog') + + +def test_ignores_bot_messages(token_remover, message): +    message.author.bot = True +    coroutine = token_remover.on_message(message) +    assert asyncio.run(coroutine) is None + + [email protected]('content', ('', 'lemon wins')) +def test_ignores_messages_without_tokens(token_remover, message, content): +    message.content = content +    coroutine = token_remover.on_message(message) +    assert asyncio.run(coroutine) is None + + [email protected]('content', ('foo.bar.baz', 'x.y.')) +def test_ignores_invalid_tokens(token_remover, message, content): +    message.content = content +    coroutine = token_remover.on_message(message) +    assert asyncio.run(coroutine) is None + + +    'content, censored_token', +    ( +        ('MTIz.DN9R_A.xyz', 'MTIz.DN9R_A.xxx'), +    ) +) +def test_censors_valid_tokens( +    token_remover, message, content, censored_token, caplog +): +    message.content = content +    coroutine = token_remover.on_message(message) +    assert asyncio.run(coroutine) is None  # still no rval + +    # asyncio logs some stuff about its reactor, discard it +    [_, record] = caplog.records +    assert record.message == ( +        "Censored a seemingly valid token sent by lemon (`42`) in #lemonade-stand, " +        f"token was `{censored_token}`" +    ) + +    message.delete.assert_called_once_with() +    message.channel.send.assert_called_once_with( +        DELETION_MESSAGE_TEMPLATE.format(mention='@lemon') +    ) +    token_remover.bot.get_cog.assert_called_with('ModLog') +    message.author.avatar_url_as.assert_called_once_with(static_format='png') + +    mod_log = token_remover.bot.get_cog.return_value +    mod_log.ignore.assert_called_once_with(Event.message_delete, message.id) +    mod_log.send_log_message.assert_called_once_with( +        icon_url=Icons.token_removed, +        colour=Colour(Colours.soft_red), +        title="Token removed!", +        text=record.message, +        thumbnail='picture-lemon.png', +        channel_id=Channels.mod_alerts +    ) + + +def test_setup(caplog): +    bot = MagicMock() +    setup_cog(bot) +    [record] = caplog.records + +    bot.add_cog.assert_called_once() +    assert record.message == "Cog loaded: TokenRemover" | 
