diff options
Diffstat (limited to '')
| -rw-r--r-- | tests/cogs/sync/test_roles.py | 81 | ||||
| -rw-r--r-- | tests/cogs/test_antispam.py | 30 | ||||
| -rw-r--r-- | tests/cogs/test_information.py | 211 | ||||
| -rw-r--r-- | tests/cogs/test_security.py | 54 | ||||
| -rw-r--r-- | tests/cogs/test_token_remover.py | 133 | 
5 files changed, 488 insertions, 21 deletions
diff --git a/tests/cogs/sync/test_roles.py b/tests/cogs/sync/test_roles.py index 18682f39f..c561ba447 100644 --- a/tests/cogs/sync/test_roles.py +++ b/tests/cogs/sync/test_roles.py @@ -2,63 +2,102 @@ from bot.cogs.sync.syncers import Role, get_roles_for_sync  def test_get_roles_for_sync_empty_return_for_equal_roles(): -    api_roles = {Role(id=41, name='name', colour=33, permissions=0x8)} -    guild_roles = {Role(id=41, name='name', colour=33, permissions=0x8)} +    api_roles = {Role(id=41, name='name', colour=33, permissions=0x8, position=1)} +    guild_roles = {Role(id=41, name='name', colour=33, permissions=0x8, position=1)} -    assert get_roles_for_sync(guild_roles, api_roles) == (set(), set()) +    assert get_roles_for_sync(guild_roles, api_roles) == (set(), set(), set())  def test_get_roles_for_sync_returns_roles_to_update_with_non_id_diff(): -    api_roles = {Role(id=41, name='old name', colour=35, permissions=0x8)} -    guild_roles = {Role(id=41, name='new name', colour=33, permissions=0x8)} +    api_roles = {Role(id=41, name='old name', colour=35, permissions=0x8, position=1)} +    guild_roles = {Role(id=41, name='new name', colour=33, permissions=0x8, position=2)}      assert get_roles_for_sync(guild_roles, api_roles) == (          set(), -        guild_roles +        guild_roles, +        set(),      )  def test_get_roles_only_returns_roles_that_require_update():      api_roles = { -        Role(id=41, name='old name', colour=33, permissions=0x8), -        Role(id=53, name='other role', colour=55, permissions=0) +        Role(id=41, name='old name', colour=33, permissions=0x8, position=1), +        Role(id=53, name='other role', colour=55, permissions=0, position=3)      }      guild_roles = { -        Role(id=41, name='new name', colour=35, permissions=0x8), -        Role(id=53, name='other role', colour=55, permissions=0) +        Role(id=41, name='new name', colour=35, permissions=0x8, position=2), +        Role(id=53, name='other role', colour=55, permissions=0, position=3)      }      assert get_roles_for_sync(guild_roles, api_roles) == (          set(), -        {Role(id=41, name='new name', colour=35, permissions=0x8)}, +        {Role(id=41, name='new name', colour=35, permissions=0x8, position=2)}, +        set(),      )  def test_get_roles_returns_new_roles_in_first_tuple_element():      api_roles = { -        Role(id=41, name='name', colour=35, permissions=0x8), +        Role(id=41, name='name', colour=35, permissions=0x8, position=1),      }      guild_roles = { -        Role(id=41, name='name', colour=35, permissions=0x8), -        Role(id=53, name='other role', colour=55, permissions=0) +        Role(id=41, name='name', colour=35, permissions=0x8, position=1), +        Role(id=53, name='other role', colour=55, permissions=0, position=2)      }      assert get_roles_for_sync(guild_roles, api_roles) == ( -        {Role(id=53, name='other role', colour=55, permissions=0)}, -        set() +        {Role(id=53, name='other role', colour=55, permissions=0, position=2)}, +        set(), +        set(),      )  def test_get_roles_returns_roles_to_update_and_new_roles():      api_roles = { -        Role(id=41, name='old name', colour=35, permissions=0x8), +        Role(id=41, name='old name', colour=35, permissions=0x8, position=1), +    } +    guild_roles = { +        Role(id=41, name='new name', colour=40, permissions=0x16, position=2), +        Role(id=53, name='other role', colour=55, permissions=0, position=3) +    } + +    assert get_roles_for_sync(guild_roles, api_roles) == ( +        {Role(id=53, name='other role', colour=55, permissions=0, position=3)}, +        {Role(id=41, name='new name', colour=40, permissions=0x16, position=2)}, +        set(), +    ) + + +def test_get_roles_returns_roles_to_delete(): +    api_roles = { +        Role(id=41, name='name', colour=35, permissions=0x8, position=1), +        Role(id=61, name='to delete', colour=99, permissions=0x9, position=2), +    } +    guild_roles = { +        Role(id=41, name='name', colour=35, permissions=0x8, position=1), +    } + +    assert get_roles_for_sync(guild_roles, api_roles) == ( +        set(), +        set(), +        {Role(id=61, name='to delete', colour=99, permissions=0x9, position=2)}, +    ) + + +def test_get_roles_returns_roles_to_delete_update_and_new_roles(): +    api_roles = { +        Role(id=41, name='not changed', colour=35, permissions=0x8, position=1), +        Role(id=61, name='to delete', colour=99, permissions=0x9, position=2), +        Role(id=71, name='to update', colour=99, permissions=0x9, position=3),      }      guild_roles = { -        Role(id=41, name='new name', colour=40, permissions=0x16), -        Role(id=53, name='other role', colour=55, permissions=0) +        Role(id=41, name='not changed', colour=35, permissions=0x8, position=1), +        Role(id=81, name='to create', colour=99, permissions=0x9, position=4), +        Role(id=71, name='updated', colour=101, permissions=0x5, position=3),      }      assert get_roles_for_sync(guild_roles, api_roles) == ( -        {Role(id=53, name='other role', colour=55, permissions=0)}, -        {Role(id=41, name='new name', colour=40, permissions=0x16)} +        {Role(id=81, name='to create', colour=99, permissions=0x9, position=4)}, +        {Role(id=71, name='updated', colour=101, permissions=0x5, position=3)}, +        {Role(id=61, name='to delete', colour=99, permissions=0x9, position=2)},      ) 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..184bd2595 --- /dev/null +++ b/tests/cogs/test_information.py @@ -0,0 +1,211 @@ +import asyncio +import logging +import textwrap +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pytest +from discord import ( +    CategoryChannel, +    Colour, +    Permissions, +    Role, +    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" + + +def test_role_info_command(cog, ctx): +    dummy_role = MagicMock(spec=Role) +    dummy_role.name = "Dummy" +    dummy_role.colour = Colour.blurple() +    dummy_role.id = 112233445566778899 +    dummy_role.position = 10 +    dummy_role.permissions = Permissions(0) +    dummy_role.members = [ctx.author] + +    admin_role = MagicMock(spec=Role) +    admin_role.name = "Admin" +    admin_role.colour = Colour.red() +    admin_role.id = 998877665544332211 +    admin_role.position = 3 +    admin_role.permissions = Permissions(0) +    admin_role.members = [ctx.author] + +    ctx.guild.roles = [dummy_role, admin_role] + +    cog.role_info.can_run = AsyncMock() +    cog.role_info.can_run.return_value = True + +    coroutine = cog.role_info.callback(cog, ctx, dummy_role, admin_role) + +    assert asyncio.run(coroutine) is None + +    assert ctx.send.call_count == 2 + +    (_, dummy_kwargs), (_, admin_kwargs) = ctx.send.call_args_list + +    dummy_embed = dummy_kwargs["embed"] +    admin_embed = admin_kwargs["embed"] + +    assert dummy_embed.title == "Dummy info" +    assert dummy_embed.colour == Colour.blurple() + +    assert dummy_embed.fields[0].value == str(dummy_role.id) +    assert dummy_embed.fields[1].value == f"#{dummy_role.colour.value:0>6x}" +    assert dummy_embed.fields[2].value == "0.63 0.48 218" +    assert dummy_embed.fields[3].value == "1" +    assert dummy_embed.fields[4].value == "10" +    assert dummy_embed.fields[5].value == "0" + +    assert admin_embed.title == "Admin info" +    assert admin_embed.colour == Colour.red() + +# 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"  |