From 0d7e303b17d76719c77906617c23bbb3674ce451 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Thu, 3 Jan 2019 20:01:48 +0100 Subject: Add role and user differ unit tests. --- tests/cogs/__init__.py | 0 tests/cogs/sync/__init__.py | 0 tests/cogs/sync/test_roles.py | 64 +++++++++++++++++++++++++++++++++++++++++++ tests/cogs/sync/test_users.py | 61 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 tests/cogs/__init__.py create mode 100644 tests/cogs/sync/__init__.py create mode 100644 tests/cogs/sync/test_roles.py create mode 100644 tests/cogs/sync/test_users.py (limited to 'tests/cogs') diff --git a/tests/cogs/__init__.py b/tests/cogs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/cogs/sync/__init__.py b/tests/cogs/sync/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/cogs/sync/test_roles.py b/tests/cogs/sync/test_roles.py new file mode 100644 index 000000000..7def815cc --- /dev/null +++ b/tests/cogs/sync/test_roles.py @@ -0,0 +1,64 @@ +from bot.cogs.sync.syncers import get_roles_for_sync, Role + + +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)} + + assert get_roles_for_sync(guild_roles, api_roles) == (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)} + + assert get_roles_for_sync(guild_roles, api_roles) == ( + set(), + guild_roles + ) + + +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) + } + guild_roles = { + Role(id=41, name='new name', colour=35, permissions=0x8), + Role(id=53, name='other role', colour=55, permissions=0) + } + + assert get_roles_for_sync(guild_roles, api_roles) == ( + set(), + {Role(id=41, name='new name', colour=35, permissions=0x8)}, + ) + + +def test_get_roles_returns_new_roles_in_first_tuple_element(): + api_roles = { + Role(id=41, name='name', colour=35, permissions=0x8), + } + guild_roles = { + Role(id=41, name='name', colour=35, permissions=0x8), + Role(id=53, name='other role', colour=55, permissions=0) + } + + assert get_roles_for_sync(guild_roles, api_roles) == ( + {Role(id=53, name='other role', colour=55, permissions=0)}, + set() + ) + + +def test_get_roles_returns_roles_to_update_and_new_roles(): + api_roles = { + Role(id=41, name='old name', colour=35, permissions=0x8), + } + guild_roles = { + Role(id=41, name='new name', colour=40, permissions=0x16), + Role(id=53, name='other role', colour=55, permissions=0) + } + + 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)} + ) diff --git a/tests/cogs/sync/test_users.py b/tests/cogs/sync/test_users.py new file mode 100644 index 000000000..6869f89c8 --- /dev/null +++ b/tests/cogs/sync/test_users.py @@ -0,0 +1,61 @@ +from bot.cogs.sync.syncers import User, get_users_for_sync + +def fake_user(**kwargs): + kwargs.setdefault('id', 43) + kwargs.setdefault('name', 'bob the test man') + kwargs.setdefault('discriminator', 1337) + kwargs.setdefault('avatar_hash', None) + kwargs.setdefault('roles', (666,)) + kwargs.setdefault('in_guild', True) + return User(**kwargs) + + +def test_get_users_for_sync_returns_nothing_for_empty_params(): + assert get_users_for_sync({}, {}) == (set(), set()) + + +def test_get_users_for_sync_returns_nothing_for_equal_users(): + api_users = {43: fake_user()} + guild_users = {43: fake_user()} + + assert get_users_for_sync(guild_users, api_users) == (set(), set()) + + +def test_get_users_for_sync_returns_users_to_update_on_non_id_field_diff(): + api_users = {43: fake_user()} + guild_users = {43: fake_user(name='new fancy name')} + + assert get_users_for_sync(guild_users, api_users) == ( + set(), + {fake_user(name='new fancy name')} + ) + + +def test_get_users_for_sync_returns_users_to_create_with_new_ids_on_guild(): + api_users = {43: fake_user()} + guild_users = {43: fake_user(), 63: fake_user(id=63)} + + assert get_users_for_sync(guild_users, api_users) == ( + {fake_user(id=63)}, + set() + ) + + +def test_get_users_for_sync_updates_in_guild_field_on_user_leave(): + api_users = {43: fake_user(), 63: fake_user(id=63)} + guild_users = {43: fake_user()} + + assert get_users_for_sync(guild_users, api_users) == ( + set(), + {fake_user(id=63, in_guild=False)} + ) + + +def test_get_users_for_sync_updates_and_creates_users_as_needed(): + api_users = {43: fake_user()} + guild_users = {63: fake_user(id=63)} + + assert get_users_for_sync(guild_users, api_users) == ( + {fake_user(id=63)}, + {fake_user(in_guild=False)} + ) -- cgit v1.2.3 From 1e8ac5e9bff2a6190bdde88fb4c171d07fdda1ba Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Thu, 3 Jan 2019 20:21:04 +0100 Subject: Add test case for deduplication of `in_guild` field update. --- tests/cogs/sync/test_users.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'tests/cogs') diff --git a/tests/cogs/sync/test_users.py b/tests/cogs/sync/test_users.py index 6869f89c8..ecf1d3926 100644 --- a/tests/cogs/sync/test_users.py +++ b/tests/cogs/sync/test_users.py @@ -59,3 +59,10 @@ def test_get_users_for_sync_updates_and_creates_users_as_needed(): {fake_user(id=63)}, {fake_user(in_guild=False)} ) + + +def test_get_users_for_sync_does_not_duplicate_update_users(): + api_users = {43: fake_user(in_guild=False)} + guild_users = {} + + assert get_users_for_sync(guild_users, api_users) == (set(), set()) -- cgit v1.2.3 From 1ff9aa88628d7c25484da1b85ef4d1eeb0049b63 Mon Sep 17 00:00:00 2001 From: Scragly <29337040+scragly@users.noreply.github.com> Date: Sat, 9 Mar 2019 17:52:23 +1000 Subject: Fix the linting issues with Bot so builds will work. --- bot/cogs/bigbrother.py | 14 +++++++------- bot/cogs/defcon.py | 2 +- bot/cogs/doc.py | 2 +- bot/cogs/information.py | 2 +- bot/cogs/modlog.py | 5 +---- bot/cogs/reminders.py | 6 ++---- bot/cogs/superstarify/__init__.py | 16 ++++------------ bot/cogs/tags.py | 7 ++----- bot/utils/moderation.py | 2 +- tests/cogs/sync/test_roles.py | 2 +- tests/cogs/sync/test_users.py | 1 + tox.ini | 2 +- 12 files changed, 23 insertions(+), 38 deletions(-) (limited to 'tests/cogs') diff --git a/bot/cogs/bigbrother.py b/bot/cogs/bigbrother.py index a6962efea..df7a0b576 100644 --- a/bot/cogs/bigbrother.py +++ b/bot/cogs/bigbrother.py @@ -4,10 +4,12 @@ import re from collections import defaultdict, deque from typing import List, Union -from discord import Color, Embed, Guild, Member, Message, TextChannel, User +from discord import Color, Embed, Guild, Member, Message, User from discord.ext.commands import Bot, Context, group -from bot.constants import BigBrother as BigBrotherConfig, Channels, Emojis, Guild as GuildConfig, Keys, Roles, URLs +from bot.constants import ( + BigBrother as BigBrotherConfig, Channels, Emojis, Guild as GuildConfig, Roles +) from bot.decorators import with_role from bot.pagination import LinePaginator from bot.utils import messages @@ -73,12 +75,11 @@ class BigBrother: ) self.watched_users.remove(user.id) del self.channel_queues[user.id] - await channel.send( + await self.channel.send( f"{Emojis.bb_message}:hammer: {user} got banned, so " f"`BigBrother` will no longer relay their messages." ) - async def on_message(self, msg: Message): """Queues up messages sent by watched users.""" @@ -100,7 +101,7 @@ class BigBrother: log.trace("Begin consuming messages.") channel_queues = self.channel_queues.copy() self.channel_queues.clear() - for user_id, queues in channel_queues.items(): + for _, queues in channel_queues.items(): for queue in queues.values(): while queue: msg = queue.popleft() @@ -219,13 +220,12 @@ class BigBrother: if user.id in self.watched_users: return await ctx.send(":x: That user is already watched.") - created_infraction = await post_infraction( + await post_infraction( ctx, user, type='watch', reason=reason, hidden=True ) self.watched_users.add(user.id) await ctx.send(f":ok_hand: will now relay messages sent by {user}") - @bigbrother_group.command(name='unwatch', aliases=('uw',)) @with_role(Roles.owner, Roles.admin, Roles.moderator) async def unwatch_command(self, ctx: Context, user: User): diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 8fa80020a..29979de83 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -5,7 +5,7 @@ from discord import Colour, Embed, Member from discord.ext.commands import Bot, Context, group from bot.cogs.modlog import ModLog -from bot.constants import Channels, Emojis, Icons, Keys, Roles, URLs +from bot.constants import Channels, Emojis, Icons, Keys, Roles from bot.decorators import with_role log = logging.getLogger(__name__) diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index 860ec7f62..d427acc3a 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -13,7 +13,7 @@ from markdownify import MarkdownConverter from requests import ConnectionError from sphinx.ext import intersphinx -from bot.constants import Keys, Roles +from bot.constants import Roles from bot.converters import ValidPythonIdentifier, ValidURL from bot.decorators import with_role from bot.pagination import LinePaginator diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 476820feb..92b2444a3 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -4,7 +4,7 @@ import textwrap from discord import CategoryChannel, Colour, Embed, Member, TextChannel, VoiceChannel from discord.ext.commands import Bot, Context, command -from bot.constants import Emojis, Keys, Roles, URLs +from bot.constants import Emojis, Keys, Roles from bot.decorators import with_role from bot.utils.time import time_since diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 911b5da03..66e80e778 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -3,7 +3,6 @@ import logging from datetime import datetime from typing import List, Optional, Union -from aiohttp import ClientResponseError from dateutil.relativedelta import relativedelta from deepdiff import DeepDiff from discord import ( @@ -16,9 +15,7 @@ from discord.abc import GuildChannel from discord.ext.commands import Bot from bot.constants import ( - Channels, Colours, Emojis, - Event, Guild as GuildConstant, Icons, - Keys, Roles, URLs + Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs ) from bot.utils.time import humanize_delta diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py index b86fecd5c..fa1be307c 100644 --- a/bot/cogs/reminders.py +++ b/bot/cogs/reminders.py @@ -5,19 +5,17 @@ import textwrap from datetime import datetime from operator import itemgetter -from aiohttp import ClientResponseError from dateutil.relativedelta import relativedelta from discord import Colour, Embed from discord.ext.commands import Bot, Context, group from bot.constants import ( - Channels, Icons, Keys, NEGATIVE_REPLIES, - POSITIVE_REPLIES, Roles, URLs + Channels, Icons, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles ) from bot.converters import ExpirationDate from bot.pagination import LinePaginator from bot.utils.scheduling import Scheduler -from bot.utils.time import humanize_delta, parse_rfc1123, wait_until +from bot.utils.time import humanize_delta, wait_until log = logging.getLogger(__name__) diff --git a/bot/cogs/superstarify/__init__.py b/bot/cogs/superstarify/__init__.py index e14336b52..efa02cb43 100644 --- a/bot/cogs/superstarify/__init__.py +++ b/bot/cogs/superstarify/__init__.py @@ -8,14 +8,10 @@ from discord.ext.commands import Bot, Context, command from bot.cogs.moderation import Moderation from bot.cogs.modlog import ModLog -from bot.constants import ( - Icons, Keys, - NEGATIVE_REPLIES, POSITIVE_REPLIES, - Roles, URLs -) +from bot.cogs.superstarify.stars import get_nick +from bot.constants import Icons, POSITIVE_REPLIES, Roles from bot.converters import ExpirationDate from bot.decorators import with_role -from bot.cogs.superstarify.stars import get_nick from bot.utils.moderation import post_infraction log = logging.getLogger(__name__) @@ -117,8 +113,7 @@ class Superstarify: forced_nick = get_nick(infraction['id'], member.id) await member.edit(nick=forced_nick) end_timestamp_human = ( - datetime.fromisoformat(response['expires_at'][:-1]) - .strftime('%c') + datetime.fromisoformat(infraction['expires_at'][:-1]).strftime('%c') ) try: @@ -157,8 +152,7 @@ class Superstarify: @command(name='superstarify', aliases=('force_nick', 'star')) @with_role(Roles.admin, Roles.owner, Roles.moderator) async def superstarify( - self, ctx: Context, member: Member, - expiration: ExpirationDate, reason: str = None + self, ctx: Context, member: Member, expiration: ExpirationDate, reason: str = None ): """ This command will force a random superstar name (like Taylor Swift) to be the user's @@ -180,7 +174,6 @@ class Superstarify: f"See infraction **#{active_superstarifies[0]['id']}**." ) - infraction = await post_infraction( ctx, member, type='superstar', reason=reason or ('old nick: ' + member.display_name), @@ -256,7 +249,6 @@ class Superstarify: ":x: There is no active superstarify infraction for this user." ) - [infraction] = active_superstarifies await self.bot.api_client.patch( 'bot/infractions/' + str(infraction['id']), diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 5a0198db8..bb4d6ba71 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -2,13 +2,10 @@ import logging import time from discord import Colour, Embed -from discord.ext.commands import ( - BadArgument, Bot, - Context, group -) +from discord.ext.commands import Bot, Context, group from bot.constants import Channels, Cooldowns, Keys, Roles -from bot.converters import TagContentConverter, TagNameConverter, ValidURL +from bot.converters import TagContentConverter, TagNameConverter from bot.decorators import with_role from bot.pagination import LinePaginator diff --git a/bot/utils/moderation.py b/bot/utils/moderation.py index 2611ee993..fcdf3c4d5 100644 --- a/bot/utils/moderation.py +++ b/bot/utils/moderation.py @@ -6,7 +6,7 @@ from aiohttp import ClientError from discord import Member, Object, User from discord.ext.commands import Context -from bot.constants import Keys, URLs +from bot.constants import Keys log = logging.getLogger(__name__) diff --git a/tests/cogs/sync/test_roles.py b/tests/cogs/sync/test_roles.py index 7def815cc..18682f39f 100644 --- a/tests/cogs/sync/test_roles.py +++ b/tests/cogs/sync/test_roles.py @@ -1,4 +1,4 @@ -from bot.cogs.sync.syncers import get_roles_for_sync, Role +from bot.cogs.sync.syncers import Role, get_roles_for_sync def test_get_roles_for_sync_empty_return_for_equal_roles(): diff --git a/tests/cogs/sync/test_users.py b/tests/cogs/sync/test_users.py index ecf1d3926..a863ae35b 100644 --- a/tests/cogs/sync/test_users.py +++ b/tests/cogs/sync/test_users.py @@ -1,5 +1,6 @@ from bot.cogs.sync.syncers import User, get_users_for_sync + def fake_user(**kwargs): kwargs.setdefault('id', 43) kwargs.setdefault('name', 'bob the test man') diff --git a/tox.ini b/tox.ini index c6fa513f4..c84827570 100644 --- a/tox.ini +++ b/tox.ini @@ -2,5 +2,5 @@ max-line-length=120 application_import_names=bot exclude=.cache,.venv -ignore=B311,W503,E226,S311 +ignore=B311,W503,E226,S311,T000 import-order-style=pycharm -- cgit v1.2.3 From bb6e9a050d14c375243fecb15f8fdffe7ad67787 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 15 Aug 2019 20:52:29 +0200 Subject: Updating role sync tests for position and adding tests for deletion detection --- tests/cogs/sync/test_roles.py | 81 ++++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 21 deletions(-) (limited to 'tests/cogs') 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)}, ) -- cgit v1.2.3 From d8f3d10a5298095d5b9dffe1f063ad69c8498883 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 12:07:07 +0200 Subject: Validate bot.cogs.antispam configuration on CI. --- bot/cogs/antispam.py | 6 ++++-- tests/cogs/test_antispam.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 tests/cogs/test_antispam.py (limited to 'tests/cogs') diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 69367b40b..482965b9b 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -1,5 +1,6 @@ import asyncio import logging +from collections.abc import Mapping from dataclasses import dataclass, field from datetime import datetime, timedelta from operator import itemgetter @@ -245,16 +246,17 @@ class AntiSpam: await deletion_context.upload_messages(self.bot.user.id, self.mod_log) -def validate_config() -> bool: +def validate_config(rules: Mapping = AntiSpamConfig.rules) -> dict: """Validates the antispam configs.""" validation_errors = {} - for name, config in AntiSpamConfig.rules.items(): + for name, config in rules.items(): if name not in RULE_FUNCTION_MAPPING: log.error( f"Unrecognized antispam rule `{name}`. " f"Valid rules are: {', '.join(RULE_FUNCTION_MAPPING)}" ) validation_errors[name] = f"`{name}` is not recognized as an antispam rule." + continue for required_key in ('interval', 'max'): if required_key not in config: log.error( 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 + + +@pytest.mark.parametrize( + ('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 -- cgit v1.2.3 From 1dd55ae6055bbe320588a7f64de1a2bdd5ebaca3 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 18:07:58 +0200 Subject: Add tests for `bot.cogs.token_remover`. --- tests/cogs/test_token_remover.py | 133 +++++++++++++++++++++++++++++++++++++++ tests/helpers.py | 10 +++ tox.ini | 2 +- 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 tests/cogs/test_token_remover.py create mode 100644 tests/helpers.py (limited to 'tests/cogs') 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 + + +@pytest.fixture() +def token_remover(): + bot = MagicMock() + bot.get_cog.return_value = MagicMock() + bot.get_cog.return_value.send_log_message = AsyncMock() + return TokenRemover(bot=bot) + + +@pytest.fixture() +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 + + +@pytest.mark.parametrize( + ('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 + + +@pytest.mark.parametrize( + ('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 + + +@pytest.mark.parametrize('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 + + +@pytest.mark.parametrize('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 + + +@pytest.mark.parametrize( + '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" diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 000000000..57c6fcc1a --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,10 @@ +from unittest.mock import MagicMock + + +__all__ = ('AsyncMock',) + + +# TODO: Remove me on 3.8 +class AsyncMock(MagicMock): + async def __call__(self, *args, **kwargs): + return super(AsyncMock, self).__call__(*args, **kwargs) diff --git a/tox.ini b/tox.ini index c84827570..21097cd97 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [flake8] max-line-length=120 -application_import_names=bot +application_import_names=bot,tests exclude=.cache,.venv ignore=B311,W503,E226,S311,T000 import-order-style=pycharm -- cgit v1.2.3 From 43cc15121482de120dcc1158153a24d5cadf27fa Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 21:11:09 +0200 Subject: Add tests for `bot.cogs.security`. --- tests/cogs/test_security.py | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/cogs/test_security.py (limited to 'tests/cogs') 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 + + +@pytest.fixture() +def cog(): + bot = MagicMock() + return security.Security(bot) + + +@pytest.fixture() +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 -- cgit v1.2.3 From 58f7e94746de7394cef7d7b5a193d43740fbe49c Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 22:10:09 +0200 Subject: Add basic tests for `bot.cogs.information`. --- tests/cogs/test_information.py | 163 +++++++++++++++++++++++++++++++++++++++++ tests/conftest.py | 32 ++++++++ 2 files changed, 195 insertions(+) create mode 100644 tests/cogs/test_information.py create mode 100644 tests/conftest.py (limited to 'tests/cogs') 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 + + +@pytest.fixture() +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 + + +@pytest.fixture() +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/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..d3de4484d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,32 @@ +from unittest.mock import MagicMock + +import pytest + +from bot.constants import Roles +from tests.helpers import AsyncMock + + +@pytest.fixture() +def moderator_role(): + mock = MagicMock() + mock.id = Roles.moderator + mock.name = 'Moderator' + mock.mention = f'&{mock.name}' + return mock + + +@pytest.fixture() +def simple_bot(): + mock = MagicMock() + mock._before_invoke = AsyncMock() + mock._after_invoke = AsyncMock() + mock.can_run = AsyncMock() + mock.can_run.return_value = True + return mock + + +@pytest.fixture() +def simple_ctx(simple_bot): + mock = MagicMock() + mock.bot = simple_bot + return mock -- cgit v1.2.3