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. --- tests/cogs/test_antispam.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/cogs/test_antispam.py (limited to 'tests') 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 82e8ca20bb1f61162d1b55c6e354c68ea4cdfcf1 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 12:57:54 +0200 Subject: Add tests for `bot.utils.checks`. --- tests/utils/__init__.py | 0 tests/utils/test_checks.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_checks.py (limited to 'tests') diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/utils/test_checks.py b/tests/utils/test_checks.py new file mode 100644 index 000000000..915d074b3 --- /dev/null +++ b/tests/utils/test_checks.py @@ -0,0 +1,67 @@ +from unittest.mock import MagicMock + +from bot.utils import checks + + +def test_with_role_check_without_guild(): + context = MagicMock() + context.guild = None + + assert not checks.with_role_check(context) + + +def test_with_role_check_with_guild_without_required_role(): + context = MagicMock() + context.guild = True + context.author.roles = [] + + assert not checks.with_role_check(context) + + +def test_with_role_check_with_guild_with_required_role(): + context = MagicMock() + 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 = MagicMock() + context.guild = None + + assert not checks.without_role_check(context) + + +def test_without_role_check_with_unwanted_role(): + context = MagicMock() + 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 = MagicMock() + 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 = MagicMock() + context.channel.id = 42 + assert checks.in_channel_check(context, context.channel.id) + + +def test_in_channel_check_for_incorrect_channel(): + context = MagicMock() + context.channel.id = 42 + assert not checks.in_channel_check(context, context.channel.id + 10) -- cgit v1.2.3 From 3da45c2a8ac967c9c0ed1525e04686914eb50e7d Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 12:43:11 +0200 Subject: Add tests for `bot.converters`. --- tests/test_converters.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/test_converters.py (limited to 'tests') 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, +) + + +@pytest.mark.parametrize( + ('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 + + +@pytest.mark.parametrize( + ('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 + + +@pytest.mark.parametrize( + ('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)) + + +@pytest.mark.parametrize( + ('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 + + +@pytest.mark.parametrize( + ('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)) + + +@pytest.mark.parametrize('value', ('foo', 'lemon')) +def test_valid_python_identifier_for_valid(value: str): + assert asyncio.run(ValidPythonIdentifier.convert(None, value)) == value + + +@pytest.mark.parametrize('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)) -- cgit v1.2.3 From ab21ed98d7373b80c52f007c8becb93a5edcd03a Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 15:10:34 +0200 Subject: Use `@pytest.fixture` for creating contexts. --- tests/utils/test_checks.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) (limited to 'tests') diff --git a/tests/utils/test_checks.py b/tests/utils/test_checks.py index 915d074b3..7121acebd 100644 --- a/tests/utils/test_checks.py +++ b/tests/utils/test_checks.py @@ -1,25 +1,29 @@ from unittest.mock import MagicMock +import pytest + from bot.utils import checks -def test_with_role_check_without_guild(): - context = MagicMock() +@pytest.fixture() +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 = MagicMock() +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 = MagicMock() +def test_with_role_check_with_guild_with_required_role(context): context.guild = True role = MagicMock() role.id = 42 @@ -28,15 +32,13 @@ def test_with_role_check_with_guild_with_required_role(): assert checks.with_role_check(context, role.id) -def test_without_role_check_without_guild(): - context = MagicMock() +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 = MagicMock() +def test_without_role_check_with_unwanted_role(context): context.guild = True role = MagicMock() role.id = 42 @@ -45,8 +47,7 @@ def test_without_role_check_with_unwanted_role(): assert not checks.without_role_check(context, role.id) -def test_without_role_check_without_unwanted_role(): - context = MagicMock() +def test_without_role_check_without_unwanted_role(context): context.guild = True role = MagicMock() role.id = 42 @@ -55,13 +56,11 @@ def test_without_role_check_without_unwanted_role(): assert checks.without_role_check(context, role.id + 10) -def test_in_channel_check_for_correct_channel(): - context = MagicMock() +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 = MagicMock() +def test_in_channel_check_for_incorrect_channel(context): context.channel.id = 42 assert not checks.in_channel_check(context, context.channel.id + 10) -- cgit v1.2.3 From 1c8b07bc2262f08af26aec00633de73dac5a4ddb Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 15:19:52 +0200 Subject: Add basic tests for `bot.pagination`. --- tests/test_pagination.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/test_pagination.py (limited to 'tests') 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] -- 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') 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 4d2e3b1afcb2ad15ff3e091929f42907effa4496 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 17:26:41 +0200 Subject: Validate `bot/resources/stars.json` in tests. --- tests/test_resources.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_resources.py (limited to 'tests') 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') -- 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') 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 0d27d5d0edf17fec789b752700e9e4a753f45df0 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 16:48:10 +0200 Subject: Validate configuration against typehints. --- azure-pipelines.yml | 2 +- bot/constants.py | 7 +------ config-default.yml | 6 ------ tests/test_constants.py | 23 +++++++++++++++++++++++ 4 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 tests/test_constants.py (limited to 'tests') diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 242513ab4..4dcad685c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -38,7 +38,7 @@ jobs: - script: python -m flake8 displayName: 'Run linter' - - script: BOT_TOKEN=foobar python -m pytest --junitxml=junit.xml --cov=bot --cov-branch --cov-report=term --cov-report=xml tests + - script: BOT_API_KEY=foo BOT_TOKEN=bar WOLFRAM_API_KEY=baz python -m pytest --junitxml=junit.xml --cov=bot --cov-branch --cov-report=term --cov-report=xml tests displayName: Run tests - task: PublishCodeCoverageResults@1 diff --git a/bot/constants.py b/bot/constants.py index d5b73bd1d..e1c47889c 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -347,9 +347,9 @@ class Channels(metaclass=YAMLGetter): message_log: int mod_alerts: int modlog: int + off_topic_0: int off_topic_1: int off_topic_2: int - off_topic_3: int python: int reddit: int talent_pool: int @@ -394,8 +394,6 @@ class Guild(metaclass=YAMLGetter): class Keys(metaclass=YAMLGetter): section = "keys" - deploy_bot: str - deploy_site: str site_api: str @@ -411,14 +409,11 @@ class URLs(metaclass=YAMLGetter): # Misc endpoints bot_avatar: str - deploy: str github_bot_repo: str - status: str # Site endpoints site: str site_api: str - site_clean_api: str site_superstarify_api: str site_logs_api: str site_logs_view: str diff --git a/config-default.yml b/config-default.yml index fd83e69a4..403de21ad 100644 --- a/config-default.yml +++ b/config-default.yml @@ -227,8 +227,6 @@ filter: keys: - deploy_bot: !ENV "DEPLOY_BOT_KEY" - deploy_site: !ENV "DEPLOY_SITE" site_api: !ENV "BOT_API_KEY" @@ -263,10 +261,6 @@ urls: # Snekbox snekbox_eval_api: "https://snekbox.pythondiscord.com/eval" - # Env vars - deploy: !ENV "DEPLOY_URL" - status: !ENV "STATUS_URL" - # Discord API URLs discord_api: &DISCORD_API "https://discordapp.com/api/v7/" discord_invite_api: !JOIN [*DISCORD_API, "invites"] 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 + + +@pytest.mark.parametrize( + '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) -- cgit v1.2.3 From ad711f04a789811c1aade6b49639474c592c044c Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sun, 15 Sep 2019 16:04:54 +0200 Subject: Add basic tests for `bot.api`. --- tests/helpers.py | 22 +++++++++++- tests/test_api.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tests/test_api.py (limited to 'tests') diff --git a/tests/helpers.py b/tests/helpers.py index 57c6fcc1a..f8fbb5e60 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,10 +1,30 @@ +import asyncio +import functools + from unittest.mock import MagicMock -__all__ = ('AsyncMock',) +__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() + + +@pytest.fixture() +def error_api_response(): + response = MagicMock() + response.status = 999 + return response + + +@pytest.fixture() +def api_log_handler(): + return api.APILoggingHandler(None) + + +@pytest.fixture() +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'] -- cgit v1.2.3 From 3ab9c2f8d26e023dc56541c00073deaa39293592 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Wed, 18 Sep 2019 01:29:42 +1000 Subject: Recombine import groups. --- tests/helpers.py | 1 - 1 file changed, 1 deletion(-) (limited to 'tests') diff --git a/tests/helpers.py b/tests/helpers.py index f8fbb5e60..2908294f7 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,6 +1,5 @@ import asyncio import functools - from unittest.mock import MagicMock -- cgit v1.2.3