aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/cogs/test_antispam.py30
-rw-r--r--tests/cogs/test_information.py163
-rw-r--r--tests/cogs/test_security.py54
-rw-r--r--tests/cogs/test_token_remover.py133
-rw-r--r--tests/conftest.py32
-rw-r--r--tests/helpers.py29
-rw-r--r--tests/test_api.py106
-rw-r--r--tests/test_constants.py23
-rw-r--r--tests/test_converters.py93
-rw-r--r--tests/test_pagination.py29
-rw-r--r--tests/test_resources.py18
-rw-r--r--tests/utils/__init__.py0
-rw-r--r--tests/utils/test_checks.py66
13 files changed, 776 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"
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
+
+
+def moderator_role():
+ mock = MagicMock()
+ mock.id = Roles.moderator
+ mock.name = 'Moderator'
+ mock.mention = f'&{mock.name}'
+ return mock
+
+
+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
+
+
+def simple_ctx(simple_bot):
+ mock = MagicMock()
+ mock.bot = simple_bot
+ return mock
diff --git a/tests/helpers.py b/tests/helpers.py
new file mode 100644
index 000000000..2908294f7
--- /dev/null
+++ b/tests/helpers.py
@@ -0,0 +1,29 @@
+import asyncio
+import functools
+from unittest.mock import MagicMock
+
+
+__all__ = ('AsyncMock', 'async_test')
+
+
+# TODO: Remove me on 3.8
+class AsyncMock(MagicMock):
+ async def __call__(self, *args, **kwargs):
+ return super(AsyncMock, self).__call__(*args, **kwargs)
+
+
+def async_test(wrapped):
+ """
+ Run a test case via asyncio.
+
+ Example:
+
+ >>> @async_test
+ ... async def lemon_wins():
+ ... assert True
+ """
+
+ @functools.wraps(wrapped)
+ def wrapper(*args, **kwargs):
+ return asyncio.run(wrapped(*args, **kwargs))
+ return wrapper
diff --git a/tests/test_api.py b/tests/test_api.py
new file mode 100644
index 000000000..ce69ef187
--- /dev/null
+++ b/tests/test_api.py
@@ -0,0 +1,106 @@
+import logging
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from bot import api
+from tests.helpers import async_test
+
+
+def test_loop_is_not_running_by_default():
+ assert not api.loop_is_running()
+
+
+@async_test
+async def test_loop_is_running_in_async_test():
+ assert api.loop_is_running()
+
+
+def error_api_response():
+ response = MagicMock()
+ response.status = 999
+ return response
+
+
+def api_log_handler():
+ return api.APILoggingHandler(None)
+
+
+def debug_log_record():
+ return logging.LogRecord(
+ name='my.logger', level=logging.DEBUG,
+ pathname='my/logger.py', lineno=666,
+ msg="Lemon wins", args=(),
+ exc_info=None
+ )
+
+
+def test_response_code_error_default_initialization(error_api_response):
+ error = api.ResponseCodeError(response=error_api_response)
+ assert error.status is error_api_response.status
+ assert not error.response_json
+ assert not error.response_text
+ assert error.response is error_api_response
+
+
+def test_response_code_error_default_representation(error_api_response):
+ error = api.ResponseCodeError(response=error_api_response)
+ assert str(error) == f"Status: {error_api_response.status} Response: "
+
+
+def test_response_code_error_representation_with_nonempty_response_json(error_api_response):
+ error = api.ResponseCodeError(
+ response=error_api_response,
+ response_json={'hello': 'world'}
+ )
+ assert str(error) == f"Status: {error_api_response.status} Response: {{'hello': 'world'}}"
+
+
+def test_response_code_error_representation_with_nonempty_response_text(error_api_response):
+ error = api.ResponseCodeError(
+ response=error_api_response,
+ response_text='Lemon will eat your soul'
+ )
+ assert str(error) == f"Status: {error_api_response.status} Response: Lemon will eat your soul"
+
+
+@patch('bot.api.APILoggingHandler.ship_off')
+def test_emit_appends_to_queue_with_stopped_event_loop(
+ ship_off_patch, api_log_handler, debug_log_record
+):
+ # This is a coroutine so returns something we should await,
+ # but asyncio complains about that. To ease testing, we patch
+ # `ship_off` to just return a regular value instead.
+ ship_off_patch.return_value = 42
+ api_log_handler.emit(debug_log_record)
+
+ assert api_log_handler.queue == [42]
+
+
+def test_emit_ignores_less_than_debug(debug_log_record, api_log_handler):
+ debug_log_record.levelno = logging.DEBUG - 5
+ api_log_handler.emit(debug_log_record)
+ assert not api_log_handler.queue
+
+
+def test_schedule_queued_tasks_for_empty_queue(api_log_handler, caplog):
+ api_log_handler.schedule_queued_tasks()
+ # Logs when tasks are scheduled
+ assert not caplog.records
+
+
+@patch('asyncio.create_task')
+def test_schedule_queued_tasks_for_nonempty_queue(create_task_patch, api_log_handler, caplog):
+ api_log_handler.queue = [555]
+ api_log_handler.schedule_queued_tasks()
+ assert not api_log_handler.queue
+ create_task_patch.assert_called_once_with(555)
+
+ [record] = caplog.records
+ assert record.message == "Scheduled 1 pending logging tasks."
+ assert record.levelno == logging.DEBUG
+ assert record.name == 'bot.api'
+ assert record.__dict__['via_handler']
diff --git a/tests/test_constants.py b/tests/test_constants.py
new file mode 100644
index 000000000..e4a29d994
--- /dev/null
+++ b/tests/test_constants.py
@@ -0,0 +1,23 @@
+import inspect
+
+import pytest
+
+from bot import constants
+
+
+ 'section',
+ (
+ cls
+ for (name, cls) in inspect.getmembers(constants)
+ if hasattr(cls, 'section') and isinstance(cls, type)
+ )
+)
+def test_section_configuration_matches_typespec(section):
+ for (name, annotation) in section.__annotations__.items():
+ value = getattr(section, name)
+
+ if getattr(annotation, '_name', None) in ('Dict', 'List'):
+ pytest.skip("Cannot validate containers yet")
+
+ assert isinstance(value, annotation)
diff --git a/tests/test_converters.py b/tests/test_converters.py
new file mode 100644
index 000000000..3cf774c80
--- /dev/null
+++ b/tests/test_converters.py
@@ -0,0 +1,93 @@
+import asyncio
+from datetime import datetime
+from unittest.mock import MagicMock
+
+import pytest
+from discord.ext.commands import BadArgument
+
+from bot.converters import (
+ ExpirationDate,
+ TagContentConverter,
+ TagNameConverter,
+ ValidPythonIdentifier,
+)
+
+
+ ('value', 'expected'),
+ (
+ # sorry aliens
+ ('2199-01-01T00:00:00', datetime(2199, 1, 1)),
+ )
+)
+def test_expiration_date_converter_for_valid(value: str, expected: datetime):
+ converter = ExpirationDate()
+ assert asyncio.run(converter.convert(None, value)) == expected
+
+
+ ('value', 'expected'),
+ (
+ ('hello', 'hello'),
+ (' h ello ', 'h ello')
+ )
+)
+def test_tag_content_converter_for_valid(value: str, expected: str):
+ assert asyncio.run(TagContentConverter.convert(None, value)) == expected
+
+
+ ('value', 'expected'),
+ (
+ ('', "Tag contents should not be empty, or filled with whitespace."),
+ (' ', "Tag contents should not be empty, or filled with whitespace.")
+ )
+)
+def test_tag_content_converter_for_invalid(value: str, expected: str):
+ context = MagicMock()
+ context.author = 'bob'
+
+ with pytest.raises(BadArgument, match=expected):
+ asyncio.run(TagContentConverter.convert(context, value))
+
+
+ ('value', 'expected'),
+ (
+ ('tracebacks', 'tracebacks'),
+ ('Tracebacks', 'tracebacks'),
+ (' Tracebacks ', 'tracebacks'),
+ )
+)
+def test_tag_name_converter_for_valid(value: str, expected: str):
+ assert asyncio.run(TagNameConverter.convert(None, value)) == expected
+
+
+ ('value', 'expected'),
+ (
+ ('👋', "Don't be ridiculous, you can't use that character!"),
+ ('', "Tag names should not be empty, or filled with whitespace."),
+ (' ', "Tag names should not be empty, or filled with whitespace."),
+ ('42', "Tag names can't be numbers."),
+ # Escape question mark as this is evaluated as regular expression.
+ ('x' * 128, r"Are you insane\? That's way too long!"),
+ )
+)
+def test_tag_name_converter_for_invalid(value: str, expected: str):
+ context = MagicMock()
+ context.author = 'bob'
+
+ with pytest.raises(BadArgument, match=expected):
+ asyncio.run(TagNameConverter.convert(context, value))
+
+
[email protected]('value', ('foo', 'lemon'))
+def test_valid_python_identifier_for_valid(value: str):
+ assert asyncio.run(ValidPythonIdentifier.convert(None, value)) == value
+
+
[email protected]('value', ('nested.stuff', '#####'))
+def test_valid_python_identifier_for_invalid(value: str):
+ with pytest.raises(BadArgument, match=f'`{value}` is not a valid Python identifier'):
+ asyncio.run(ValidPythonIdentifier.convert(None, value))
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
new file mode 100644
index 000000000..11d6541ae
--- /dev/null
+++ b/tests/test_pagination.py
@@ -0,0 +1,29 @@
+from unittest import TestCase
+
+import pytest
+
+from bot import pagination
+
+
+class LinePaginatorTests(TestCase):
+ def setUp(self):
+ self.paginator = pagination.LinePaginator(prefix='', suffix='', max_size=30)
+
+ def test_add_line_raises_on_too_long_lines(self):
+ message = f"Line exceeds maximum page size {self.paginator.max_size - 2}"
+ with pytest.raises(RuntimeError, match=message):
+ self.paginator.add_line('x' * self.paginator.max_size)
+
+ def test_add_line_works_on_small_lines(self):
+ self.paginator.add_line('x' * (self.paginator.max_size - 3))
+
+
+class ImagePaginatorTests(TestCase):
+ def setUp(self):
+ self.paginator = pagination.ImagePaginator()
+
+ def test_add_image_appends_image(self):
+ image = 'lemon'
+ self.paginator.add_image(image)
+
+ assert self.paginator.images == [image]
diff --git a/tests/test_resources.py b/tests/test_resources.py
new file mode 100644
index 000000000..2b17aea64
--- /dev/null
+++ b/tests/test_resources.py
@@ -0,0 +1,18 @@
+import json
+import mimetypes
+from pathlib import Path
+from urllib.parse import urlparse
+
+
+def test_stars_valid():
+ """Validates that `bot/resources/stars.json` contains valid images."""
+
+ path = Path('bot', 'resources', 'stars.json')
+ content = path.read_text()
+ data = json.loads(content)
+
+ for url in data.values():
+ assert urlparse(url).scheme == 'https'
+
+ mimetype, _ = mimetypes.guess_type(url)
+ assert mimetype in ('image/jpeg', 'image/png')
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/utils/__init__.py
diff --git a/tests/utils/test_checks.py b/tests/utils/test_checks.py
new file mode 100644
index 000000000..7121acebd
--- /dev/null
+++ b/tests/utils/test_checks.py
@@ -0,0 +1,66 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from bot.utils import checks
+
+
+def context():
+ return MagicMock()
+
+
+def test_with_role_check_without_guild(context):
+ context.guild = None
+
+ assert not checks.with_role_check(context)
+
+
+def test_with_role_check_with_guild_without_required_role(context):
+ context.guild = True
+ context.author.roles = []
+
+ assert not checks.with_role_check(context)
+
+
+def test_with_role_check_with_guild_with_required_role(context):
+ context.guild = True
+ role = MagicMock()
+ role.id = 42
+ context.author.roles = (role,)
+
+ assert checks.with_role_check(context, role.id)
+
+
+def test_without_role_check_without_guild(context):
+ context.guild = None
+
+ assert not checks.without_role_check(context)
+
+
+def test_without_role_check_with_unwanted_role(context):
+ context.guild = True
+ role = MagicMock()
+ role.id = 42
+ context.author.roles = (role,)
+
+ assert not checks.without_role_check(context, role.id)
+
+
+def test_without_role_check_without_unwanted_role(context):
+ context.guild = True
+ role = MagicMock()
+ role.id = 42
+ context.author.roles = (role,)
+
+ assert checks.without_role_check(context, role.id + 10)
+
+
+def test_in_channel_check_for_correct_channel(context):
+ context.channel.id = 42
+ assert checks.in_channel_check(context, context.channel.id)
+
+
+def test_in_channel_check_for_incorrect_channel(context):
+ context.channel.id = 42
+ assert not checks.in_channel_check(context, context.channel.id + 10)