From d9407a56ba34f3a446f3fa583c0c4dec107913dc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 31 Dec 2019 12:19:44 -0800 Subject: Tests: add a MockAPIClient --- tests/helpers.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'tests') diff --git a/tests/helpers.py b/tests/helpers.py index 5df796c23..71b80a223 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -12,6 +12,7 @@ from typing import Any, Iterable, Optional import discord from discord.ext.commands import Context +from bot.api import APIClient from bot.bot import Bot @@ -324,6 +325,22 @@ class MockUser(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): self.mention = f"@{self.name}" +# Create an APIClient instance to get a realistic MagicMock of `bot.api.APIClient` +api_client_instance = APIClient(loop=unittest.mock.MagicMock()) + + +class MockAPIClient(CustomMockMixin, unittest.mock.MagicMock): + """ + A MagicMock subclass to mock APIClient objects. + + Instances of this class will follow the specifications of `bot.api.APIClient` instances. + For more information, see the `MockGuild` docstring. + """ + + def __init__(self, **kwargs) -> None: + super().__init__(spec_set=api_client_instance, **kwargs) + + # Create a Bot instance to get a realistic MagicMock of `discord.ext.commands.Bot` bot_instance = Bot(command_prefix=unittest.mock.MagicMock()) bot_instance.http_session = None @@ -340,6 +357,7 @@ class MockBot(CustomMockMixin, unittest.mock.MagicMock): def __init__(self, **kwargs) -> None: super().__init__(spec_set=bot_instance, **kwargs) + self.api_client = MockAPIClient() # self.wait_for is *not* a coroutine function, but returns a coroutine nonetheless and # and should therefore be awaited. (The documentation calls it a coroutine as well, which -- cgit v1.2.3 From 43f25fcbbb6cf7b9960317955b57f5e171675d85 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 31 Dec 2019 16:45:57 -0800 Subject: Sync tests: rename the role syncer test case --- tests/bot/cogs/sync/test_roles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 27ae27639..450a192b7 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -3,7 +3,7 @@ import unittest from bot.cogs.sync.syncers import Role, get_roles_for_sync -class GetRolesForSyncTests(unittest.TestCase): +class RoleSyncerTests(unittest.TestCase): """Tests constructing the roles to synchronize with the site.""" def test_get_roles_for_sync_empty_return_for_equal_roles(self): -- cgit v1.2.3 From c487d80c163682ad8e079257b6bf4bfd11743629 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 31 Dec 2019 16:05:14 -0800 Subject: Sync tests: add fixture to create a guild with roles --- tests/bot/cogs/sync/test_roles.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 450a192b7..5ae475b2a 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -1,11 +1,31 @@ import unittest -from bot.cogs.sync.syncers import Role, get_roles_for_sync +import discord + +from bot.cogs.sync.syncers import RoleSyncer +from tests import helpers class RoleSyncerTests(unittest.TestCase): """Tests constructing the roles to synchronize with the site.""" + def setUp(self): + self.bot = helpers.MockBot() + self.syncer = RoleSyncer(self.bot) + + @staticmethod + def get_guild(*roles): + """Fixture to return a guild object with the given roles.""" + guild = helpers.MockGuild() + guild.roles = [] + + for role in roles: + role.colour = discord.Colour(role.colour) + role.permissions = discord.Permissions(role.permissions) + guild.roles.append(helpers.MockRole(**role)) + + return guild + def test_get_roles_for_sync_empty_return_for_equal_roles(self): """No roles should be synced when no diff is found.""" api_roles = {Role(id=41, name='name', colour=33, permissions=0x8, position=1)} -- cgit v1.2.3 From 28c7ce0465bafc0e07432a94d6f388938a2b3b4d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 31 Dec 2019 16:52:45 -0800 Subject: Sync tests: fix creation of MockRoles Role was being accessed like a class when it is actually a dict. --- tests/bot/cogs/sync/test_roles.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 5ae475b2a..b1fe500cd 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -20,9 +20,10 @@ class RoleSyncerTests(unittest.TestCase): guild.roles = [] for role in roles: - role.colour = discord.Colour(role.colour) - role.permissions = discord.Permissions(role.permissions) - guild.roles.append(helpers.MockRole(**role)) + mock_role = helpers.MockRole(**role) + mock_role.colour = discord.Colour(role["colour"]) + mock_role.permissions = discord.Permissions(role["permissions"]) + guild.roles.append(mock_role) return guild -- cgit v1.2.3 From 384a27d18ba258477239daa37569397092e26d76 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 1 Jan 2020 11:44:06 -0800 Subject: Sync tests: test empty diff for identical roles --- tests/bot/cogs/sync/test_roles.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index b1fe500cd..2a60e1fe2 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -1,3 +1,4 @@ +import asyncio import unittest import discord @@ -27,15 +28,17 @@ class RoleSyncerTests(unittest.TestCase): return guild - def test_get_roles_for_sync_empty_return_for_equal_roles(self): - """No roles should be synced when no diff is found.""" - 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)} + def test_empty_diff_for_identical_roles(self): + """No differences should be found if the roles in the guild and DB are identical.""" + role = {"id": 41, "name": "name", "colour": 33, "permissions": 0x8, "position": 1} - self.assertEqual( - get_roles_for_sync(guild_roles, api_roles), - (set(), set(), set()) - ) + self.bot.api_client.get.return_value = [role] + guild = self.get_guild(role) + + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = (set(), set(), set()) + + self.assertEqual(actual_diff, expected_diff) def test_get_roles_for_sync_returns_roles_to_update_with_non_id_diff(self): """Roles to be synced are returned when non-ID attributes differ.""" -- cgit v1.2.3 From 3bafbde6eddbecf3a987b4fe40da00ec79ce4bd4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 1 Jan 2020 16:47:17 -0800 Subject: Sync tests: test diff for updated roles --- tests/bot/cogs/sync/test_roles.py | 43 +++++++++++++++------------------------ 1 file changed, 16 insertions(+), 27 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 2a60e1fe2..31bf13933 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -3,7 +3,7 @@ import unittest import discord -from bot.cogs.sync.syncers import RoleSyncer +from bot.cogs.sync.syncers import RoleSyncer, _Role from tests import helpers @@ -40,35 +40,24 @@ class RoleSyncerTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) - def test_get_roles_for_sync_returns_roles_to_update_with_non_id_diff(self): - """Roles to be synced are returned when non-ID attributes differ.""" - 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)} + def test_diff_for_updated_roles(self): + """Only updated roles should be added to the updated set of the diff.""" + db_roles = [ + {"id": 41, "name": "old", "colour": 33, "permissions": 0x8, "position": 1}, + {"id": 53, "name": "other", "colour": 55, "permissions": 0, "position": 3}, + ] + guild_roles = [ + {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1}, + {"id": 53, "name": "other", "colour": 55, "permissions": 0, "position": 3}, + ] - self.assertEqual( - get_roles_for_sync(guild_roles, api_roles), - (set(), guild_roles, set()) - ) + self.bot.api_client.get.return_value = db_roles + guild = self.get_guild(*guild_roles) - def test_get_roles_only_returns_roles_that_require_update(self): - """Roles that require an update should be returned as the second tuple element.""" - api_roles = { - 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, position=2), - Role(id=53, name='other role', colour=55, permissions=0, position=3) - } + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = (set(), {_Role(**guild_roles[0])}, set()) - self.assertEqual( - get_roles_for_sync(guild_roles, api_roles), - ( - set(), - {Role(id=41, name='new name', colour=35, permissions=0x8, position=2)}, - set(), - ) - ) + self.assertEqual(actual_diff, expected_diff) def test_get_roles_returns_new_roles_in_first_tuple_element(self): """Newly created roles are returned as the first tuple element.""" -- cgit v1.2.3 From d9f6fc4c089814992f8c049cb2837e798390ea7d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 1 Jan 2020 17:14:25 -0800 Subject: Sync tests: create a role in setUp to use as a constant --- tests/bot/cogs/sync/test_roles.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 31bf13933..4eadf8f34 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -13,6 +13,7 @@ class RoleSyncerTests(unittest.TestCase): def setUp(self): self.bot = helpers.MockBot() self.syncer = RoleSyncer(self.bot) + self.constant_role = {"id": 9, "name": "test", "colour": 7, "permissions": 0, "position": 3} @staticmethod def get_guild(*roles): @@ -30,10 +31,8 @@ class RoleSyncerTests(unittest.TestCase): def test_empty_diff_for_identical_roles(self): """No differences should be found if the roles in the guild and DB are identical.""" - role = {"id": 41, "name": "name", "colour": 33, "permissions": 0x8, "position": 1} - - self.bot.api_client.get.return_value = [role] - guild = self.get_guild(role) + self.bot.api_client.get.return_value = [self.constant_role] + guild = self.get_guild(self.constant_role) actual_diff = asyncio.run(self.syncer._get_diff(guild)) expected_diff = (set(), set(), set()) @@ -44,11 +43,11 @@ class RoleSyncerTests(unittest.TestCase): """Only updated roles should be added to the updated set of the diff.""" db_roles = [ {"id": 41, "name": "old", "colour": 33, "permissions": 0x8, "position": 1}, - {"id": 53, "name": "other", "colour": 55, "permissions": 0, "position": 3}, + self.constant_role, ] guild_roles = [ {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1}, - {"id": 53, "name": "other", "colour": 55, "permissions": 0, "position": 3}, + self.constant_role, ] self.bot.api_client.get.return_value = db_roles -- cgit v1.2.3 From 99ff41a7abe6b1ccba809654657ba0ba25c43008 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 1 Jan 2020 17:25:02 -0800 Subject: Sync tests: test diff for new roles --- tests/bot/cogs/sync/test_roles.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 4eadf8f34..184050618 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -40,8 +40,8 @@ class RoleSyncerTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) def test_diff_for_updated_roles(self): - """Only updated roles should be added to the updated set of the diff.""" - db_roles = [ + """Only updated roles should be added to the 'updated' set of the diff.""" + self.bot.api_client.get.return_value = [ {"id": 41, "name": "old", "colour": 33, "permissions": 0x8, "position": 1}, self.constant_role, ] @@ -50,7 +50,6 @@ class RoleSyncerTests(unittest.TestCase): self.constant_role, ] - self.bot.api_client.get.return_value = db_roles guild = self.get_guild(*guild_roles) actual_diff = asyncio.run(self.syncer._get_diff(guild)) @@ -58,24 +57,20 @@ class RoleSyncerTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) - def test_get_roles_returns_new_roles_in_first_tuple_element(self): - """Newly created roles are returned as the first tuple element.""" - api_roles = { - Role(id=41, name='name', colour=35, permissions=0x8, position=1), - } - guild_roles = { - Role(id=41, name='name', colour=35, permissions=0x8, position=1), - Role(id=53, name='other role', colour=55, permissions=0, position=2) - } + def test_diff_for_new_roles(self): + """Only new roles should be added to the 'created' set of the diff.""" + self.bot.api_client.get.return_value = [self.constant_role] + guild_roles = [ + self.constant_role, + {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1} + ] - self.assertEqual( - get_roles_for_sync(guild_roles, api_roles), - ( - {Role(id=53, name='other role', colour=55, permissions=0, position=2)}, - set(), - set(), - ) - ) + guild = self.get_guild(*guild_roles) + + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = ({_Role(**guild_roles[1])}, set(), set()) + + self.assertEqual(actual_diff, expected_diff) def test_get_roles_returns_roles_to_update_and_new_roles(self): """Newly created and updated roles should be returned together.""" -- cgit v1.2.3 From 51d0e8672a4836b46d99a7a5af42a3d9f363cf57 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 2 Jan 2020 09:17:43 -0800 Subject: Sync tests: test diff for deleted roles --- tests/bot/cogs/sync/test_roles.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 184050618..694ee6276 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -91,24 +91,17 @@ class RoleSyncerTests(unittest.TestCase): ) ) - def test_get_roles_returns_roles_to_delete(self): - """Roles to be deleted should be returned as the third tuple element.""" - 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), - } + def test_diff_for_deleted_roles(self): + """Only deleted roles should be added to the 'deleted' set of the diff.""" + deleted_role = {"id": 61, "name": "delete", "colour": 99, "permissions": 0x9, "position": 2} - self.assertEqual( - get_roles_for_sync(guild_roles, api_roles), - ( - set(), - set(), - {Role(id=61, name='to delete', colour=99, permissions=0x9, position=2)}, - ) - ) + self.bot.api_client.get.return_value = [self.constant_role, deleted_role] + guild = self.get_guild(self.constant_role) + + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = (set(), set(), {_Role(**deleted_role)}) + + self.assertEqual(actual_diff, expected_diff) def test_get_roles_returns_roles_to_delete_update_and_new_roles(self): """When roles were added, updated, and removed, all of them are returned properly.""" -- cgit v1.2.3 From f17a61ac8426bf756ee1f236bbd8f0e33d4932b5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 2 Jan 2020 09:27:00 -0800 Subject: Sync tests: test diff for all 3 role changes simultaneously --- tests/bot/cogs/sync/test_roles.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 694ee6276..ccd617463 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -62,7 +62,7 @@ class RoleSyncerTests(unittest.TestCase): self.bot.api_client.get.return_value = [self.constant_role] guild_roles = [ self.constant_role, - {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1} + {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1}, ] guild = self.get_guild(*guild_roles) @@ -103,24 +103,20 @@ class RoleSyncerTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) - def test_get_roles_returns_roles_to_delete_update_and_new_roles(self): - """When roles were added, updated, and removed, all of them are returned properly.""" - 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='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), - } + def test_diff_for_new_updated_and_deleted_roles(self): + """When roles are added, updated, and removed, all of them are returned properly.""" + new = {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1} + updated = {"id": 71, "name": "updated", "colour": 101, "permissions": 0x5, "position": 4} + deleted = {"id": 61, "name": "delete", "colour": 99, "permissions": 0x9, "position": 2} - self.assertEqual( - get_roles_for_sync(guild_roles, api_roles), - ( - {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)}, - ) - ) + self.bot.api_client.get.return_value = [ + self.constant_role, + {"id": 71, "name": "update", "colour": 99, "permissions": 0x9, "position": 4}, + deleted, + ] + guild = self.get_guild(self.constant_role, new, updated) + + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = ({_Role(**new)}, {_Role(**updated)}, {_Role(**deleted)}) + + self.assertEqual(actual_diff, expected_diff) -- cgit v1.2.3 From d212fb724be9ac6ab05671f28113318113a4bbe3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 2 Jan 2020 09:27:51 -0800 Subject: Sync tests: remove diff test for updated and new roles together Redundant since test_diff_for_new_updated_and_deleted_roles tests all 3 types together. --- tests/bot/cogs/sync/test_roles.py | 19 ------------------- 1 file changed, 19 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index ccd617463..ca9df4305 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -72,25 +72,6 @@ class RoleSyncerTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) - def test_get_roles_returns_roles_to_update_and_new_roles(self): - """Newly created and updated roles should be returned together.""" - api_roles = { - 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) - } - - self.assertEqual( - 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_diff_for_deleted_roles(self): """Only deleted roles should be added to the 'deleted' set of the diff.""" deleted_role = {"id": 61, "name": "delete", "colour": 99, "permissions": 0x9, "position": 2} -- cgit v1.2.3 From 86cdf82bc7fc96334994f8289f77ea3a6a14828b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 2 Jan 2020 09:31:09 -0800 Subject: Sync tests: remove guild_roles lists and assign roles to variables Makes the creation of the expected diff clearer since the variable has a name compared to accessing some index of a list. --- tests/bot/cogs/sync/test_roles.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index ca9df4305..b9a4fe6cd 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -41,34 +41,28 @@ class RoleSyncerTests(unittest.TestCase): def test_diff_for_updated_roles(self): """Only updated roles should be added to the 'updated' set of the diff.""" + updated_role = {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1} + self.bot.api_client.get.return_value = [ {"id": 41, "name": "old", "colour": 33, "permissions": 0x8, "position": 1}, self.constant_role, ] - guild_roles = [ - {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1}, - self.constant_role, - ] - - guild = self.get_guild(*guild_roles) + guild = self.get_guild(updated_role, self.constant_role) actual_diff = asyncio.run(self.syncer._get_diff(guild)) - expected_diff = (set(), {_Role(**guild_roles[0])}, set()) + expected_diff = (set(), {_Role(**updated_role)}, set()) self.assertEqual(actual_diff, expected_diff) def test_diff_for_new_roles(self): """Only new roles should be added to the 'created' set of the diff.""" - self.bot.api_client.get.return_value = [self.constant_role] - guild_roles = [ - self.constant_role, - {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1}, - ] + new_role = {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1} - guild = self.get_guild(*guild_roles) + self.bot.api_client.get.return_value = [self.constant_role] + guild = self.get_guild(self.constant_role, new_role) actual_diff = asyncio.run(self.syncer._get_diff(guild)) - expected_diff = ({_Role(**guild_roles[1])}, set(), set()) + expected_diff = ({_Role(**new_role)}, set(), set()) self.assertEqual(actual_diff, expected_diff) -- cgit v1.2.3 From 7c39e44e5c611e01edb0510e23c69dc316ffd184 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 2 Jan 2020 10:16:54 -0800 Subject: Sync tests: create separate role test cases for diff and sync tests --- tests/bot/cogs/sync/test_roles.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index b9a4fe6cd..10818a501 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -7,8 +7,8 @@ from bot.cogs.sync.syncers import RoleSyncer, _Role from tests import helpers -class RoleSyncerTests(unittest.TestCase): - """Tests constructing the roles to synchronize with the site.""" +class RoleSyncerDiffTests(unittest.TestCase): + """Tests for determining differences between roles in the DB and roles in the Guild cache.""" def setUp(self): self.bot = helpers.MockBot() @@ -95,3 +95,11 @@ class RoleSyncerTests(unittest.TestCase): expected_diff = ({_Role(**new)}, {_Role(**updated)}, {_Role(**deleted)}) self.assertEqual(actual_diff, expected_diff) + + +class RoleSyncerSyncTests(unittest.TestCase): + """Tests for the API requests that sync roles.""" + + def setUp(self): + self.bot = helpers.MockBot() + self.syncer = RoleSyncer(self.bot) -- cgit v1.2.3 From dd07547977a4d49d34ebf597d6072d274b2e4feb Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 2 Jan 2020 10:22:11 -0800 Subject: Sync tests: test API requests for role syncing --- tests/bot/cogs/sync/test_roles.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 10818a501..719c93d7a 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -3,7 +3,7 @@ import unittest import discord -from bot.cogs.sync.syncers import RoleSyncer, _Role +from bot.cogs.sync.syncers import RoleSyncer, _Diff, _Role from tests import helpers @@ -103,3 +103,36 @@ class RoleSyncerSyncTests(unittest.TestCase): def setUp(self): self.bot = helpers.MockBot() self.syncer = RoleSyncer(self.bot) + + def test_sync_created_role(self): + """Only a POST request should be made with the correct payload.""" + role = {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1} + diff = _Diff({_Role(**role)}, set(), set()) + + asyncio.run(self.syncer._sync(diff)) + + self.bot.api_client.post.assert_called_once_with("bot/roles", json=role) + self.bot.api_client.put.assert_not_called() + self.bot.api_client.delete.assert_not_called() + + def test_sync_updated_role(self): + """Only a PUT request should be made with the correct payload.""" + role = {"id": 51, "name": "updated", "colour": 44, "permissions": 0x7, "position": 2} + diff = _Diff(set(), {_Role(**role)}, set()) + + asyncio.run(self.syncer._sync(diff)) + + self.bot.api_client.put.assert_called_once_with(f"bot/roles/{role['id']}", json=role) + self.bot.api_client.post.assert_not_called() + self.bot.api_client.delete.assert_not_called() + + def test_sync_deleted_role(self): + """Only a DELETE request should be made with the correct payload.""" + role = {"id": 61, "name": "deleted", "colour": 55, "permissions": 0x6, "position": 3} + diff = _Diff(set(), set(), {_Role(**role)}) + + asyncio.run(self.syncer._sync(diff)) + + self.bot.api_client.delete.assert_called_once_with(f"bot/roles/{role['id']}") + self.bot.api_client.post.assert_not_called() + self.bot.api_client.put.assert_not_called() -- cgit v1.2.3 From dc3841f5d737a2f697f62970186205c7b12d825e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 2 Jan 2020 12:20:18 -0800 Subject: Sync tests: test syncs with multiple roles --- tests/bot/cogs/sync/test_roles.py | 52 ++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 15 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 719c93d7a..389985bc3 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -1,5 +1,6 @@ import asyncio import unittest +from unittest import mock import discord @@ -104,35 +105,56 @@ class RoleSyncerSyncTests(unittest.TestCase): self.bot = helpers.MockBot() self.syncer = RoleSyncer(self.bot) - def test_sync_created_role(self): - """Only a POST request should be made with the correct payload.""" - role = {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1} - diff = _Diff({_Role(**role)}, set(), set()) + def test_sync_created_roles(self): + """Only POST requests should be made with the correct payload.""" + roles = [ + {"id": 111, "name": "new", "colour": 4, "permissions": 0x7, "position": 1}, + {"id": 222, "name": "new2", "colour": 44, "permissions": 0x7, "position": 11}, + ] + role_tuples = {_Role(**role) for role in roles} + diff = _Diff(role_tuples, set(), set()) asyncio.run(self.syncer._sync(diff)) - self.bot.api_client.post.assert_called_once_with("bot/roles", json=role) + calls = [mock.call("bot/roles", json=role) for role in roles] + self.bot.api_client.post.assert_has_calls(calls, any_order=True) + self.assertEqual(self.bot.api_client.post.call_count, len(roles)) + self.bot.api_client.put.assert_not_called() self.bot.api_client.delete.assert_not_called() - def test_sync_updated_role(self): - """Only a PUT request should be made with the correct payload.""" - role = {"id": 51, "name": "updated", "colour": 44, "permissions": 0x7, "position": 2} - diff = _Diff(set(), {_Role(**role)}, set()) + def test_sync_updated_roles(self): + """Only PUT requests should be made with the correct payload.""" + roles = [ + {"id": 333, "name": "updated", "colour": 5, "permissions": 0x7, "position": 2}, + {"id": 444, "name": "updated2", "colour": 55, "permissions": 0x7, "position": 22}, + ] + role_tuples = {_Role(**role) for role in roles} + diff = _Diff(set(), role_tuples, set()) asyncio.run(self.syncer._sync(diff)) - self.bot.api_client.put.assert_called_once_with(f"bot/roles/{role['id']}", json=role) + calls = [mock.call(f"bot/roles/{role['id']}", json=role) for role in roles] + self.bot.api_client.put.assert_has_calls(calls, any_order=True) + self.assertEqual(self.bot.api_client.put.call_count, len(roles)) + self.bot.api_client.post.assert_not_called() self.bot.api_client.delete.assert_not_called() - def test_sync_deleted_role(self): - """Only a DELETE request should be made with the correct payload.""" - role = {"id": 61, "name": "deleted", "colour": 55, "permissions": 0x6, "position": 3} - diff = _Diff(set(), set(), {_Role(**role)}) + def test_sync_deleted_roles(self): + """Only DELETE requests should be made with the correct payload.""" + roles = [ + {"id": 555, "name": "deleted", "colour": 6, "permissions": 0x7, "position": 3}, + {"id": 666, "name": "deleted2", "colour": 66, "permissions": 0x7, "position": 33}, + ] + role_tuples = {_Role(**role) for role in roles} + diff = _Diff(set(), set(), role_tuples) asyncio.run(self.syncer._sync(diff)) - self.bot.api_client.delete.assert_called_once_with(f"bot/roles/{role['id']}") + calls = [mock.call(f"bot/roles/{role['id']}") for role in roles] + self.bot.api_client.delete.assert_has_calls(calls, any_order=True) + self.assertEqual(self.bot.api_client.delete.call_count, len(roles)) + self.bot.api_client.post.assert_not_called() self.bot.api_client.put.assert_not_called() -- cgit v1.2.3 From aa9f5a5eb96cfdf3482f94b0484eed1e54c3b75e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 2 Jan 2020 13:52:38 -0800 Subject: Sync tests: rename user sync test case --- tests/bot/cogs/sync/test_users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index ccaf67490..509b703ae 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -13,8 +13,8 @@ def fake_user(**kwargs): return User(**kwargs) -class GetUsersForSyncTests(unittest.TestCase): - """Tests constructing the users to synchronize with the site.""" +class UserSyncerDiffTests(unittest.TestCase): + """Tests for determining differences between users in the DB and users in the Guild cache.""" def test_get_users_for_sync_returns_nothing_for_empty_params(self): """When no users are given, none are returned.""" -- cgit v1.2.3 From 7a8c71b7cd5b446188b053aef139255af7bf0154 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 2 Jan 2020 19:31:08 -0800 Subject: Sync tests: add fixture to get a guild with members --- tests/bot/cogs/sync/test_users.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index 509b703ae..83a9cdaf0 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -1,6 +1,7 @@ import unittest -from bot.cogs.sync.syncers import User, get_users_for_sync +from bot.cogs.sync.syncers import UserSyncer +from tests import helpers def fake_user(**kwargs): @@ -16,6 +17,23 @@ def fake_user(**kwargs): class UserSyncerDiffTests(unittest.TestCase): """Tests for determining differences between users in the DB and users in the Guild cache.""" + def setUp(self): + self.bot = helpers.MockBot() + self.syncer = UserSyncer(self.bot) + + @staticmethod + def get_guild(*members): + """Fixture to return a guild object with the given members.""" + guild = helpers.MockGuild() + guild.members = [] + + for member in members: + roles = (helpers.MockRole(id=role_id) for role_id in member.pop("roles")) + mock_member = helpers.MockMember(roles, **member) + guild.members.append(mock_member) + + return guild + def test_get_users_for_sync_returns_nothing_for_empty_params(self): """When no users are given, none are returned.""" self.assertEqual( -- cgit v1.2.3 From f263877518562e33b661e70f6ea3e8f3b1ab914b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 2 Jan 2020 19:34:25 -0800 Subject: Sync tests: test empty diff for no users --- tests/bot/cogs/sync/test_users.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index 83a9cdaf0..b5175a27c 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -1,3 +1,4 @@ +import asyncio import unittest from bot.cogs.sync.syncers import UserSyncer @@ -34,12 +35,14 @@ class UserSyncerDiffTests(unittest.TestCase): return guild - def test_get_users_for_sync_returns_nothing_for_empty_params(self): - """When no users are given, none are returned.""" - self.assertEqual( - get_users_for_sync({}, {}), - (set(), set()) - ) + def test_empty_diff_for_no_users(self): + """When no users are given, an empty diff should be returned.""" + guild = self.get_guild() + + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = (set(), set(), None) + + self.assertEqual(actual_diff, expected_diff) def test_get_users_for_sync_returns_nothing_for_equal_users(self): """When no users are updated, none are returned.""" -- cgit v1.2.3 From 7036a9a32651ee0cfb820f994a7332f024169579 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 10:01:48 -0800 Subject: Sync tests: fix fake_user fixture --- tests/bot/cogs/sync/test_users.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index b5175a27c..f3d88c59f 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -6,13 +6,15 @@ from tests import helpers 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) + """Fixture to return a dictionary representing a user with default values set.""" + 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 kwargs class UserSyncerDiffTests(unittest.TestCase): -- cgit v1.2.3 From f49d50164cc8afcf1245f3ec47b7963c6874ece6 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 10:15:33 -0800 Subject: Sync tests: fix mismatched attributes when creating a mock user --- tests/bot/cogs/sync/test_users.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index f3d88c59f..4c79c51c5 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -32,6 +32,9 @@ class UserSyncerDiffTests(unittest.TestCase): for member in members: roles = (helpers.MockRole(id=role_id) for role_id in member.pop("roles")) + member["avatar"] = member.pop("avatar_hash") + del member["in_guild"] + mock_member = helpers.MockMember(roles, **member) guild.members.append(mock_member) -- cgit v1.2.3 From eab415b61122de4c039b229390e1d6c180d101da Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 10:53:32 -0800 Subject: Sync tests: work around @everyone role being added by MockMember --- tests/bot/cogs/sync/test_users.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index 4c79c51c5..3dd2942b5 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -31,11 +31,12 @@ class UserSyncerDiffTests(unittest.TestCase): guild.members = [] for member in members: - roles = (helpers.MockRole(id=role_id) for role_id in member.pop("roles")) member["avatar"] = member.pop("avatar_hash") del member["in_guild"] - mock_member = helpers.MockMember(roles, **member) + mock_member = helpers.MockMember(**member) + mock_member.roles = [helpers.MockRole(id=role_id) for role_id in member["roles"]] + guild.members.append(mock_member) return guild -- cgit v1.2.3 From 4912e94e3079b01b9481dee785c0b7f2552f7a1b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 10:53:45 -0800 Subject: Sync tests: test empty diff for identical users --- tests/bot/cogs/sync/test_users.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index 3dd2942b5..7a4a85c96 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -50,15 +50,15 @@ class UserSyncerDiffTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) - def test_get_users_for_sync_returns_nothing_for_equal_users(self): - """When no users are updated, none are returned.""" - api_users = {43: fake_user()} - guild_users = {43: fake_user()} + def test_empty_diff_for_identical_users(self): + """No differences should be found if the users in the guild and DB are identical.""" + self.bot.api_client.get.return_value = [fake_user()] + guild = self.get_guild(fake_user()) - self.assertEqual( - get_users_for_sync(guild_users, api_users), - (set(), set()) - ) + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = (set(), set(), None) + + self.assertEqual(actual_diff, expected_diff) def test_get_users_for_sync_returns_users_to_update_on_non_id_field_diff(self): """When a non-ID-field differs, the user to update is returned.""" -- cgit v1.2.3 From c53cc07217faa15f56c60c3b36aefbb7676e6011 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 11:20:07 -0800 Subject: Sync tests: fix get_guild modifying the original member dicts --- tests/bot/cogs/sync/test_users.py | 1 + 1 file changed, 1 insertion(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index 7a4a85c96..0d00e6970 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -31,6 +31,7 @@ class UserSyncerDiffTests(unittest.TestCase): guild.members = [] for member in members: + member = member.copy() member["avatar"] = member.pop("avatar_hash") del member["in_guild"] -- cgit v1.2.3 From e74d360e3834511ffa2fb93f1146cda664a403a5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 11:20:33 -0800 Subject: Sync tests: test diff for updated users --- tests/bot/cogs/sync/test_users.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index 0d00e6970..f1084fa98 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -1,7 +1,7 @@ import asyncio import unittest -from bot.cogs.sync.syncers import UserSyncer +from bot.cogs.sync.syncers import UserSyncer, _User from tests import helpers @@ -61,15 +61,17 @@ class UserSyncerDiffTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) - def test_get_users_for_sync_returns_users_to_update_on_non_id_field_diff(self): - """When a non-ID-field differs, the user to update is returned.""" - api_users = {43: fake_user()} - guild_users = {43: fake_user(name='new fancy name')} + def test_diff_for_updated_users(self): + """Only updated users should be added to the 'updated' set of the diff.""" + updated_user = fake_user(id=99, name="new") - self.assertEqual( - get_users_for_sync(guild_users, api_users), - (set(), {fake_user(name='new fancy name')}) - ) + self.bot.api_client.get.return_value = [fake_user(id=99, name="old"), fake_user()] + guild = self.get_guild(updated_user, fake_user()) + + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = (set(), {_User(**updated_user)}, None) + + self.assertEqual(actual_diff, expected_diff) def test_get_users_for_sync_returns_users_to_create_with_new_ids_on_guild(self): """When new users join the guild, they are returned as the first tuple element.""" -- cgit v1.2.3 From 30ebb0184d12000db3ae5f276395fecd52d5dfa5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 11:22:46 -0800 Subject: Sync tests: test diff for new users --- tests/bot/cogs/sync/test_users.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index f1084fa98..c8ce7c04d 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -73,15 +73,17 @@ class UserSyncerDiffTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) - def test_get_users_for_sync_returns_users_to_create_with_new_ids_on_guild(self): - """When new users join the guild, they are returned as the first tuple element.""" - api_users = {43: fake_user()} - guild_users = {43: fake_user(), 63: fake_user(id=63)} + def test_diff_for_new_users(self): + """Only new users should be added to the 'created' set of the diff.""" + new_user = fake_user(id=99, name="new") - self.assertEqual( - get_users_for_sync(guild_users, api_users), - ({fake_user(id=63)}, set()) - ) + self.bot.api_client.get.return_value = [fake_user()] + guild = self.get_guild(fake_user(), new_user) + + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = ({_User(**new_user)}, set(), None) + + self.assertEqual(actual_diff, expected_diff) def test_get_users_for_sync_updates_in_guild_field_on_user_leave(self): """When a user leaves the guild, the `in_guild` flag is updated to `False`.""" -- cgit v1.2.3 From 16f7eda6005b974ee2bc77f0440e05afad46c8e7 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 11:34:34 -0800 Subject: Sync tests: test diff for users which leave the guild --- tests/bot/cogs/sync/test_users.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index c8ce7c04d..faa5918df 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -85,15 +85,17 @@ class UserSyncerDiffTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) - def test_get_users_for_sync_updates_in_guild_field_on_user_leave(self): + def test_diff_sets_in_guild_false_for_leaving_users(self): """When a user leaves the guild, the `in_guild` flag is updated to `False`.""" - api_users = {43: fake_user(), 63: fake_user(id=63)} - guild_users = {43: fake_user()} + leaving_user = fake_user(id=63, in_guild=False) - self.assertEqual( - get_users_for_sync(guild_users, api_users), - (set(), {fake_user(id=63, in_guild=False)}) - ) + self.bot.api_client.get.return_value = [fake_user(), fake_user(id=63)] + guild = self.get_guild(fake_user()) + + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = (set(), {_User(**leaving_user)}, None) + + self.assertEqual(actual_diff, expected_diff) def test_get_users_for_sync_updates_and_creates_users_as_needed(self): """When one user left and another one was updated, both are returned.""" -- cgit v1.2.3 From 6401306228526250092fece786640be281eac812 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 11:43:35 -0800 Subject: Sync tests: test diff for all 3 changes simultaneously --- tests/bot/cogs/sync/test_users.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index faa5918df..ff863a929 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -97,15 +97,19 @@ class UserSyncerDiffTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) - def test_get_users_for_sync_updates_and_creates_users_as_needed(self): - """When one user left and another one was updated, both are returned.""" - api_users = {43: fake_user()} - guild_users = {63: fake_user(id=63)} + def test_diff_for_new_updated_and_leaving_users(self): + """When users are added, updated, and removed, all of them are returned properly.""" + new_user = fake_user(id=99, name="new") + updated_user = fake_user(id=55, name="updated") + leaving_user = fake_user(id=63, in_guild=False) - self.assertEqual( - get_users_for_sync(guild_users, api_users), - ({fake_user(id=63)}, {fake_user(in_guild=False)}) - ) + self.bot.api_client.get.return_value = [fake_user(), fake_user(id=55), fake_user(id=63)] + guild = self.get_guild(fake_user(), new_user, updated_user) + + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = ({_User(**new_user)}, {_User(**updated_user), _User(**leaving_user)}, None) + + self.assertEqual(actual_diff, expected_diff) def test_get_users_for_sync_does_not_duplicate_update_users(self): """When the API knows a user the guild doesn't, nothing is performed.""" -- cgit v1.2.3 From 01d7b53180864b1e47ebc8c831a706dc1a3c0d79 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 11:45:47 -0800 Subject: Sync tests: test diff is empty when DB has a user not in the guild --- tests/bot/cogs/sync/test_users.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index ff863a929..dfb9ac405 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -111,12 +111,12 @@ class UserSyncerDiffTests(unittest.TestCase): self.assertEqual(actual_diff, expected_diff) - def test_get_users_for_sync_does_not_duplicate_update_users(self): - """When the API knows a user the guild doesn't, nothing is performed.""" - api_users = {43: fake_user(in_guild=False)} - guild_users = {} - - self.assertEqual( - get_users_for_sync(guild_users, api_users), - (set(), set()) - ) + def test_empty_diff_for_db_users_not_in_guild(self): + """When the DB knows a user the guild doesn't, no difference is found.""" + self.bot.api_client.get.return_value = [fake_user(), fake_user(id=63, in_guild=False)] + guild = self.get_guild(fake_user()) + + actual_diff = asyncio.run(self.syncer._get_diff(guild)) + expected_diff = (set(), set(), None) + + self.assertEqual(actual_diff, expected_diff) -- cgit v1.2.3 From 155c4c7a1bb73ef42cf19ccacc612c7a5bc17201 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 11:54:39 -0800 Subject: Sync tests: add tests for API requests for syncing users --- tests/bot/cogs/sync/test_users.py | 41 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index dfb9ac405..7fc1b400f 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -1,7 +1,8 @@ import asyncio import unittest +from unittest import mock -from bot.cogs.sync.syncers import UserSyncer, _User +from bot.cogs.sync.syncers import UserSyncer, _Diff, _User from tests import helpers @@ -120,3 +121,41 @@ class UserSyncerDiffTests(unittest.TestCase): expected_diff = (set(), set(), None) self.assertEqual(actual_diff, expected_diff) + + +class UserSyncerSyncTests(unittest.TestCase): + """Tests for the API requests that sync roles.""" + + def setUp(self): + self.bot = helpers.MockBot() + self.syncer = UserSyncer(self.bot) + + def test_sync_created_users(self): + """Only POST requests should be made with the correct payload.""" + users = [fake_user(id=111), fake_user(id=222)] + + user_tuples = {_User(**user) for user in users} + diff = _Diff(user_tuples, set(), None) + asyncio.run(self.syncer._sync(diff)) + + calls = [mock.call("bot/users", json=user) for user in users] + self.bot.api_client.post.assert_has_calls(calls, any_order=True) + self.assertEqual(self.bot.api_client.post.call_count, len(users)) + + self.bot.api_client.put.assert_not_called() + self.bot.api_client.delete.assert_not_called() + + def test_sync_updated_users(self): + """Only PUT requests should be made with the correct payload.""" + users = [fake_user(id=111), fake_user(id=222)] + + user_tuples = {_User(**user) for user in users} + diff = _Diff(set(), user_tuples, None) + asyncio.run(self.syncer._sync(diff)) + + calls = [mock.call(f"bot/users/{user['id']}", json=user) for user in users] + self.bot.api_client.put.assert_has_calls(calls, any_order=True) + self.assertEqual(self.bot.api_client.put.call_count, len(users)) + + self.bot.api_client.post.assert_not_called() + self.bot.api_client.delete.assert_not_called() -- cgit v1.2.3 From b5febafba40e3de655b723eed274ac94919a395e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 12:01:47 -0800 Subject: Sync tests: create and use a fake_role fixture --- tests/bot/cogs/sync/test_roles.py | 64 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 33 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 389985bc3..8324b99cd 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -8,13 +8,23 @@ from bot.cogs.sync.syncers import RoleSyncer, _Diff, _Role from tests import helpers +def fake_role(**kwargs): + """Fixture to return a dictionary representing a role with default values set.""" + kwargs.setdefault("id", 9) + kwargs.setdefault("name", "fake role") + kwargs.setdefault("colour", 7) + kwargs.setdefault("permissions", 0) + kwargs.setdefault("position", 55) + + return kwargs + + class RoleSyncerDiffTests(unittest.TestCase): """Tests for determining differences between roles in the DB and roles in the Guild cache.""" def setUp(self): self.bot = helpers.MockBot() self.syncer = RoleSyncer(self.bot) - self.constant_role = {"id": 9, "name": "test", "colour": 7, "permissions": 0, "position": 3} @staticmethod def get_guild(*roles): @@ -32,8 +42,8 @@ class RoleSyncerDiffTests(unittest.TestCase): def test_empty_diff_for_identical_roles(self): """No differences should be found if the roles in the guild and DB are identical.""" - self.bot.api_client.get.return_value = [self.constant_role] - guild = self.get_guild(self.constant_role) + self.bot.api_client.get.return_value = [fake_role()] + guild = self.get_guild(fake_role()) actual_diff = asyncio.run(self.syncer._get_diff(guild)) expected_diff = (set(), set(), set()) @@ -42,13 +52,10 @@ class RoleSyncerDiffTests(unittest.TestCase): def test_diff_for_updated_roles(self): """Only updated roles should be added to the 'updated' set of the diff.""" - updated_role = {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1} + updated_role = fake_role(id=41, name="new") - self.bot.api_client.get.return_value = [ - {"id": 41, "name": "old", "colour": 33, "permissions": 0x8, "position": 1}, - self.constant_role, - ] - guild = self.get_guild(updated_role, self.constant_role) + self.bot.api_client.get.return_value = [fake_role(id=41, name="old"), fake_role()] + guild = self.get_guild(updated_role, fake_role()) actual_diff = asyncio.run(self.syncer._get_diff(guild)) expected_diff = (set(), {_Role(**updated_role)}, set()) @@ -57,10 +64,10 @@ class RoleSyncerDiffTests(unittest.TestCase): def test_diff_for_new_roles(self): """Only new roles should be added to the 'created' set of the diff.""" - new_role = {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1} + new_role = fake_role(id=41, name="new") - self.bot.api_client.get.return_value = [self.constant_role] - guild = self.get_guild(self.constant_role, new_role) + self.bot.api_client.get.return_value = [fake_role()] + guild = self.get_guild(fake_role(), new_role) actual_diff = asyncio.run(self.syncer._get_diff(guild)) expected_diff = ({_Role(**new_role)}, set(), set()) @@ -69,10 +76,10 @@ class RoleSyncerDiffTests(unittest.TestCase): def test_diff_for_deleted_roles(self): """Only deleted roles should be added to the 'deleted' set of the diff.""" - deleted_role = {"id": 61, "name": "delete", "colour": 99, "permissions": 0x9, "position": 2} + deleted_role = fake_role(id=61, name="deleted") - self.bot.api_client.get.return_value = [self.constant_role, deleted_role] - guild = self.get_guild(self.constant_role) + self.bot.api_client.get.return_value = [fake_role(), deleted_role] + guild = self.get_guild(fake_role()) actual_diff = asyncio.run(self.syncer._get_diff(guild)) expected_diff = (set(), set(), {_Role(**deleted_role)}) @@ -81,16 +88,16 @@ class RoleSyncerDiffTests(unittest.TestCase): def test_diff_for_new_updated_and_deleted_roles(self): """When roles are added, updated, and removed, all of them are returned properly.""" - new = {"id": 41, "name": "new", "colour": 33, "permissions": 0x8, "position": 1} - updated = {"id": 71, "name": "updated", "colour": 101, "permissions": 0x5, "position": 4} - deleted = {"id": 61, "name": "delete", "colour": 99, "permissions": 0x9, "position": 2} + new = fake_role(id=41, name="new") + updated = fake_role(id=71, name="updated") + deleted = fake_role(id=61, name="deleted") self.bot.api_client.get.return_value = [ - self.constant_role, - {"id": 71, "name": "update", "colour": 99, "permissions": 0x9, "position": 4}, + fake_role(), + fake_role(id=71, name="updated name"), deleted, ] - guild = self.get_guild(self.constant_role, new, updated) + guild = self.get_guild(fake_role(), new, updated) actual_diff = asyncio.run(self.syncer._get_diff(guild)) expected_diff = ({_Role(**new)}, {_Role(**updated)}, {_Role(**deleted)}) @@ -107,10 +114,7 @@ class RoleSyncerSyncTests(unittest.TestCase): def test_sync_created_roles(self): """Only POST requests should be made with the correct payload.""" - roles = [ - {"id": 111, "name": "new", "colour": 4, "permissions": 0x7, "position": 1}, - {"id": 222, "name": "new2", "colour": 44, "permissions": 0x7, "position": 11}, - ] + roles = [fake_role(id=111), fake_role(id=222)] role_tuples = {_Role(**role) for role in roles} diff = _Diff(role_tuples, set(), set()) @@ -125,10 +129,7 @@ class RoleSyncerSyncTests(unittest.TestCase): def test_sync_updated_roles(self): """Only PUT requests should be made with the correct payload.""" - roles = [ - {"id": 333, "name": "updated", "colour": 5, "permissions": 0x7, "position": 2}, - {"id": 444, "name": "updated2", "colour": 55, "permissions": 0x7, "position": 22}, - ] + roles = [fake_role(id=111), fake_role(id=222)] role_tuples = {_Role(**role) for role in roles} diff = _Diff(set(), role_tuples, set()) @@ -143,10 +144,7 @@ class RoleSyncerSyncTests(unittest.TestCase): def test_sync_deleted_roles(self): """Only DELETE requests should be made with the correct payload.""" - roles = [ - {"id": 555, "name": "deleted", "colour": 6, "permissions": 0x7, "position": 3}, - {"id": 666, "name": "deleted2", "colour": 66, "permissions": 0x7, "position": 33}, - ] + roles = [fake_role(id=111), fake_role(id=222)] role_tuples = {_Role(**role) for role in roles} diff = _Diff(set(), set(), role_tuples) -- cgit v1.2.3 From 396d2b393a255580ea23c3cc4abb4bdb1e84ea7d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 12:08:44 -0800 Subject: Sync tests: fix docstring for UserSyncerSyncTests --- tests/bot/cogs/sync/test_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index 7fc1b400f..e9f9db2ea 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -124,7 +124,7 @@ class UserSyncerDiffTests(unittest.TestCase): class UserSyncerSyncTests(unittest.TestCase): - """Tests for the API requests that sync roles.""" + """Tests for the API requests that sync users.""" def setUp(self): self.bot = helpers.MockBot() -- cgit v1.2.3 From f6c78b63bccc36526d8ee8072a27e0678db0781a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 3 Jan 2020 12:12:00 -0800 Subject: Sync tests: fix wait_until_ready in duck pond tests --- tests/bot/cogs/test_duck_pond.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/test_duck_pond.py b/tests/bot/cogs/test_duck_pond.py index d07b2bce1..5b0a3b8c3 100644 --- a/tests/bot/cogs/test_duck_pond.py +++ b/tests/bot/cogs/test_duck_pond.py @@ -54,7 +54,7 @@ class DuckPondTests(base.LoggingTestCase): asyncio.run(self.cog.fetch_webhook()) - self.bot.wait_until_ready.assert_called_once() + self.bot.wait_until_guild_available.assert_called_once() self.bot.fetch_webhook.assert_called_once_with(1) self.assertEqual(self.cog.webhook, "dummy webhook") @@ -67,7 +67,7 @@ class DuckPondTests(base.LoggingTestCase): with self.assertLogs(logger=log, level=logging.ERROR) as log_watcher: asyncio.run(self.cog.fetch_webhook()) - self.bot.wait_until_ready.assert_called_once() + self.bot.wait_until_guild_available.assert_called_once() self.bot.fetch_webhook.assert_called_once_with(1) self.assertEqual(len(log_watcher.records), 1) -- cgit v1.2.3 From 5024a75004f8d9f4726017af74cace6c1ab6c501 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 4 Jan 2020 10:25:33 -0800 Subject: Sync tests: test instantiation fails without abstract methods --- tests/bot/cogs/sync/test_base.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/bot/cogs/sync/test_base.py (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py new file mode 100644 index 000000000..79ec86fee --- /dev/null +++ b/tests/bot/cogs/sync/test_base.py @@ -0,0 +1,17 @@ +import unittest + +from bot.cogs.sync.syncers import Syncer +from tests import helpers + + +class SyncerBaseTests(unittest.TestCase): + """Tests for the syncer base class.""" + + def setUp(self): + self.bot = helpers.MockBot() + + def test_instantiation_fails_without_abstract_methods(self): + """The class must have abstract methods implemented.""" + with self.assertRaisesRegex(TypeError, "Can't instantiate abstract class"): + Syncer(self.bot) + -- cgit v1.2.3 From c4caf865ce677a8d1d827cbd1107338c251ff90b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 4 Jan 2020 10:27:48 -0800 Subject: Sync tests: create a Syncer subclass for testing --- tests/bot/cogs/sync/test_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 79ec86fee..d38c90410 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -4,11 +4,20 @@ from bot.cogs.sync.syncers import Syncer from tests import helpers +class TestSyncer(Syncer): + """Syncer subclass with mocks for abstract methods for testing purposes.""" + + name = "test" + _get_diff = helpers.AsyncMock() + _sync = helpers.AsyncMock() + + class SyncerBaseTests(unittest.TestCase): """Tests for the syncer base class.""" def setUp(self): self.bot = helpers.MockBot() + self.syncer = TestSyncer(self.bot) def test_instantiation_fails_without_abstract_methods(self): """The class must have abstract methods implemented.""" -- cgit v1.2.3 From 113029aae7625118ac1a5491652f3960172a3605 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 5 Jan 2020 09:41:28 -0800 Subject: Sync tests: test that _send_prompt edits message contents --- tests/bot/cogs/sync/test_base.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index d38c90410..048d6c533 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -1,3 +1,4 @@ +import asyncio import unittest from bot.cogs.sync.syncers import Syncer @@ -24,3 +25,10 @@ class SyncerBaseTests(unittest.TestCase): with self.assertRaisesRegex(TypeError, "Can't instantiate abstract class"): Syncer(self.bot) + def test_send_prompt_edits_message_content(self): + """The contents of the given message should be edited to display the prompt.""" + msg = helpers.MockMessage() + asyncio.run(self.syncer._send_prompt(msg)) + + msg.edit.assert_called_once() + self.assertIn("content", msg.edit.call_args[1]) -- cgit v1.2.3 From e6bb9a79faad03ea7c3a373af84f707722da106f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 5 Jan 2020 10:43:34 -0800 Subject: Sync tests: test that _send_prompt gets channel from cache --- tests/bot/cogs/sync/test_base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 048d6c533..9b177f25c 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -2,6 +2,7 @@ import asyncio import unittest from bot.cogs.sync.syncers import Syncer +from bot import constants from tests import helpers @@ -32,3 +33,13 @@ class SyncerBaseTests(unittest.TestCase): msg.edit.assert_called_once() self.assertIn("content", msg.edit.call_args[1]) + + def test_send_prompt_gets_channel_from_cache(self): + """The dev-core channel should be retrieved from cache if an extant message isn't given.""" + mock_channel = helpers.MockTextChannel() + mock_channel.send.return_value = helpers.MockMessage() + self.bot.get_channel.return_value = mock_channel + + asyncio.run(self.syncer._send_prompt()) + + self.bot.get_channel.assert_called_once_with(constants.Channels.devcore) -- cgit v1.2.3 From a6f01dbd55aef97a39f615348ea22b62a59f2c70 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 5 Jan 2020 10:44:14 -0800 Subject: Sync tests: test _send_prompt fetches channel on a cache miss --- tests/bot/cogs/sync/test_base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 9b177f25c..c18fa5fbb 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -43,3 +43,14 @@ class SyncerBaseTests(unittest.TestCase): asyncio.run(self.syncer._send_prompt()) self.bot.get_channel.assert_called_once_with(constants.Channels.devcore) + + def test_send_prompt_fetches_channel_if_cache_miss(self): + """The dev-core channel should be fetched with an API call if it's not in the cache.""" + self.bot.get_channel.return_value = None + mock_channel = helpers.MockTextChannel() + mock_channel.send.return_value = helpers.MockMessage() + self.bot.fetch_channel.return_value = mock_channel + + asyncio.run(self.syncer._send_prompt()) + + self.bot.fetch_channel.assert_called_once_with(constants.Channels.devcore) -- cgit v1.2.3 From 6f116956395fa1b48233a3014d215a3704b929ed Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 5 Jan 2020 10:44:31 -0800 Subject: Sync tests: test _send_prompt returns None if channel fetch fails --- tests/bot/cogs/sync/test_base.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index c18fa5fbb..8eecea53f 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -1,5 +1,8 @@ import asyncio import unittest +from unittest import mock + +import discord from bot.cogs.sync.syncers import Syncer from bot import constants @@ -54,3 +57,12 @@ class SyncerBaseTests(unittest.TestCase): asyncio.run(self.syncer._send_prompt()) self.bot.fetch_channel.assert_called_once_with(constants.Channels.devcore) + + def test_send_prompt_returns_None_if_channel_fetch_fails(self): + """None should be returned if there's an HTTPException when fetching the channel.""" + self.bot.get_channel.return_value = None + self.bot.fetch_channel.side_effect = discord.HTTPException(mock.MagicMock(), "test error!") + + ret_val = asyncio.run(self.syncer._send_prompt()) + + self.assertIsNone(ret_val) -- cgit v1.2.3 From d57db0b39b52b4660986e90d308434c823428b71 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 5 Jan 2020 11:06:44 -0800 Subject: Sync tests: test _send_prompt sends a new message if one isn't given --- tests/bot/cogs/sync/test_base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 8eecea53f..f4ea33823 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -66,3 +66,14 @@ class SyncerBaseTests(unittest.TestCase): ret_val = asyncio.run(self.syncer._send_prompt()) self.assertIsNone(ret_val) + + def test_send_prompt_sends_new_message_if_not_given(self): + """A new message that mentions core devs should be sent if an extant message isn't given.""" + mock_channel = helpers.MockTextChannel() + mock_channel.send.return_value = helpers.MockMessage() + self.bot.get_channel.return_value = mock_channel + + asyncio.run(self.syncer._send_prompt()) + + mock_channel.send.assert_called_once() + self.assertIn(self.syncer._CORE_DEV_MENTION, mock_channel.send.call_args[0][0]) -- cgit v1.2.3 From 3298312ad182dd1a8a5c9596d7bdc1d6f4905ebf Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 5 Jan 2020 11:23:55 -0800 Subject: Sync tests: test _send_prompt adds reactions --- tests/bot/cogs/sync/test_base.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index f4ea33823..e509b3c98 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -77,3 +77,11 @@ class SyncerBaseTests(unittest.TestCase): mock_channel.send.assert_called_once() self.assertIn(self.syncer._CORE_DEV_MENTION, mock_channel.send.call_args[0][0]) + + def test_send_prompt_adds_reactions(self): + """The message should have reactions for confirmation added.""" + msg = helpers.MockMessage() + asyncio.run(self.syncer._send_prompt(msg)) + + calls = [mock.call(emoji) for emoji in self.syncer._REACTION_EMOJIS] + msg.add_reaction.assert_has_calls(calls) -- cgit v1.2.3 From 0fdd675f5bc85a20268e257e073d9605126ee322 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 7 Jan 2020 09:56:34 -0800 Subject: Sync tests: add fixtures to mock dev core channel get and fetch --- tests/bot/cogs/sync/test_base.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index e509b3c98..2c6857246 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -24,6 +24,27 @@ class SyncerBaseTests(unittest.TestCase): self.bot = helpers.MockBot() self.syncer = TestSyncer(self.bot) + def mock_dev_core_channel(self): + """Fixture to return a mock channel and message for when `get_channel` is used.""" + mock_channel = helpers.MockTextChannel() + mock_message = helpers.MockMessage() + + mock_channel.send.return_value = mock_message + self.bot.get_channel.return_value = mock_channel + + return mock_channel, mock_message + + def mock_dev_core_channel_cache_miss(self): + """Fixture to return a mock channel and message for when `fetch_channel` is used.""" + mock_channel = helpers.MockTextChannel() + mock_message = helpers.MockMessage() + + self.bot.get_channel.return_value = None + mock_channel.send.return_value = mock_message + self.bot.fetch_channel.return_value = mock_channel + + return mock_channel, mock_message + def test_instantiation_fails_without_abstract_methods(self): """The class must have abstract methods implemented.""" with self.assertRaisesRegex(TypeError, "Can't instantiate abstract class"): -- cgit v1.2.3 From 7d3b46741cfd12d2f8cc40107464f7b3210b9af5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 7 Jan 2020 10:39:20 -0800 Subject: Sync tests: reset mocks in channel fixtures --- tests/bot/cogs/sync/test_base.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 2c6857246..ff67eb334 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -26,6 +26,8 @@ class SyncerBaseTests(unittest.TestCase): def mock_dev_core_channel(self): """Fixture to return a mock channel and message for when `get_channel` is used.""" + self.bot.reset_mock() + mock_channel = helpers.MockTextChannel() mock_message = helpers.MockMessage() @@ -36,6 +38,8 @@ class SyncerBaseTests(unittest.TestCase): def mock_dev_core_channel_cache_miss(self): """Fixture to return a mock channel and message for when `fetch_channel` is used.""" + self.bot.reset_mock() + mock_channel = helpers.MockTextChannel() mock_message = helpers.MockMessage() -- cgit v1.2.3 From 7215a9483cb9ebae89d147f950dd62996d86beeb Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 7 Jan 2020 10:45:02 -0800 Subject: Sync tests: rename channel fixtures --- tests/bot/cogs/sync/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index ff67eb334..1d61f8cb2 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -24,7 +24,7 @@ class SyncerBaseTests(unittest.TestCase): self.bot = helpers.MockBot() self.syncer = TestSyncer(self.bot) - def mock_dev_core_channel(self): + def mock_get_channel(self): """Fixture to return a mock channel and message for when `get_channel` is used.""" self.bot.reset_mock() @@ -36,7 +36,7 @@ class SyncerBaseTests(unittest.TestCase): return mock_channel, mock_message - def mock_dev_core_channel_cache_miss(self): + def mock_fetch_channel(self): """Fixture to return a mock channel and message for when `fetch_channel` is used.""" self.bot.reset_mock() -- cgit v1.2.3 From 1cef637f4d53ba1a093403f4e237e6004330cc1d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 7 Jan 2020 10:46:16 -0800 Subject: Sync tests: use channel fixtures with subtests * Merge test_send_prompt_fetches_channel_if_cache_miss into test_send_prompt_gets_channel_from_cache * Rename test_send_prompt_gets_channel_from_cache * Test test_send_prompt_sends_new_message_if_not_given with fetch_channel too --- tests/bot/cogs/sync/test_base.py | 42 ++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 25 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 1d61f8cb2..d46965738 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -62,26 +62,19 @@ class SyncerBaseTests(unittest.TestCase): msg.edit.assert_called_once() self.assertIn("content", msg.edit.call_args[1]) - def test_send_prompt_gets_channel_from_cache(self): - """The dev-core channel should be retrieved from cache if an extant message isn't given.""" - mock_channel = helpers.MockTextChannel() - mock_channel.send.return_value = helpers.MockMessage() - self.bot.get_channel.return_value = mock_channel - - asyncio.run(self.syncer._send_prompt()) + def test_send_prompt_gets_dev_core_channel(self): + """The dev-core channel should be retrieved if an extant message isn't given.""" + subtests = ( + (self.bot.get_channel, self.mock_get_channel), + (self.bot.fetch_channel, self.mock_fetch_channel), + ) - self.bot.get_channel.assert_called_once_with(constants.Channels.devcore) + for method, mock_ in subtests: + with self.subTest(method=method, msg=mock_.__name__): + mock_() + asyncio.run(self.syncer._send_prompt()) - def test_send_prompt_fetches_channel_if_cache_miss(self): - """The dev-core channel should be fetched with an API call if it's not in the cache.""" - self.bot.get_channel.return_value = None - mock_channel = helpers.MockTextChannel() - mock_channel.send.return_value = helpers.MockMessage() - self.bot.fetch_channel.return_value = mock_channel - - asyncio.run(self.syncer._send_prompt()) - - self.bot.fetch_channel.assert_called_once_with(constants.Channels.devcore) + method.assert_called_once_with(constants.Channels.devcore) def test_send_prompt_returns_None_if_channel_fetch_fails(self): """None should be returned if there's an HTTPException when fetching the channel.""" @@ -94,14 +87,13 @@ class SyncerBaseTests(unittest.TestCase): def test_send_prompt_sends_new_message_if_not_given(self): """A new message that mentions core devs should be sent if an extant message isn't given.""" - mock_channel = helpers.MockTextChannel() - mock_channel.send.return_value = helpers.MockMessage() - self.bot.get_channel.return_value = mock_channel - - asyncio.run(self.syncer._send_prompt()) + for mock_ in (self.mock_get_channel, self.mock_fetch_channel): + with self.subTest(msg=mock_.__name__): + mock_channel, _ = mock_() + asyncio.run(self.syncer._send_prompt()) - mock_channel.send.assert_called_once() - self.assertIn(self.syncer._CORE_DEV_MENTION, mock_channel.send.call_args[0][0]) + mock_channel.send.assert_called_once() + self.assertIn(self.syncer._CORE_DEV_MENTION, mock_channel.send.call_args[0][0]) def test_send_prompt_adds_reactions(self): """The message should have reactions for confirmation added.""" -- cgit v1.2.3 From cc8ecb9fd52b24e323c4e6f5ce8a2ddcc8d31777 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 7 Jan 2020 11:18:35 -0800 Subject: Sync tests: use channel fixtures with subtests in add reaction test --- tests/bot/cogs/sync/test_base.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index d46965738..e0a3f4127 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -97,8 +97,19 @@ class SyncerBaseTests(unittest.TestCase): def test_send_prompt_adds_reactions(self): """The message should have reactions for confirmation added.""" - msg = helpers.MockMessage() - asyncio.run(self.syncer._send_prompt(msg)) + extant_message = helpers.MockMessage() + subtests = ( + (extant_message, lambda: (None, extant_message)), + (None, self.mock_get_channel), + (None, self.mock_fetch_channel), + ) + + for message_arg, mock_ in subtests: + subtest_msg = "Extant message" if mock_.__name__ == "" else mock_.__name__ + + with self.subTest(msg=subtest_msg): + _, mock_message = mock_() + asyncio.run(self.syncer._send_prompt(message_arg)) - calls = [mock.call(emoji) for emoji in self.syncer._REACTION_EMOJIS] - msg.add_reaction.assert_has_calls(calls) + calls = [mock.call(emoji) for emoji in self.syncer._REACTION_EMOJIS] + mock_message.add_reaction.assert_has_calls(calls) -- cgit v1.2.3 From 04dbf347e08d4e2a3690e59a537ab73544c82be6 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 7 Jan 2020 11:34:05 -0800 Subject: Sync tests: test the return value of _send_prompt --- tests/bot/cogs/sync/test_base.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index e0a3f4127..4c3eae1b3 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -54,13 +54,14 @@ class SyncerBaseTests(unittest.TestCase): with self.assertRaisesRegex(TypeError, "Can't instantiate abstract class"): Syncer(self.bot) - def test_send_prompt_edits_message_content(self): - """The contents of the given message should be edited to display the prompt.""" + def test_send_prompt_edits_and_returns_message(self): + """The given message should be edited to display the prompt and then should be returned.""" msg = helpers.MockMessage() - asyncio.run(self.syncer._send_prompt(msg)) + ret_val = asyncio.run(self.syncer._send_prompt(msg)) msg.edit.assert_called_once() self.assertIn("content", msg.edit.call_args[1]) + self.assertEqual(ret_val, msg) def test_send_prompt_gets_dev_core_channel(self): """The dev-core channel should be retrieved if an extant message isn't given.""" @@ -85,15 +86,16 @@ class SyncerBaseTests(unittest.TestCase): self.assertIsNone(ret_val) - def test_send_prompt_sends_new_message_if_not_given(self): - """A new message that mentions core devs should be sent if an extant message isn't given.""" + def test_send_prompt_sends_and_returns_new_message_if_not_given(self): + """A new message mentioning core devs should be sent and returned if message isn't given.""" for mock_ in (self.mock_get_channel, self.mock_fetch_channel): with self.subTest(msg=mock_.__name__): - mock_channel, _ = mock_() - asyncio.run(self.syncer._send_prompt()) + mock_channel, mock_message = mock_() + ret_val = asyncio.run(self.syncer._send_prompt()) mock_channel.send.assert_called_once() self.assertIn(self.syncer._CORE_DEV_MENTION, mock_channel.send.call_args[0][0]) + self.assertEqual(ret_val, mock_message) def test_send_prompt_adds_reactions(self): """The message should have reactions for confirmation added.""" -- cgit v1.2.3 From d020e5ebaf72448b015351b550ea3c82bde3c61f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 8 Jan 2020 16:42:01 -0800 Subject: Sync tests: create a separate test case for _send_prompt tests --- tests/bot/cogs/sync/test_base.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 4c3eae1b3..af15b544b 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -20,6 +20,18 @@ class TestSyncer(Syncer): class SyncerBaseTests(unittest.TestCase): """Tests for the syncer base class.""" + def setUp(self): + self.bot = helpers.MockBot() + + def test_instantiation_fails_without_abstract_methods(self): + """The class must have abstract methods implemented.""" + with self.assertRaisesRegex(TypeError, "Can't instantiate abstract class"): + Syncer(self.bot) + + +class SyncerSendPromptTests(unittest.TestCase): + """Tests for sending the sync confirmation prompt.""" + def setUp(self): self.bot = helpers.MockBot() self.syncer = TestSyncer(self.bot) @@ -49,11 +61,6 @@ class SyncerBaseTests(unittest.TestCase): return mock_channel, mock_message - def test_instantiation_fails_without_abstract_methods(self): - """The class must have abstract methods implemented.""" - with self.assertRaisesRegex(TypeError, "Can't instantiate abstract class"): - Syncer(self.bot) - def test_send_prompt_edits_and_returns_message(self): """The given message should be edited to display the prompt and then should be returned.""" msg = helpers.MockMessage() -- cgit v1.2.3 From b43b0bc611a0ba7d7ee62bc94a11ac661772f3ca Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 10 Jan 2020 11:46:18 -0800 Subject: Sync tests: create a test suite for confirmation tests --- tests/bot/cogs/sync/test_base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index af15b544b..ca344c865 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -4,8 +4,8 @@ from unittest import mock import discord -from bot.cogs.sync.syncers import Syncer from bot import constants +from bot.cogs.sync.syncers import Syncer from tests import helpers @@ -122,3 +122,11 @@ class SyncerSendPromptTests(unittest.TestCase): calls = [mock.call(emoji) for emoji in self.syncer._REACTION_EMOJIS] mock_message.add_reaction.assert_has_calls(calls) + + +class SyncerConfirmationTests(unittest.TestCase): + """Tests for waiting for a sync confirmation reaction on the prompt.""" + + def setUp(self): + self.bot = helpers.MockBot() + self.syncer = TestSyncer(self.bot) -- cgit v1.2.3 From 9a73feb93a7680211e597f0cc9d09b06ebc84335 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 10 Jan 2020 11:47:41 -0800 Subject: Sync tests: test _reaction_check for valid emoji and authors Should return True if authors are identical or are a bot and a core dev, respectively. * Create a mock core dev role in the setup fixture * Create a fixture to create a mock message and reaction from an emoji --- tests/bot/cogs/sync/test_base.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index ca344c865..f722a83e8 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -130,3 +130,30 @@ class SyncerConfirmationTests(unittest.TestCase): def setUp(self): self.bot = helpers.MockBot() self.syncer = TestSyncer(self.bot) + self.core_dev_role = helpers.MockRole(id=constants.Roles.core_developer) + + @staticmethod + def get_message_reaction(emoji): + """Fixture to return a mock message an reaction from the given `emoji`.""" + message = helpers.MockMessage() + reaction = helpers.MockReaction(emoji=emoji, message=message) + + return message, reaction + + def test_reaction_check_for_valid_emoji_and_authors(self): + """Should return True if authors are identical or are a bot and a core dev, respectively.""" + user_subtests = ( + (helpers.MockMember(id=77), helpers.MockMember(id=77)), + ( + helpers.MockMember(id=77, bot=True), + helpers.MockMember(id=43, roles=[self.core_dev_role]), + ) + ) + + for emoji in self.syncer._REACTION_EMOJIS: + for author, user in user_subtests: + with self.subTest(author=author, user=user, emoji=emoji): + message, reaction = self.get_message_reaction(emoji) + ret_val = self.syncer._reaction_check(author, message, reaction, user) + + self.assertTrue(ret_val) -- cgit v1.2.3 From 7ac2d59c485cddc37ef3fd7ebe175cf5bef784fd Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 13 Jan 2020 09:18:43 -0800 Subject: Sync tests: test _reaction_check for invalid reactions Should return False for invalid reaction events. --- tests/bot/cogs/sync/test_base.py | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index f722a83e8..43d72dda9 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -157,3 +157,46 @@ class SyncerConfirmationTests(unittest.TestCase): ret_val = self.syncer._reaction_check(author, message, reaction, user) self.assertTrue(ret_val) + + def test_reaction_check_for_invalid_reactions(self): + """Should return False for invalid reaction events.""" + valid_emoji = self.syncer._REACTION_EMOJIS[0] + subtests = ( + ( + helpers.MockMember(id=77), + *self.get_message_reaction(valid_emoji), + helpers.MockMember(id=43, roles=[self.core_dev_role]), + "users are not identical", + ), + ( + helpers.MockMember(id=77, bot=True), + *self.get_message_reaction(valid_emoji), + helpers.MockMember(id=43), + "reactor lacks the core-dev role", + ), + ( + helpers.MockMember(id=77, bot=True, roles=[self.core_dev_role]), + *self.get_message_reaction(valid_emoji), + helpers.MockMember(id=77, bot=True, roles=[self.core_dev_role]), + "reactor is a bot", + ), + ( + helpers.MockMember(id=77), + helpers.MockMessage(id=95), + helpers.MockReaction(emoji=valid_emoji, message=helpers.MockMessage(id=26)), + helpers.MockMember(id=77), + "messages are not identical", + ), + ( + helpers.MockMember(id=77), + *self.get_message_reaction("InVaLiD"), + helpers.MockMember(id=77), + "emoji is invalid", + ), + ) + + for *args, msg in subtests: + kwargs = dict(zip(("author", "message", "reaction", "user"), args)) + with self.subTest(**kwargs, msg=msg): + ret_val = self.syncer._reaction_check(*args) + self.assertFalse(ret_val) -- cgit v1.2.3 From ad402f5bc8f4db6b97f197fdb518a1b3e7f95eb5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 13 Jan 2020 09:55:45 -0800 Subject: Sync tests: add messages to _reaction_check subtests The message will be displayed by the test runner when a subtest fails. --- tests/bot/cogs/sync/test_base.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 43d72dda9..2d682faad 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -143,16 +143,21 @@ class SyncerConfirmationTests(unittest.TestCase): def test_reaction_check_for_valid_emoji_and_authors(self): """Should return True if authors are identical or are a bot and a core dev, respectively.""" user_subtests = ( - (helpers.MockMember(id=77), helpers.MockMember(id=77)), + ( + helpers.MockMember(id=77), + helpers.MockMember(id=77), + "identical users", + ), ( helpers.MockMember(id=77, bot=True), helpers.MockMember(id=43, roles=[self.core_dev_role]), - ) + "bot author and core-dev reactor", + ), ) for emoji in self.syncer._REACTION_EMOJIS: - for author, user in user_subtests: - with self.subTest(author=author, user=user, emoji=emoji): + for author, user, msg in user_subtests: + with self.subTest(author=author, user=user, emoji=emoji, msg=msg): message, reaction = self.get_message_reaction(emoji) ret_val = self.syncer._reaction_check(author, message, reaction, user) -- cgit v1.2.3 From 745c9d15114f90d01f8c21e30c2c40335c199a9e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 14 Jan 2020 10:18:18 -0800 Subject: Tests: add a return value for MockReaction.__str__ --- tests/helpers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'tests') diff --git a/tests/helpers.py b/tests/helpers.py index 71b80a223..b18a27ebe 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -521,6 +521,7 @@ class MockReaction(CustomMockMixin, unittest.mock.MagicMock): self.emoji = kwargs.get('emoji', MockEmoji()) self.message = kwargs.get('message', MockMessage()) self.users = AsyncIteratorMock(kwargs.get('users', [])) + self.__str__.return_value = str(self.emoji) webhook_instance = discord.Webhook(data=unittest.mock.MagicMock(), adapter=unittest.mock.MagicMock()) -- cgit v1.2.3 From 792e7d4bc71ffd7aa6087097b8276a6833c28b90 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 14 Jan 2020 10:27:02 -0800 Subject: Sync tests: test _wait_for_confirmation The message should always be edited and only return True if the emoji is a check mark. --- tests/bot/cogs/sync/test_base.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 2d682faad..d9f9c6d98 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -205,3 +205,41 @@ class SyncerConfirmationTests(unittest.TestCase): with self.subTest(**kwargs, msg=msg): ret_val = self.syncer._reaction_check(*args) self.assertFalse(ret_val) + + def test_wait_for_confirmation(self): + """The message should always be edited and only return True if the emoji is a check mark.""" + subtests = ( + (constants.Emojis.check_mark, True, None), + ("InVaLiD", False, None), + (None, False, TimeoutError), + ) + + for emoji, ret_val, side_effect in subtests: + for bot in (True, False): + with self.subTest(emoji=emoji, ret_val=ret_val, side_effect=side_effect, bot=bot): + # Set up mocks + message = helpers.MockMessage() + member = helpers.MockMember(bot=bot) + + self.bot.wait_for.reset_mock() + self.bot.wait_for.return_value = (helpers.MockReaction(emoji=emoji), None) + self.bot.wait_for.side_effect = side_effect + + # Call the function + actual_return = asyncio.run(self.syncer._wait_for_confirmation(member, message)) + + # Perform assertions + self.bot.wait_for.assert_called_once() + self.assertIn("reaction_add", self.bot.wait_for.call_args[0]) + + message.edit.assert_called_once() + kwargs = message.edit.call_args[1] + self.assertIn("content", kwargs) + + # Core devs should only be mentioned if the author is a bot. + if bot: + self.assertIn(self.syncer._CORE_DEV_MENTION, kwargs["content"]) + else: + self.assertNotIn(self.syncer._CORE_DEV_MENTION, kwargs["content"]) + + self.assertIs(actual_return, ret_val) -- cgit v1.2.3 From 555d1f47d75afbaaae2758fac8460d8d6af65d61 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 15 Jan 2020 10:53:38 -0800 Subject: Sync tests: test sync with an empty diff A confirmation prompt should not be sent if the diff is too small. --- tests/bot/cogs/sync/test_base.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index d9f9c6d98..642be75eb 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -5,7 +5,7 @@ from unittest import mock import discord from bot import constants -from bot.cogs.sync.syncers import Syncer +from bot.cogs.sync.syncers import Syncer, _Diff from tests import helpers @@ -243,3 +243,27 @@ class SyncerConfirmationTests(unittest.TestCase): self.assertNotIn(self.syncer._CORE_DEV_MENTION, kwargs["content"]) self.assertIs(actual_return, ret_val) + + +class SyncerSyncTests(unittest.TestCase): + """Tests for main function orchestrating the sync.""" + + def setUp(self): + self.bot = helpers.MockBot() + self.syncer = TestSyncer(self.bot) + + def test_sync_with_empty_diff(self): + """A confirmation prompt should not be sent if the diff is too small.""" + guild = helpers.MockGuild() + diff = _Diff(set(), set(), set()) + + self.syncer._send_prompt = helpers.AsyncMock() + self.syncer._wait_for_confirmation = helpers.AsyncMock() + self.syncer._get_diff.return_value = diff + + asyncio.run(self.syncer.sync(guild)) + + self.syncer._get_diff.assert_called_once_with(guild) + self.syncer._send_prompt.assert_not_called() + self.syncer._wait_for_confirmation.assert_not_called() + self.syncer._sync.assert_called_once_with(diff) -- cgit v1.2.3 From a7ba405732e28e8c44e7ddedce8136f6319980b0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 17 Jan 2020 10:43:51 -0800 Subject: Sync tests: test sync sends a confirmation prompt The prompt should be sent only if the diff is large and should fail if not confirmed. The empty diff test was integrated into this new test. --- tests/bot/cogs/sync/test_base.py | 48 ++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 12 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 642be75eb..898b12b07 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -252,18 +252,42 @@ class SyncerSyncTests(unittest.TestCase): self.bot = helpers.MockBot() self.syncer = TestSyncer(self.bot) - def test_sync_with_empty_diff(self): - """A confirmation prompt should not be sent if the diff is too small.""" - guild = helpers.MockGuild() - diff = _Diff(set(), set(), set()) + def test_sync_sends_confirmation_prompt(self): + """The prompt should be sent only if the diff is large and should fail if not confirmed.""" + large_diff = _Diff({1}, {2}, {3}) + subtests = ( + (False, False, True, None, None, _Diff({1}, {2}, set()), "diff too small"), + (True, True, True, helpers.MockMessage(), True, large_diff, "confirmed"), + (True, False, False, None, None, large_diff, "couldn't get channel"), + (True, True, False, helpers.MockMessage(), False, large_diff, "not confirmed"), + ) + + for prompt_called, wait_called, sync_called, prompt_msg, confirmed, diff, msg in subtests: + with self.subTest(msg=msg): + self.syncer._sync.reset_mock() + self.syncer._get_diff.reset_mock() + + self.syncer.MAX_DIFF = 2 + self.syncer._get_diff.return_value = diff + self.syncer._send_prompt = helpers.AsyncMock(return_value=prompt_msg) + self.syncer._wait_for_confirmation = helpers.AsyncMock(return_value=confirmed) + + guild = helpers.MockGuild() + asyncio.run(self.syncer.sync(guild)) + + self.syncer._get_diff.assert_called_once_with(guild) - self.syncer._send_prompt = helpers.AsyncMock() - self.syncer._wait_for_confirmation = helpers.AsyncMock() - self.syncer._get_diff.return_value = diff + if prompt_called: + self.syncer._send_prompt.assert_called_once() + else: + self.syncer._send_prompt.assert_not_called() - asyncio.run(self.syncer.sync(guild)) + if wait_called: + self.syncer._wait_for_confirmation.assert_called_once() + else: + self.syncer._wait_for_confirmation.assert_not_called() - self.syncer._get_diff.assert_called_once_with(guild) - self.syncer._send_prompt.assert_not_called() - self.syncer._wait_for_confirmation.assert_not_called() - self.syncer._sync.assert_called_once_with(diff) + if sync_called: + self.syncer._sync.assert_called_once_with(diff) + else: + self.syncer._sync.assert_not_called() -- cgit v1.2.3 From 08ad97d24590882dbb6a5575b6a3e7bfdbf145a3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 18 Jan 2020 09:18:18 -0800 Subject: Sync tests: adjust sync test to account for _get_confirmation_result --- tests/bot/cogs/sync/test_base.py | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 898b12b07..f82984157 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -252,42 +252,32 @@ class SyncerSyncTests(unittest.TestCase): self.bot = helpers.MockBot() self.syncer = TestSyncer(self.bot) - def test_sync_sends_confirmation_prompt(self): - """The prompt should be sent only if the diff is large and should fail if not confirmed.""" - large_diff = _Diff({1}, {2}, {3}) + def test_sync_respects_confirmation_result(self): + """The sync should abort if confirmation fails and continue if confirmed.""" + mock_message = helpers.MockMessage() subtests = ( - (False, False, True, None, None, _Diff({1}, {2}, set()), "diff too small"), - (True, True, True, helpers.MockMessage(), True, large_diff, "confirmed"), - (True, False, False, None, None, large_diff, "couldn't get channel"), - (True, True, False, helpers.MockMessage(), False, large_diff, "not confirmed"), + (True, mock_message), + (False, None), ) - for prompt_called, wait_called, sync_called, prompt_msg, confirmed, diff, msg in subtests: - with self.subTest(msg=msg): + for confirmed, message in subtests: + with self.subTest(confirmed=confirmed): self.syncer._sync.reset_mock() self.syncer._get_diff.reset_mock() - self.syncer.MAX_DIFF = 2 + diff = _Diff({1, 2, 3}, {4, 5}, None) self.syncer._get_diff.return_value = diff - self.syncer._send_prompt = helpers.AsyncMock(return_value=prompt_msg) - self.syncer._wait_for_confirmation = helpers.AsyncMock(return_value=confirmed) + self.syncer._get_confirmation_result = helpers.AsyncMock( + return_value=(confirmed, message) + ) guild = helpers.MockGuild() asyncio.run(self.syncer.sync(guild)) self.syncer._get_diff.assert_called_once_with(guild) + self.syncer._get_confirmation_result.assert_called_once() - if prompt_called: - self.syncer._send_prompt.assert_called_once() - else: - self.syncer._send_prompt.assert_not_called() - - if wait_called: - self.syncer._wait_for_confirmation.assert_called_once() - else: - self.syncer._wait_for_confirmation.assert_not_called() - - if sync_called: + if confirmed: self.syncer._sync.assert_called_once_with(diff) else: self.syncer._sync.assert_not_called() -- cgit v1.2.3 From 8fab4db24939d6d7dd9256c0faf13395e7caddb7 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 18 Jan 2020 09:33:40 -0800 Subject: Sync tests: test diff size calculation --- tests/bot/cogs/sync/test_base.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index f82984157..6d784d0de 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -281,3 +281,25 @@ class SyncerSyncTests(unittest.TestCase): self.syncer._sync.assert_called_once_with(diff) else: self.syncer._sync.assert_not_called() + + def test_sync_diff_size(self): + """The diff size should be correctly calculated.""" + subtests = ( + (6, _Diff({1, 2}, {3, 4}, {5, 6})), + (5, _Diff({1, 2, 3}, None, {4, 5})), + (0, _Diff(None, None, None)), + (0, _Diff(set(), set(), set())), + ) + + for size, diff in subtests: + with self.subTest(size=size, diff=diff): + self.syncer._get_diff.reset_mock() + self.syncer._get_diff.return_value = diff + self.syncer._get_confirmation_result = helpers.AsyncMock(return_value=(False, None)) + + guild = helpers.MockGuild() + asyncio.run(self.syncer.sync(guild)) + + self.syncer._get_diff.assert_called_once_with(guild) + self.syncer._get_confirmation_result.assert_called_once() + self.assertEqual(self.syncer._get_confirmation_result.call_args[0][0], size) -- cgit v1.2.3 From 7692d506454d5aa125135eac17ed291cc160ef2b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 20 Jan 2020 09:24:46 -0800 Subject: Sync tests: test sync edits the message if one was sent --- tests/bot/cogs/sync/test_base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 6d784d0de..ae8e53ffa 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -303,3 +303,18 @@ class SyncerSyncTests(unittest.TestCase): self.syncer._get_diff.assert_called_once_with(guild) self.syncer._get_confirmation_result.assert_called_once() self.assertEqual(self.syncer._get_confirmation_result.call_args[0][0], size) + + def test_sync_message_edited(self): + """The message should be edited if one was sent.""" + for message in (helpers.MockMessage(), None): + with self.subTest(message=message): + self.syncer._get_confirmation_result = helpers.AsyncMock( + return_value=(True, message) + ) + + guild = helpers.MockGuild() + asyncio.run(self.syncer.sync(guild)) + + if message is not None: + message.edit.assert_called_once() + self.assertIn("content", message.edit.call_args[1]) -- cgit v1.2.3 From bf22311fb844c7122f2af9b3a51d9c25382fc452 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 21 Jan 2020 17:00:09 -0800 Subject: Sync tests: test sync passes correct author for confirmation Author should be the bot or the ctx author, if a ctx is given. --- tests/bot/cogs/sync/test_base.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index ae8e53ffa..dfc8320d2 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -249,7 +249,7 @@ class SyncerSyncTests(unittest.TestCase): """Tests for main function orchestrating the sync.""" def setUp(self): - self.bot = helpers.MockBot() + self.bot = helpers.MockBot(user=helpers.MockMember(bot=True)) self.syncer = TestSyncer(self.bot) def test_sync_respects_confirmation_result(self): @@ -318,3 +318,21 @@ class SyncerSyncTests(unittest.TestCase): if message is not None: message.edit.assert_called_once() self.assertIn("content", message.edit.call_args[1]) + + def test_sync_confirmation_author(self): + """Author should be the bot or the ctx author, if a ctx is given.""" + mock_member = helpers.MockMember() + subtests = ( + (None, self.bot.user), + (helpers.MockContext(author=mock_member), mock_member), + ) + + for ctx, author in subtests: + with self.subTest(ctx=ctx, author=author): + self.syncer._get_confirmation_result = helpers.AsyncMock(return_value=(False, None)) + + guild = helpers.MockGuild() + asyncio.run(self.syncer.sync(guild, ctx)) + + self.syncer._get_confirmation_result.assert_called_once() + self.assertEqual(self.syncer._get_confirmation_result.call_args[0][1], author) -- cgit v1.2.3 From eaf44846fd8eaee3f52ca1d8b2f146655298b488 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 22 Jan 2020 18:07:29 -0800 Subject: Sync tests: test sync redirects confirmation message to given context If ctx is given, a new message should be sent and author should be ctx's author. test_sync_confirmation_author was re-worked to include a test for the message being sent and passed. --- tests/bot/cogs/sync/test_base.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index dfc8320d2..a2df3e24e 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -319,20 +319,27 @@ class SyncerSyncTests(unittest.TestCase): message.edit.assert_called_once() self.assertIn("content", message.edit.call_args[1]) - def test_sync_confirmation_author(self): - """Author should be the bot or the ctx author, if a ctx is given.""" + def test_sync_confirmation_context_redirect(self): + """If ctx is given, a new message should be sent and author should be ctx's author.""" mock_member = helpers.MockMember() subtests = ( - (None, self.bot.user), - (helpers.MockContext(author=mock_member), mock_member), + (None, self.bot.user, None), + (helpers.MockContext(author=mock_member), mock_member, helpers.MockMessage()), ) - for ctx, author in subtests: - with self.subTest(ctx=ctx, author=author): + for ctx, author, message in subtests: + with self.subTest(ctx=ctx, author=author, message=message): + if ctx is not None: + ctx.send.return_value = message + self.syncer._get_confirmation_result = helpers.AsyncMock(return_value=(False, None)) guild = helpers.MockGuild() asyncio.run(self.syncer.sync(guild, ctx)) + if ctx is not None: + ctx.send.assert_called_once() + self.syncer._get_confirmation_result.assert_called_once() self.assertEqual(self.syncer._get_confirmation_result.call_args[0][1], author) + self.assertEqual(self.syncer._get_confirmation_result.call_args[0][2], message) -- cgit v1.2.3 From 879ada59bf0a17f5cbf2590a7eb2426825b3635e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 23 Jan 2020 13:35:36 -0800 Subject: Sync tests: test sync edits message even if there's an API error --- tests/bot/cogs/sync/test_base.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index a2df3e24e..314f8a70c 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -5,6 +5,7 @@ from unittest import mock import discord from bot import constants +from bot.api import ResponseCodeError from bot.cogs.sync.syncers import Syncer, _Diff from tests import helpers @@ -305,9 +306,16 @@ class SyncerSyncTests(unittest.TestCase): self.assertEqual(self.syncer._get_confirmation_result.call_args[0][0], size) def test_sync_message_edited(self): - """The message should be edited if one was sent.""" - for message in (helpers.MockMessage(), None): - with self.subTest(message=message): + """The message should be edited if one was sent, even if the sync has an API error.""" + subtests = ( + (None, None, False), + (helpers.MockMessage(), None, True), + (helpers.MockMessage(), ResponseCodeError(mock.MagicMock()), True), + ) + + for message, side_effect, should_edit in subtests: + with self.subTest(message=message, side_effect=side_effect, should_edit=should_edit): + self.syncer._sync.side_effect = side_effect self.syncer._get_confirmation_result = helpers.AsyncMock( return_value=(True, message) ) @@ -315,7 +323,7 @@ class SyncerSyncTests(unittest.TestCase): guild = helpers.MockGuild() asyncio.run(self.syncer.sync(guild)) - if message is not None: + if should_edit: message.edit.assert_called_once() self.assertIn("content", message.edit.call_args[1]) -- cgit v1.2.3 From dfd4ca2bf4d4b8717a648d3f291cc3daeeb762d4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 24 Jan 2020 18:32:10 -0800 Subject: Sync tests: test _get_confirmation_result for small diffs Should always return True and the given message if the diff size is too small. --- tests/bot/cogs/sync/test_base.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 314f8a70c..21f14f89a 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -351,3 +351,22 @@ class SyncerSyncTests(unittest.TestCase): self.syncer._get_confirmation_result.assert_called_once() self.assertEqual(self.syncer._get_confirmation_result.call_args[0][1], author) self.assertEqual(self.syncer._get_confirmation_result.call_args[0][2], message) + + def test_confirmation_result_small_diff(self): + """Should always return True and the given message if the diff size is too small.""" + self.syncer.MAX_DIFF = 3 + author = helpers.MockMember() + expected_message = helpers.MockMessage() + + for size in (3, 2): + with self.subTest(size=size): + self.syncer._send_prompt = helpers.AsyncMock() + self.syncer._wait_for_confirmation = helpers.AsyncMock() + + coro = self.syncer._get_confirmation_result(size, author, expected_message) + result, actual_message = asyncio.run(coro) + + self.assertTrue(result) + self.assertEqual(actual_message, expected_message) + self.syncer._send_prompt.assert_not_called() + self.syncer._wait_for_confirmation.assert_not_called() -- cgit v1.2.3 From 4385422fc0f64cb592a9bb1d5815cc91a0ca09a0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 24 Jan 2020 18:48:40 -0800 Subject: Sync tests: test _get_confirmation_result for large diffs Should return True if confirmed and False if _send_prompt fails or aborted. --- tests/bot/cogs/sync/test_base.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 21f14f89a..ff11d911e 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -370,3 +370,32 @@ class SyncerSyncTests(unittest.TestCase): self.assertEqual(actual_message, expected_message) self.syncer._send_prompt.assert_not_called() self.syncer._wait_for_confirmation.assert_not_called() + + def test_confirmation_result_large_diff(self): + """Should return True if confirmed and False if _send_prompt fails or aborted.""" + self.syncer.MAX_DIFF = 3 + author = helpers.MockMember() + mock_message = helpers.MockMessage() + + subtests = ( + (True, mock_message, True, "confirmed"), + (False, None, False, "_send_prompt failed"), + (False, mock_message, False, "aborted"), + ) + + for expected_result, expected_message, confirmed, msg in subtests: + with self.subTest(msg=msg): + self.syncer._send_prompt = helpers.AsyncMock(return_value=expected_message) + self.syncer._wait_for_confirmation = helpers.AsyncMock(return_value=confirmed) + + coro = self.syncer._get_confirmation_result(4, author) + actual_result, actual_message = asyncio.run(coro) + + self.syncer._send_prompt.assert_called_once_with(None) # message defaults to None + self.assertIs(actual_result, expected_result) + self.assertEqual(actual_message, expected_message) + + if expected_message: + self.syncer._wait_for_confirmation.assert_called_once_with( + author, expected_message + ) -- cgit v1.2.3 From 69f59078394193f615753b0a20d74982e58d5c0f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 26 Jan 2020 18:27:06 -0800 Subject: Sync tests: test the extension setup The Sync cog should be added. --- tests/bot/cogs/sync/test_cog.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/bot/cogs/sync/test_cog.py (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py new file mode 100644 index 000000000..fb0f044b0 --- /dev/null +++ b/tests/bot/cogs/sync/test_cog.py @@ -0,0 +1,15 @@ +import unittest + +from bot.cogs import sync +from tests import helpers + + +class SyncExtensionTests(unittest.TestCase): + """Tests for the sync extension.""" + + @staticmethod + def test_extension_setup(): + """The Sync cog should be added.""" + bot = helpers.MockBot() + sync.setup(bot) + bot.add_cog.assert_called_once() -- cgit v1.2.3 From 32048b12d98d3b04a336ae53e12b81681a51e72a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 27 Jan 2020 22:00:11 -0800 Subject: Sync tests: test Sync cog __init__ Should instantiate syncers and run a sync for the guild. --- tests/bot/cogs/sync/test_cog.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index fb0f044b0..efffaf53b 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -1,4 +1,5 @@ import unittest +from unittest import mock from bot.cogs import sync from tests import helpers @@ -13,3 +14,23 @@ class SyncExtensionTests(unittest.TestCase): bot = helpers.MockBot() sync.setup(bot) bot.add_cog.assert_called_once() + + +class SyncCogTests(unittest.TestCase): + """Tests for the Sync cog.""" + + def setUp(self): + self.bot = helpers.MockBot() + + @mock.patch("bot.cogs.sync.syncers.RoleSyncer", autospec=True) + @mock.patch("bot.cogs.sync.syncers.UserSyncer", autospec=True) + def test_sync_cog_init(self, mock_role, mock_sync): + """Should instantiate syncers and run a sync for the guild.""" + mock_sync_guild_coro = mock.MagicMock() + sync.Sync.sync_guild = mock.MagicMock(return_value=mock_sync_guild_coro) + + sync.Sync(self.bot) + + mock_role.assert_called_once_with(self.bot) + mock_sync.assert_called_once_with(self.bot) + self.bot.loop.create_task.assert_called_once_with(mock_sync_guild_coro) -- cgit v1.2.3 From f9e72150b5e2f4c2ae4b3968ef2d2da29fd5adbd Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 28 Jan 2020 18:31:06 -0800 Subject: Sync tests: instantiate a Sync cog in setUp * Move patches to setUp --- tests/bot/cogs/sync/test_cog.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index efffaf53b..74afa2f9d 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -22,15 +22,29 @@ class SyncCogTests(unittest.TestCase): def setUp(self): self.bot = helpers.MockBot() - @mock.patch("bot.cogs.sync.syncers.RoleSyncer", autospec=True) - @mock.patch("bot.cogs.sync.syncers.UserSyncer", autospec=True) - def test_sync_cog_init(self, mock_role, mock_sync): + self.role_syncer_patcher = mock.patch("bot.cogs.sync.syncers.RoleSyncer", autospec=True) + self.user_syncer_patcher = mock.patch("bot.cogs.sync.syncers.UserSyncer", autospec=True) + self.RoleSyncer = self.role_syncer_patcher.start() + self.UserSyncer = self.user_syncer_patcher.start() + + self.cog = sync.Sync(self.bot) + + def tearDown(self): + self.role_syncer_patcher.stop() + self.user_syncer_patcher.stop() + + def test_sync_cog_init(self): """Should instantiate syncers and run a sync for the guild.""" + # Reset because a Sync cog was already instantiated in setUp. + self.RoleSyncer.reset_mock() + self.UserSyncer.reset_mock() + self.bot.loop.create_task.reset_mock() + mock_sync_guild_coro = mock.MagicMock() sync.Sync.sync_guild = mock.MagicMock(return_value=mock_sync_guild_coro) sync.Sync(self.bot) - mock_role.assert_called_once_with(self.bot) - mock_sync.assert_called_once_with(self.bot) + self.RoleSyncer.assert_called_once_with(self.bot) + self.UserSyncer.assert_called_once_with(self.bot) self.bot.loop.create_task.assert_called_once_with(mock_sync_guild_coro) -- cgit v1.2.3 From f1502c6cc6c65be5b2b29066c8a2d774e73935d9 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 29 Jan 2020 17:00:55 -0800 Subject: Sync tests: use mock.patch for sync_guild This prevents persistence of changes to the cog instance; sync_guild would otherwise remain as a mock object for any subsequent tests. --- tests/bot/cogs/sync/test_cog.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 74afa2f9d..118782db3 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -33,7 +33,8 @@ class SyncCogTests(unittest.TestCase): self.role_syncer_patcher.stop() self.user_syncer_patcher.stop() - def test_sync_cog_init(self): + @mock.patch.object(sync.Sync, "sync_guild") + def test_sync_cog_init(self, sync_guild): """Should instantiate syncers and run a sync for the guild.""" # Reset because a Sync cog was already instantiated in setUp. self.RoleSyncer.reset_mock() @@ -41,10 +42,11 @@ class SyncCogTests(unittest.TestCase): self.bot.loop.create_task.reset_mock() mock_sync_guild_coro = mock.MagicMock() - sync.Sync.sync_guild = mock.MagicMock(return_value=mock_sync_guild_coro) + sync_guild.return_value = mock_sync_guild_coro sync.Sync(self.bot) self.RoleSyncer.assert_called_once_with(self.bot) self.UserSyncer.assert_called_once_with(self.bot) + sync_guild.assert_called_once_with() self.bot.loop.create_task.assert_called_once_with(mock_sync_guild_coro) -- cgit v1.2.3 From bd5980728bd7bfd5bba53369934698c43f12fa05 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 29 Jan 2020 17:09:43 -0800 Subject: Sync tests: fix Syncer mocks not having async methods While on 3.7, the CustomMockMixin needs to be leveraged so that coroutine members are replace with AsyncMocks instead. --- tests/bot/cogs/sync/test_cog.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 118782db3..ec66c795d 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -2,9 +2,21 @@ import unittest from unittest import mock from bot.cogs import sync +from bot.cogs.sync.syncers import Syncer from tests import helpers +class MockSyncer(helpers.CustomMockMixin, mock.MagicMock): + """ + A MagicMock subclass to mock Syncer objects. + + Instances of this class will follow the specifications of `bot.cogs.sync.syncers.Syncer` + instances. For more information, see the `MockGuild` docstring. + """ + def __init__(self, **kwargs) -> None: + super().__init__(spec_set=Syncer, **kwargs) + + class SyncExtensionTests(unittest.TestCase): """Tests for the sync extension.""" @@ -22,8 +34,17 @@ class SyncCogTests(unittest.TestCase): def setUp(self): self.bot = helpers.MockBot() - self.role_syncer_patcher = mock.patch("bot.cogs.sync.syncers.RoleSyncer", autospec=True) - self.user_syncer_patcher = mock.patch("bot.cogs.sync.syncers.UserSyncer", autospec=True) + # These patch the type. When the type is called, a MockSyncer instanced is returned. + # MockSyncer is needed so that our custom AsyncMock is used. + # TODO: Use autospec instead in 3.8, which will automatically use AsyncMock when needed. + self.role_syncer_patcher = mock.patch( + "bot.cogs.sync.syncers.RoleSyncer", + new=mock.MagicMock(return_value=MockSyncer()) + ) + self.user_syncer_patcher = mock.patch( + "bot.cogs.sync.syncers.UserSyncer", + new=mock.MagicMock(return_value=MockSyncer()) + ) self.RoleSyncer = self.role_syncer_patcher.start() self.UserSyncer = self.user_syncer_patcher.start() -- cgit v1.2.3 From 607e4480badd58d5de36d5be3306498afcb4348c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 29 Jan 2020 18:47:58 -0800 Subject: Sync tests: test sync_guild Roles and users should be synced only if a guild is successfully retrieved. --- tests/bot/cogs/sync/test_cog.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index ec66c795d..09ce0ae16 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -1,6 +1,8 @@ +import asyncio import unittest from unittest import mock +from bot import constants from bot.cogs import sync from bot.cogs.sync.syncers import Syncer from tests import helpers @@ -71,3 +73,25 @@ class SyncCogTests(unittest.TestCase): self.UserSyncer.assert_called_once_with(self.bot) sync_guild.assert_called_once_with() self.bot.loop.create_task.assert_called_once_with(mock_sync_guild_coro) + + def test_sync_cog_sync_guild(self): + """Roles and users should be synced only if a guild is successfully retrieved.""" + for guild in (helpers.MockGuild(), None): + with self.subTest(guild=guild): + self.bot.reset_mock() + self.cog.role_syncer.reset_mock() + self.cog.user_syncer.reset_mock() + + self.bot.get_guild = mock.MagicMock(return_value=guild) + + asyncio.run(self.cog.sync_guild()) + + self.bot.wait_until_guild_available.assert_called_once() + self.bot.get_guild.assert_called_once_with(constants.Guild.id) + + if guild is None: + self.cog.role_syncer.sync.assert_not_called() + self.cog.user_syncer.sync.assert_not_called() + else: + self.cog.role_syncer.sync.assert_called_once_with(guild) + self.cog.user_syncer.sync.assert_called_once_with(guild) -- cgit v1.2.3 From a0253c2349bead625633737964ba4203d75db7aa Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 31 Jan 2020 11:21:19 -0800 Subject: Sync tests: test patch_user A PATCH request should be sent. The error should only be raised if it is not a 404. * Add a fixture to create ResponseCodeErrors with a specific status --- tests/bot/cogs/sync/test_cog.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 09ce0ae16..0eb8954f1 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -3,6 +3,7 @@ import unittest from unittest import mock from bot import constants +from bot.api import ResponseCodeError from bot.cogs import sync from bot.cogs.sync.syncers import Syncer from tests import helpers @@ -56,6 +57,14 @@ class SyncCogTests(unittest.TestCase): self.role_syncer_patcher.stop() self.user_syncer_patcher.stop() + @staticmethod + def response_error(status: int) -> ResponseCodeError: + """Fixture to return a ResponseCodeError with the given status code.""" + response = mock.MagicMock() + response.status = status + + return ResponseCodeError(response) + @mock.patch.object(sync.Sync, "sync_guild") def test_sync_cog_init(self, sync_guild): """Should instantiate syncers and run a sync for the guild.""" @@ -95,3 +104,20 @@ class SyncCogTests(unittest.TestCase): else: self.cog.role_syncer.sync.assert_called_once_with(guild) self.cog.user_syncer.sync.assert_called_once_with(guild) + + def test_sync_cog_patch_user(self): + """A PATCH request should be sent and 404 errors ignored.""" + for side_effect in (None, self.response_error(404)): + with self.subTest(side_effect=side_effect): + self.bot.api_client.patch.reset_mock(side_effect=True) + self.bot.api_client.patch.side_effect = side_effect + + asyncio.run(self.cog.patch_user(5, {})) + self.bot.api_client.patch.assert_called_once() + + def test_sync_cog_patch_user_non_404(self): + """A PATCH request should be sent and the error raised if it's not a 404.""" + self.bot.api_client.patch.side_effect = self.response_error(500) + with self.assertRaises(ResponseCodeError): + asyncio.run(self.cog.patch_user(5, {})) + self.bot.api_client.patch.assert_called_once() -- cgit v1.2.3 From 93b3ec43526096bdf3f4c8a9ee2c9de29d25a562 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 31 Jan 2020 12:52:40 -0800 Subject: Sync tests: add helper function for testing patch_user Reduces redundancy in the tests by taking care of the mocks, calling of the function, and the assertion. --- tests/bot/cogs/sync/test_cog.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 0eb8954f1..bdb7aeb63 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -105,19 +105,26 @@ class SyncCogTests(unittest.TestCase): self.cog.role_syncer.sync.assert_called_once_with(guild) self.cog.user_syncer.sync.assert_called_once_with(guild) + def patch_user_helper(self, side_effect: BaseException) -> None: + """Helper to set a side effect for bot.api_client.patch and then assert it is called.""" + self.bot.api_client.patch.reset_mock(side_effect=True) + self.bot.api_client.patch.side_effect = side_effect + + user_id, updated_information = 5, {"key": 123} + asyncio.run(self.cog.patch_user(user_id, updated_information)) + + self.bot.api_client.patch.assert_called_once_with( + f"bot/users/{user_id}", + json=updated_information, + ) + def test_sync_cog_patch_user(self): """A PATCH request should be sent and 404 errors ignored.""" for side_effect in (None, self.response_error(404)): with self.subTest(side_effect=side_effect): - self.bot.api_client.patch.reset_mock(side_effect=True) - self.bot.api_client.patch.side_effect = side_effect - - asyncio.run(self.cog.patch_user(5, {})) - self.bot.api_client.patch.assert_called_once() + self.patch_user_helper(side_effect) def test_sync_cog_patch_user_non_404(self): """A PATCH request should be sent and the error raised if it's not a 404.""" - self.bot.api_client.patch.side_effect = self.response_error(500) with self.assertRaises(ResponseCodeError): - asyncio.run(self.cog.patch_user(5, {})) - self.bot.api_client.patch.assert_called_once() + self.patch_user_helper(self.response_error(500)) -- cgit v1.2.3 From 097a5231067320b73277852202444c404bb0adbb Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 31 Jan 2020 13:39:19 -0800 Subject: Sync tests: create a base TestCase class for Sync cog tests --- tests/bot/cogs/sync/test_cog.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index bdb7aeb63..c6009b2e5 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -31,8 +31,8 @@ class SyncExtensionTests(unittest.TestCase): bot.add_cog.assert_called_once() -class SyncCogTests(unittest.TestCase): - """Tests for the Sync cog.""" +class SyncCogTestCase(unittest.TestCase): + """Base class for Sync cog tests. Sets up patches for syncers.""" def setUp(self): self.bot = helpers.MockBot() @@ -65,6 +65,10 @@ class SyncCogTests(unittest.TestCase): return ResponseCodeError(response) + +class SyncCogTests(SyncCogTestCase): + """Tests for the Sync cog.""" + @mock.patch.object(sync.Sync, "sync_guild") def test_sync_cog_init(self, sync_guild): """Should instantiate syncers and run a sync for the guild.""" -- cgit v1.2.3 From 3c0937de8641092100acc6424f4455c49d2e7855 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 31 Jan 2020 13:54:20 -0800 Subject: Sync tests: create a test case for listener tests --- tests/bot/cogs/sync/test_cog.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index c6009b2e5..d71366791 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -132,3 +132,10 @@ class SyncCogTests(SyncCogTestCase): """A PATCH request should be sent and the error raised if it's not a 404.""" with self.assertRaises(ResponseCodeError): self.patch_user_helper(self.response_error(500)) + + +class SyncCogListenerTests(SyncCogTestCase): + """Tests for the listeners of the Sync cog.""" + def setUp(self): + super().setUp() + self.cog.patch_user = helpers.AsyncMock(spec_set=self.cog.patch_user) -- cgit v1.2.3 From 948661e3738ae2bd2636631bf2a91c1589aa0bde Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 31 Jan 2020 14:04:17 -0800 Subject: Sync tests: test Sync cog's on_guild_role_create listener A POST request should be sent with the new role's data. * Add a fixture to create a MockRole --- tests/bot/cogs/sync/test_cog.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index d71366791..a4969551d 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -1,7 +1,10 @@ import asyncio +import typing as t import unittest from unittest import mock +import discord + from bot import constants from bot.api import ResponseCodeError from bot.cogs import sync @@ -139,3 +142,29 @@ class SyncCogListenerTests(SyncCogTestCase): def setUp(self): super().setUp() self.cog.patch_user = helpers.AsyncMock(spec_set=self.cog.patch_user) + + @staticmethod + def mock_role() -> t.Tuple[helpers.MockRole, t.Dict[str, t.Any]]: + """Fixture to return a MockRole and corresponding JSON dict.""" + colour = 49 + permissions = 8 + role_data = { + "colour": colour, + "id": 777, + "name": "rolename", + "permissions": permissions, + "position": 23, + } + + role = helpers.MockRole(**role_data) + role.colour = discord.Colour(colour) + role.permissions = discord.Permissions(permissions) + + return role, role_data + + def test_sync_cog_on_guild_role_create(self): + """A POST request should be sent with the new role's data.""" + role, role_data = self.mock_role() + asyncio.run(self.cog.on_guild_role_create(role)) + + self.bot.api_client.post.assert_called_once_with("bot/roles", json=role_data) -- cgit v1.2.3 From d249d4517cbd903a550047bd91e9c83bf828b9d0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 31 Jan 2020 14:10:52 -0800 Subject: Sync tests: test Sync cog's on_guild_role_delete listener A DELETE request should be sent. --- tests/bot/cogs/sync/test_cog.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index a4969551d..e183b429f 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -168,3 +168,10 @@ class SyncCogListenerTests(SyncCogTestCase): asyncio.run(self.cog.on_guild_role_create(role)) self.bot.api_client.post.assert_called_once_with("bot/roles", json=role_data) + + def test_sync_cog_on_guild_role_delete(self): + """A DELETE request should be sent.""" + role = helpers.MockRole(id=99) + asyncio.run(self.cog.on_guild_role_delete(role)) + + self.bot.api_client.delete.assert_called_once_with("bot/roles/99") -- cgit v1.2.3 From 535095ff647277922b7d1930da8d038f15af74fd Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 1 Feb 2020 15:32:16 -0800 Subject: Tests: use objects for colour and permissions of MockRole Instances of discord.Colour and discord.Permissions will be created by default or when ints are given as values for those attributes. --- tests/helpers.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/helpers.py b/tests/helpers.py index b18a27ebe..a40673bb9 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -270,9 +270,21 @@ class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): information, see the `MockGuild` docstring. """ def __init__(self, **kwargs) -> None: - default_kwargs = {'id': next(self.discord_id), 'name': 'role', 'position': 1} + default_kwargs = { + 'id': next(self.discord_id), + 'name': 'role', + 'position': 1, + 'colour': discord.Colour(0xdeadbf), + 'permissions': discord.Permissions(), + } super().__init__(spec_set=role_instance, **collections.ChainMap(kwargs, default_kwargs)) + if isinstance(self.colour, int): + self.colour = discord.Colour(self.colour) + + if isinstance(self.permissions, int): + self.permissions = discord.Permissions(self.permissions) + if 'mention' not in kwargs: self.mention = f'&{self.name}' -- cgit v1.2.3 From 4e81281ecc87a6d2af320b3c000aea286a50f2a7 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 1 Feb 2020 20:58:15 -0800 Subject: Sync tests: remove mock_role fixture It is obsolete because MockRole now takes care of creating the Colour and Permissions objects. --- tests/bot/cogs/sync/test_cog.py | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index e183b429f..604daa437 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -1,10 +1,7 @@ import asyncio -import typing as t import unittest from unittest import mock -import discord - from bot import constants from bot.api import ResponseCodeError from bot.cogs import sync @@ -143,28 +140,16 @@ class SyncCogListenerTests(SyncCogTestCase): super().setUp() self.cog.patch_user = helpers.AsyncMock(spec_set=self.cog.patch_user) - @staticmethod - def mock_role() -> t.Tuple[helpers.MockRole, t.Dict[str, t.Any]]: - """Fixture to return a MockRole and corresponding JSON dict.""" - colour = 49 - permissions = 8 + def test_sync_cog_on_guild_role_create(self): + """A POST request should be sent with the new role's data.""" role_data = { - "colour": colour, + "colour": 49, "id": 777, "name": "rolename", - "permissions": permissions, + "permissions": 8, "position": 23, } - role = helpers.MockRole(**role_data) - role.colour = discord.Colour(colour) - role.permissions = discord.Permissions(permissions) - - return role, role_data - - def test_sync_cog_on_guild_role_create(self): - """A POST request should be sent with the new role's data.""" - role, role_data = self.mock_role() asyncio.run(self.cog.on_guild_role_create(role)) self.bot.api_client.post.assert_called_once_with("bot/roles", json=role_data) -- cgit v1.2.3 From ad53b51b860858cb9434435de3d205165b2d78f8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 1 Feb 2020 21:35:33 -0800 Subject: Sync tests: test Sync cog's on_guild_role_update A PUT request should be sent if the colour, name, permissions, or position changes. --- tests/bot/cogs/sync/test_cog.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 604daa437..9a3232b3a 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -160,3 +160,38 @@ class SyncCogListenerTests(SyncCogTestCase): asyncio.run(self.cog.on_guild_role_delete(role)) self.bot.api_client.delete.assert_called_once_with("bot/roles/99") + + def test_sync_cog_on_guild_role_update(self): + """A PUT request should be sent if the colour, name, permissions, or position changes.""" + role_data = { + "colour": 49, + "id": 777, + "name": "rolename", + "permissions": 8, + "position": 23, + } + subtests = ( + (True, ("colour", "name", "permissions", "position")), + (False, ("hoist", "mentionable")), + ) + + for should_put, attributes in subtests: + for attribute in attributes: + with self.subTest(should_put=should_put, changed_attribute=attribute): + self.bot.api_client.put.reset_mock() + + after_role_data = role_data.copy() + after_role_data[attribute] = 876 + + before_role = helpers.MockRole(**role_data) + after_role = helpers.MockRole(**after_role_data) + + asyncio.run(self.cog.on_guild_role_update(before_role, after_role)) + + if should_put: + self.bot.api_client.put.assert_called_once_with( + f"bot/roles/{after_role.id}", + json=after_role_data + ) + else: + self.bot.api_client.put.assert_not_called() -- cgit v1.2.3 From 524026576d89cf84d0e44b3cb36ee8810e924396 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 4 Feb 2020 21:07:52 -0800 Subject: Sync tests: test Sync cog's on_member_remove A PUT request should be sent to set in_guild as False and update other fields. --- tests/bot/cogs/sync/test_cog.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 9a3232b3a..4ee66a518 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -195,3 +195,20 @@ class SyncCogListenerTests(SyncCogTestCase): ) else: self.bot.api_client.put.assert_not_called() + + def test_sync_cog_on_member_remove(self): + """A PUT request should be sent to set in_guild as False and update other fields.""" + roles = [helpers.MockRole(id=i) for i in (57, 22, 43)] # purposefully unsorted + member = helpers.MockMember(roles=roles) + + asyncio.run(self.cog.on_member_remove(member)) + + json_data = { + "avatar_hash": member.avatar, + "discriminator": int(member.discriminator), + "id": member.id, + "in_guild": False, + "name": member.name, + "roles": sorted(role.id for role in member.roles) + } + self.bot.api_client.put.assert_called_once_with("bot/users/88", json=json_data) -- cgit v1.2.3 From 7748de87d507d2732c58a77ae6300b8c925fa8c9 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 5 Feb 2020 11:34:01 -0800 Subject: Sync tests: test Sync cog's on_member_update for roles Members should be patched if their roles have changed. --- tests/bot/cogs/sync/test_cog.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 4ee66a518..f04d53caa 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -212,3 +212,14 @@ class SyncCogListenerTests(SyncCogTestCase): "roles": sorted(role.id for role in member.roles) } self.bot.api_client.put.assert_called_once_with("bot/users/88", json=json_data) + + def test_sync_cog_on_member_update_roles(self): + """Members should be patched if their roles have changed.""" + before_roles = [helpers.MockRole(id=12), helpers.MockRole(id=30)] + before_member = helpers.MockMember(roles=before_roles) + after_member = helpers.MockMember(roles=before_roles[1:]) + + asyncio.run(self.cog.on_member_update(before_member, after_member)) + + data = {"roles": sorted(role.id for role in after_member.roles)} + self.cog.patch_user.assert_called_once_with(after_member.id, updated_information=data) -- cgit v1.2.3 From 562a33184b52525bc8f9cfda8aaeb8245087e135 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 5 Feb 2020 11:37:20 -0800 Subject: Sync tests: test Sync cog's on_member_update for other attributes Members should not be patched if other attributes have changed. --- tests/bot/cogs/sync/test_cog.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index f04d53caa..36945b82e 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -2,6 +2,8 @@ import asyncio import unittest from unittest import mock +import discord + from bot import constants from bot.api import ResponseCodeError from bot.cogs import sync @@ -223,3 +225,22 @@ class SyncCogListenerTests(SyncCogTestCase): data = {"roles": sorted(role.id for role in after_member.roles)} self.cog.patch_user.assert_called_once_with(after_member.id, updated_information=data) + + def test_sync_cog_on_member_update_other(self): + """Members should not be patched if other attributes have changed.""" + subtests = ( + ("activities", discord.Game("Pong"), discord.Game("Frogger")), + ("nick", "old nick", "new nick"), + ("status", discord.Status.online, discord.Status.offline) + ) + + for attribute, old_value, new_value in subtests: + with self.subTest(attribute=attribute): + self.cog.patch_user.reset_mock() + + before_member = helpers.MockMember(**{attribute: old_value}) + after_member = helpers.MockMember(**{attribute: new_value}) + + asyncio.run(self.cog.on_member_update(before_member, after_member)) + + self.cog.patch_user.assert_not_called() -- cgit v1.2.3 From df1e4f10b4ffc6a514528d03d10d3854385986ac Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 5 Feb 2020 12:08:23 -0800 Subject: Sync tests: fix ID in endpoint for test_sync_cog_on_member_remove --- tests/bot/cogs/sync/test_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 36945b82e..75165a5b2 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -213,7 +213,7 @@ class SyncCogListenerTests(SyncCogTestCase): "name": member.name, "roles": sorted(role.id for role in member.roles) } - self.bot.api_client.put.assert_called_once_with("bot/users/88", json=json_data) + self.bot.api_client.put.assert_called_once_with(f"bot/users/{member.id}", json=json_data) def test_sync_cog_on_member_update_roles(self): """Members should be patched if their roles have changed.""" -- cgit v1.2.3 From b3d19d72596052629f56823dfd6c63b42dda6253 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 5 Feb 2020 13:14:33 -0800 Subject: Sync tests: test Sync cog's on_user_update A user should be patched only if the name, discriminator, or avatar changes. --- tests/bot/cogs/sync/test_cog.py | 43 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 75165a5b2..88c5e00b9 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -231,7 +231,7 @@ class SyncCogListenerTests(SyncCogTestCase): subtests = ( ("activities", discord.Game("Pong"), discord.Game("Frogger")), ("nick", "old nick", "new nick"), - ("status", discord.Status.online, discord.Status.offline) + ("status", discord.Status.online, discord.Status.offline), ) for attribute, old_value, new_value in subtests: @@ -244,3 +244,44 @@ class SyncCogListenerTests(SyncCogTestCase): asyncio.run(self.cog.on_member_update(before_member, after_member)) self.cog.patch_user.assert_not_called() + + def test_sync_cog_on_user_update(self): + """A user should be patched only if the name, discriminator, or avatar changes.""" + before_data = { + "name": "old name", + "discriminator": "1234", + "avatar": "old avatar", + "bot": False, + } + + subtests = ( + (True, "name", "name", "new name", "new name"), + (True, "discriminator", "discriminator", "8765", 8765), + (True, "avatar", "avatar_hash", "9j2e9", "9j2e9"), + (False, "bot", "bot", True, True), + ) + + for should_patch, attribute, api_field, value, api_value in subtests: + with self.subTest(attribute=attribute): + self.cog.patch_user.reset_mock() + + after_data = before_data.copy() + after_data[attribute] = value + before_user = helpers.MockUser(**before_data) + after_user = helpers.MockUser(**after_data) + + asyncio.run(self.cog.on_user_update(before_user, after_user)) + + if should_patch: + self.cog.patch_user.assert_called_once() + + # Don't care if *all* keys are present; only the changed one is required + call_args = self.cog.patch_user.call_args + self.assertEqual(call_args[0][0], after_user.id) + self.assertIn("updated_information", call_args[1]) + + updated_information = call_args[1]["updated_information"] + self.assertIn(api_field, updated_information) + self.assertEqual(updated_information[api_field], api_value) + else: + self.cog.patch_user.assert_not_called() -- cgit v1.2.3 From 5a685dfa2a99ee61a898940812b289cb9f448fdc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 6 Feb 2020 13:01:25 -0800 Subject: Sync tests: test sync roles command sync() should be called on the RoleSyncer. --- tests/bot/cogs/sync/test_cog.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 88c5e00b9..4de058965 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -285,3 +285,12 @@ class SyncCogListenerTests(SyncCogTestCase): self.assertEqual(updated_information[api_field], api_value) else: self.cog.patch_user.assert_not_called() + + +class SyncCogCommandTests(SyncCogTestCase): + def test_sync_roles_command(self): + """sync() should be called on the RoleSyncer.""" + ctx = helpers.MockContext() + asyncio.run(self.cog.sync_roles_command.callback(self.cog, ctx)) + + self.cog.role_syncer.sync.assert_called_once_with(ctx.guild, ctx) -- cgit v1.2.3 From 0e7211e80c76973e781db3bbea82a54e6a9ebb1c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 6 Feb 2020 13:11:37 -0800 Subject: Sync tests: test sync users command sync() should be called on the UserSyncer. --- tests/bot/cogs/sync/test_cog.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 4de058965..f21d1574b 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -294,3 +294,10 @@ class SyncCogCommandTests(SyncCogTestCase): asyncio.run(self.cog.sync_roles_command.callback(self.cog, ctx)) self.cog.role_syncer.sync.assert_called_once_with(ctx.guild, ctx) + + def test_sync_users_command(self): + """sync() should be called on the UserSyncer.""" + ctx = helpers.MockContext() + asyncio.run(self.cog.sync_users_command.callback(self.cog, ctx)) + + self.cog.user_syncer.sync.assert_called_once_with(ctx.guild, ctx) -- cgit v1.2.3 From 7b9e71fbb1364a416e5239b45434874fed9eb857 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 10 Feb 2020 16:31:07 -0800 Subject: Tests: create TestCase subclass with a permissions check assertion The subclass will contain assertions that are useful for testing Discord commands. The currently included assertion tests that a command will raise a MissingPermissions exception if the author lacks permissions. --- tests/base.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'tests') diff --git a/tests/base.py b/tests/base.py index 029a249ed..88693f382 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,6 +1,12 @@ import logging import unittest from contextlib import contextmanager +from typing import Dict + +import discord +from discord.ext import commands + +from tests import helpers class _CaptureLogHandler(logging.Handler): @@ -65,3 +71,31 @@ class LoggingTestCase(unittest.TestCase): standard_message = self._truncateMessage(base_message, record_message) msg = self._formatMessage(msg, standard_message) self.fail(msg) + + +class CommandTestCase(unittest.TestCase): + """TestCase with additional assertions that are useful for testing Discord commands.""" + + @helpers.async_test + async def assertHasPermissionsCheck( + self, + cmd: commands.Command, + permissions: Dict[str, bool], + ) -> None: + """ + Test that `cmd` raises a `MissingPermissions` exception if author lacks `permissions`. + + Every permission in `permissions` is expected to be reported as missing. In other words, do + not include permissions which should not raise an exception along with those which should. + """ + # Invert permission values because it's more intuitive to pass to this assertion the same + # permissions as those given to the check decorator. + permissions = {k: not v for k, v in permissions.items()} + + ctx = helpers.MockContext() + ctx.channel.permissions_for.return_value = discord.Permissions(**permissions) + + with self.assertRaises(commands.MissingPermissions) as cm: + await cmd.can_run(ctx) + + self.assertCountEqual(permissions.keys(), cm.exception.missing_perms) -- cgit v1.2.3 From 2d0f25c2472b94e2b40fc12cc49fd2ad4272c9ee Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 10 Feb 2020 16:49:27 -0800 Subject: Sync tests: test sync commands require the admin permission The sync commands should only run if the author has the administrator permission. * Add missing spaces after class docstrings * Add missing docstring to SyncCogCommandTests --- tests/bot/cogs/sync/test_cog.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index f21d1574b..b1f586a5b 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -9,6 +9,7 @@ from bot.api import ResponseCodeError from bot.cogs import sync from bot.cogs.sync.syncers import Syncer from tests import helpers +from tests.base import CommandTestCase class MockSyncer(helpers.CustomMockMixin, mock.MagicMock): @@ -18,6 +19,7 @@ class MockSyncer(helpers.CustomMockMixin, mock.MagicMock): Instances of this class will follow the specifications of `bot.cogs.sync.syncers.Syncer` instances. For more information, see the `MockGuild` docstring. """ + def __init__(self, **kwargs) -> None: super().__init__(spec_set=Syncer, **kwargs) @@ -138,6 +140,7 @@ class SyncCogTests(SyncCogTestCase): class SyncCogListenerTests(SyncCogTestCase): """Tests for the listeners of the Sync cog.""" + def setUp(self): super().setUp() self.cog.patch_user = helpers.AsyncMock(spec_set=self.cog.patch_user) @@ -287,7 +290,9 @@ class SyncCogListenerTests(SyncCogTestCase): self.cog.patch_user.assert_not_called() -class SyncCogCommandTests(SyncCogTestCase): +class SyncCogCommandTests(SyncCogTestCase, CommandTestCase): + """Tests for the commands in the Sync cog.""" + def test_sync_roles_command(self): """sync() should be called on the RoleSyncer.""" ctx = helpers.MockContext() @@ -301,3 +306,15 @@ class SyncCogCommandTests(SyncCogTestCase): asyncio.run(self.cog.sync_users_command.callback(self.cog, ctx)) self.cog.user_syncer.sync.assert_called_once_with(ctx.guild, ctx) + + def test_commands_require_admin(self): + """The sync commands should only run if the author has the administrator permission.""" + cmds = ( + self.cog.sync_group, + self.cog.sync_roles_command, + self.cog.sync_users_command, + ) + + for cmd in cmds: + with self.subTest(cmd=cmd): + self.assertHasPermissionsCheck(cmd, {"administrator": True}) -- cgit v1.2.3 From e8b1fa52daf5950ad253e52c3b386a9d4967e739 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 10 Feb 2020 17:02:02 -0800 Subject: Sync tests: assert that listeners are actually added as listeners --- tests/bot/cogs/sync/test_cog.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index b1f586a5b..f7e86f063 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -147,6 +147,8 @@ class SyncCogListenerTests(SyncCogTestCase): def test_sync_cog_on_guild_role_create(self): """A POST request should be sent with the new role's data.""" + self.assertTrue(self.cog.on_guild_role_create.__cog_listener__) + role_data = { "colour": 49, "id": 777, @@ -161,6 +163,8 @@ class SyncCogListenerTests(SyncCogTestCase): def test_sync_cog_on_guild_role_delete(self): """A DELETE request should be sent.""" + self.assertTrue(self.cog.on_guild_role_delete.__cog_listener__) + role = helpers.MockRole(id=99) asyncio.run(self.cog.on_guild_role_delete(role)) @@ -168,6 +172,8 @@ class SyncCogListenerTests(SyncCogTestCase): def test_sync_cog_on_guild_role_update(self): """A PUT request should be sent if the colour, name, permissions, or position changes.""" + self.assertTrue(self.cog.on_guild_role_update.__cog_listener__) + role_data = { "colour": 49, "id": 777, @@ -203,6 +209,8 @@ class SyncCogListenerTests(SyncCogTestCase): def test_sync_cog_on_member_remove(self): """A PUT request should be sent to set in_guild as False and update other fields.""" + self.assertTrue(self.cog.on_member_remove.__cog_listener__) + roles = [helpers.MockRole(id=i) for i in (57, 22, 43)] # purposefully unsorted member = helpers.MockMember(roles=roles) @@ -220,6 +228,8 @@ class SyncCogListenerTests(SyncCogTestCase): def test_sync_cog_on_member_update_roles(self): """Members should be patched if their roles have changed.""" + self.assertTrue(self.cog.on_member_update.__cog_listener__) + before_roles = [helpers.MockRole(id=12), helpers.MockRole(id=30)] before_member = helpers.MockMember(roles=before_roles) after_member = helpers.MockMember(roles=before_roles[1:]) @@ -231,6 +241,8 @@ class SyncCogListenerTests(SyncCogTestCase): def test_sync_cog_on_member_update_other(self): """Members should not be patched if other attributes have changed.""" + self.assertTrue(self.cog.on_member_update.__cog_listener__) + subtests = ( ("activities", discord.Game("Pong"), discord.Game("Frogger")), ("nick", "old nick", "new nick"), @@ -250,6 +262,8 @@ class SyncCogListenerTests(SyncCogTestCase): def test_sync_cog_on_user_update(self): """A user should be patched only if the name, discriminator, or avatar changes.""" + self.assertTrue(self.cog.on_user_update.__cog_listener__) + before_data = { "name": "old name", "discriminator": "1234", -- cgit v1.2.3 From 5c385da1a41b2a6463b38b1973e13fd4590d61cb Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 10 Feb 2020 17:06:18 -0800 Subject: Sync tests: fix on_member_remove listener test The listener was changed earlier to simply set in_guild to False. This commit accounts for that in the test. --- tests/bot/cogs/sync/test_cog.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index f7e86f063..a8c79e0d3 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -208,23 +208,16 @@ class SyncCogListenerTests(SyncCogTestCase): self.bot.api_client.put.assert_not_called() def test_sync_cog_on_member_remove(self): - """A PUT request should be sent to set in_guild as False and update other fields.""" + """Member should patched to set in_guild as False.""" self.assertTrue(self.cog.on_member_remove.__cog_listener__) - roles = [helpers.MockRole(id=i) for i in (57, 22, 43)] # purposefully unsorted - member = helpers.MockMember(roles=roles) - + member = helpers.MockMember() asyncio.run(self.cog.on_member_remove(member)) - json_data = { - "avatar_hash": member.avatar, - "discriminator": int(member.discriminator), - "id": member.id, - "in_guild": False, - "name": member.name, - "roles": sorted(role.id for role in member.roles) - } - self.bot.api_client.put.assert_called_once_with(f"bot/users/{member.id}", json=json_data) + self.cog.patch_user.assert_called_once_with( + member.id, + updated_information={"in_guild": False} + ) def test_sync_cog_on_member_update_roles(self): """Members should be patched if their roles have changed.""" -- cgit v1.2.3 From 03b885ac9f8e0d30d4c38ad0f18a1d391c94765b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 11 Feb 2020 09:56:43 -0800 Subject: Sync tests: add a third role with a lower ID to on_member_update test This better ensures that roles are being sorted when patching. --- tests/bot/cogs/sync/test_cog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index a8c79e0d3..88f6eb6cf 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -223,7 +223,8 @@ class SyncCogListenerTests(SyncCogTestCase): """Members should be patched if their roles have changed.""" self.assertTrue(self.cog.on_member_update.__cog_listener__) - before_roles = [helpers.MockRole(id=12), helpers.MockRole(id=30)] + # Roles are intentionally unsorted. + before_roles = [helpers.MockRole(id=12), helpers.MockRole(id=30), helpers.MockRole(id=20)] before_member = helpers.MockMember(roles=before_roles) after_member = helpers.MockMember(roles=before_roles[1:]) -- cgit v1.2.3 From a1cb58ac1e784db64d82a082be25df3d524bfc20 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 12 Feb 2020 08:33:19 -0800 Subject: Sync tests: test on_member_join Should PUT user's data or POST it if the user doesn't exist. ResponseCodeError should be re-raised if status code isn't a 404. A helper method was added to reduce code redundancy between the 2 tests. --- tests/bot/cogs/sync/test_cog.py | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index 88f6eb6cf..f66adfea1 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -297,6 +297,58 @@ class SyncCogListenerTests(SyncCogTestCase): else: self.cog.patch_user.assert_not_called() + def on_member_join_helper(self, side_effect: Exception) -> dict: + """ + Helper to set `side_effect` for on_member_join and assert a PUT request was sent. + + The request data for the mock member is returned. All exceptions will be re-raised. + """ + member = helpers.MockMember( + discriminator="1234", + roles=[helpers.MockRole(id=22), helpers.MockRole(id=12)], + ) + + data = { + "avatar_hash": member.avatar, + "discriminator": int(member.discriminator), + "id": member.id, + "in_guild": True, + "name": member.name, + "roles": sorted(role.id for role in member.roles) + } + + self.bot.api_client.put.reset_mock(side_effect=True) + self.bot.api_client.put.side_effect = side_effect + + try: + asyncio.run(self.cog.on_member_join(member)) + except Exception: + raise + finally: + self.bot.api_client.put.assert_called_once_with( + f"bot/users/{member.id}", + json=data + ) + + return data + + def test_sync_cog_on_member_join(self): + """Should PUT user's data or POST it if the user doesn't exist.""" + for side_effect in (None, self.response_error(404)): + with self.subTest(side_effect=side_effect): + self.bot.api_client.post.reset_mock() + data = self.on_member_join_helper(side_effect) + + if side_effect: + self.bot.api_client.post.assert_called_once_with("bot/users", json=data) + else: + self.bot.api_client.post.assert_not_called() + + def test_sync_cog_on_member_join_non_404(self): + """ResponseCodeError should be re-raised if status code isn't a 404.""" + self.assertRaises(ResponseCodeError, self.on_member_join_helper, self.response_error(500)) + self.bot.api_client.post.assert_not_called() + class SyncCogCommandTests(SyncCogTestCase, CommandTestCase): """Tests for the commands in the Sync cog.""" -- cgit v1.2.3 From b11e2eb365405dd63ac0fc3a830804b4b58e1ebc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 12 Feb 2020 08:52:02 -0800 Subject: Sync tests: use async_test decorator --- tests/bot/cogs/sync/test_base.py | 61 +++++++++++++++++------------ tests/bot/cogs/sync/test_cog.py | 81 +++++++++++++++++++++++---------------- tests/bot/cogs/sync/test_roles.py | 41 ++++++++++++-------- tests/bot/cogs/sync/test_users.py | 46 +++++++++++++--------- 4 files changed, 135 insertions(+), 94 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index ff11d911e..0539f5683 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -1,4 +1,3 @@ -import asyncio import unittest from unittest import mock @@ -62,16 +61,18 @@ class SyncerSendPromptTests(unittest.TestCase): return mock_channel, mock_message - def test_send_prompt_edits_and_returns_message(self): + @helpers.async_test + async def test_send_prompt_edits_and_returns_message(self): """The given message should be edited to display the prompt and then should be returned.""" msg = helpers.MockMessage() - ret_val = asyncio.run(self.syncer._send_prompt(msg)) + ret_val = await self.syncer._send_prompt(msg) msg.edit.assert_called_once() self.assertIn("content", msg.edit.call_args[1]) self.assertEqual(ret_val, msg) - def test_send_prompt_gets_dev_core_channel(self): + @helpers.async_test + async def test_send_prompt_gets_dev_core_channel(self): """The dev-core channel should be retrieved if an extant message isn't given.""" subtests = ( (self.bot.get_channel, self.mock_get_channel), @@ -81,31 +82,34 @@ class SyncerSendPromptTests(unittest.TestCase): for method, mock_ in subtests: with self.subTest(method=method, msg=mock_.__name__): mock_() - asyncio.run(self.syncer._send_prompt()) + await self.syncer._send_prompt() method.assert_called_once_with(constants.Channels.devcore) - def test_send_prompt_returns_None_if_channel_fetch_fails(self): + @helpers.async_test + async def test_send_prompt_returns_None_if_channel_fetch_fails(self): """None should be returned if there's an HTTPException when fetching the channel.""" self.bot.get_channel.return_value = None self.bot.fetch_channel.side_effect = discord.HTTPException(mock.MagicMock(), "test error!") - ret_val = asyncio.run(self.syncer._send_prompt()) + ret_val = await self.syncer._send_prompt() self.assertIsNone(ret_val) - def test_send_prompt_sends_and_returns_new_message_if_not_given(self): + @helpers.async_test + async def test_send_prompt_sends_and_returns_new_message_if_not_given(self): """A new message mentioning core devs should be sent and returned if message isn't given.""" for mock_ in (self.mock_get_channel, self.mock_fetch_channel): with self.subTest(msg=mock_.__name__): mock_channel, mock_message = mock_() - ret_val = asyncio.run(self.syncer._send_prompt()) + ret_val = await self.syncer._send_prompt() mock_channel.send.assert_called_once() self.assertIn(self.syncer._CORE_DEV_MENTION, mock_channel.send.call_args[0][0]) self.assertEqual(ret_val, mock_message) - def test_send_prompt_adds_reactions(self): + @helpers.async_test + async def test_send_prompt_adds_reactions(self): """The message should have reactions for confirmation added.""" extant_message = helpers.MockMessage() subtests = ( @@ -119,7 +123,7 @@ class SyncerSendPromptTests(unittest.TestCase): with self.subTest(msg=subtest_msg): _, mock_message = mock_() - asyncio.run(self.syncer._send_prompt(message_arg)) + await self.syncer._send_prompt(message_arg) calls = [mock.call(emoji) for emoji in self.syncer._REACTION_EMOJIS] mock_message.add_reaction.assert_has_calls(calls) @@ -207,7 +211,8 @@ class SyncerConfirmationTests(unittest.TestCase): ret_val = self.syncer._reaction_check(*args) self.assertFalse(ret_val) - def test_wait_for_confirmation(self): + @helpers.async_test + async def test_wait_for_confirmation(self): """The message should always be edited and only return True if the emoji is a check mark.""" subtests = ( (constants.Emojis.check_mark, True, None), @@ -227,7 +232,7 @@ class SyncerConfirmationTests(unittest.TestCase): self.bot.wait_for.side_effect = side_effect # Call the function - actual_return = asyncio.run(self.syncer._wait_for_confirmation(member, message)) + actual_return = await self.syncer._wait_for_confirmation(member, message) # Perform assertions self.bot.wait_for.assert_called_once() @@ -253,7 +258,8 @@ class SyncerSyncTests(unittest.TestCase): self.bot = helpers.MockBot(user=helpers.MockMember(bot=True)) self.syncer = TestSyncer(self.bot) - def test_sync_respects_confirmation_result(self): + @helpers.async_test + async def test_sync_respects_confirmation_result(self): """The sync should abort if confirmation fails and continue if confirmed.""" mock_message = helpers.MockMessage() subtests = ( @@ -273,7 +279,7 @@ class SyncerSyncTests(unittest.TestCase): ) guild = helpers.MockGuild() - asyncio.run(self.syncer.sync(guild)) + await self.syncer.sync(guild) self.syncer._get_diff.assert_called_once_with(guild) self.syncer._get_confirmation_result.assert_called_once() @@ -283,7 +289,8 @@ class SyncerSyncTests(unittest.TestCase): else: self.syncer._sync.assert_not_called() - def test_sync_diff_size(self): + @helpers.async_test + async def test_sync_diff_size(self): """The diff size should be correctly calculated.""" subtests = ( (6, _Diff({1, 2}, {3, 4}, {5, 6})), @@ -299,13 +306,14 @@ class SyncerSyncTests(unittest.TestCase): self.syncer._get_confirmation_result = helpers.AsyncMock(return_value=(False, None)) guild = helpers.MockGuild() - asyncio.run(self.syncer.sync(guild)) + await self.syncer.sync(guild) self.syncer._get_diff.assert_called_once_with(guild) self.syncer._get_confirmation_result.assert_called_once() self.assertEqual(self.syncer._get_confirmation_result.call_args[0][0], size) - def test_sync_message_edited(self): + @helpers.async_test + async def test_sync_message_edited(self): """The message should be edited if one was sent, even if the sync has an API error.""" subtests = ( (None, None, False), @@ -321,13 +329,14 @@ class SyncerSyncTests(unittest.TestCase): ) guild = helpers.MockGuild() - asyncio.run(self.syncer.sync(guild)) + await self.syncer.sync(guild) if should_edit: message.edit.assert_called_once() self.assertIn("content", message.edit.call_args[1]) - def test_sync_confirmation_context_redirect(self): + @helpers.async_test + async def test_sync_confirmation_context_redirect(self): """If ctx is given, a new message should be sent and author should be ctx's author.""" mock_member = helpers.MockMember() subtests = ( @@ -343,7 +352,7 @@ class SyncerSyncTests(unittest.TestCase): self.syncer._get_confirmation_result = helpers.AsyncMock(return_value=(False, None)) guild = helpers.MockGuild() - asyncio.run(self.syncer.sync(guild, ctx)) + await self.syncer.sync(guild, ctx) if ctx is not None: ctx.send.assert_called_once() @@ -352,7 +361,8 @@ class SyncerSyncTests(unittest.TestCase): self.assertEqual(self.syncer._get_confirmation_result.call_args[0][1], author) self.assertEqual(self.syncer._get_confirmation_result.call_args[0][2], message) - def test_confirmation_result_small_diff(self): + @helpers.async_test + async def test_confirmation_result_small_diff(self): """Should always return True and the given message if the diff size is too small.""" self.syncer.MAX_DIFF = 3 author = helpers.MockMember() @@ -364,14 +374,15 @@ class SyncerSyncTests(unittest.TestCase): self.syncer._wait_for_confirmation = helpers.AsyncMock() coro = self.syncer._get_confirmation_result(size, author, expected_message) - result, actual_message = asyncio.run(coro) + result, actual_message = await coro self.assertTrue(result) self.assertEqual(actual_message, expected_message) self.syncer._send_prompt.assert_not_called() self.syncer._wait_for_confirmation.assert_not_called() - def test_confirmation_result_large_diff(self): + @helpers.async_test + async def test_confirmation_result_large_diff(self): """Should return True if confirmed and False if _send_prompt fails or aborted.""" self.syncer.MAX_DIFF = 3 author = helpers.MockMember() @@ -389,7 +400,7 @@ class SyncerSyncTests(unittest.TestCase): self.syncer._wait_for_confirmation = helpers.AsyncMock(return_value=confirmed) coro = self.syncer._get_confirmation_result(4, author) - actual_result, actual_message = asyncio.run(coro) + actual_result, actual_message = await coro self.syncer._send_prompt.assert_called_once_with(None) # message defaults to None self.assertIs(actual_result, expected_result) diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/cogs/sync/test_cog.py index f66adfea1..98c9afc0d 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/cogs/sync/test_cog.py @@ -1,4 +1,3 @@ -import asyncio import unittest from unittest import mock @@ -91,7 +90,8 @@ class SyncCogTests(SyncCogTestCase): sync_guild.assert_called_once_with() self.bot.loop.create_task.assert_called_once_with(mock_sync_guild_coro) - def test_sync_cog_sync_guild(self): + @helpers.async_test + async def test_sync_cog_sync_guild(self): """Roles and users should be synced only if a guild is successfully retrieved.""" for guild in (helpers.MockGuild(), None): with self.subTest(guild=guild): @@ -101,7 +101,7 @@ class SyncCogTests(SyncCogTestCase): self.bot.get_guild = mock.MagicMock(return_value=guild) - asyncio.run(self.cog.sync_guild()) + await self.cog.sync_guild() self.bot.wait_until_guild_available.assert_called_once() self.bot.get_guild.assert_called_once_with(constants.Guild.id) @@ -113,29 +113,31 @@ class SyncCogTests(SyncCogTestCase): self.cog.role_syncer.sync.assert_called_once_with(guild) self.cog.user_syncer.sync.assert_called_once_with(guild) - def patch_user_helper(self, side_effect: BaseException) -> None: + async def patch_user_helper(self, side_effect: BaseException) -> None: """Helper to set a side effect for bot.api_client.patch and then assert it is called.""" self.bot.api_client.patch.reset_mock(side_effect=True) self.bot.api_client.patch.side_effect = side_effect user_id, updated_information = 5, {"key": 123} - asyncio.run(self.cog.patch_user(user_id, updated_information)) + await self.cog.patch_user(user_id, updated_information) self.bot.api_client.patch.assert_called_once_with( f"bot/users/{user_id}", json=updated_information, ) - def test_sync_cog_patch_user(self): + @helpers.async_test + async def test_sync_cog_patch_user(self): """A PATCH request should be sent and 404 errors ignored.""" for side_effect in (None, self.response_error(404)): with self.subTest(side_effect=side_effect): - self.patch_user_helper(side_effect) + await self.patch_user_helper(side_effect) - def test_sync_cog_patch_user_non_404(self): + @helpers.async_test + async def test_sync_cog_patch_user_non_404(self): """A PATCH request should be sent and the error raised if it's not a 404.""" with self.assertRaises(ResponseCodeError): - self.patch_user_helper(self.response_error(500)) + await self.patch_user_helper(self.response_error(500)) class SyncCogListenerTests(SyncCogTestCase): @@ -145,7 +147,8 @@ class SyncCogListenerTests(SyncCogTestCase): super().setUp() self.cog.patch_user = helpers.AsyncMock(spec_set=self.cog.patch_user) - def test_sync_cog_on_guild_role_create(self): + @helpers.async_test + async def test_sync_cog_on_guild_role_create(self): """A POST request should be sent with the new role's data.""" self.assertTrue(self.cog.on_guild_role_create.__cog_listener__) @@ -157,20 +160,22 @@ class SyncCogListenerTests(SyncCogTestCase): "position": 23, } role = helpers.MockRole(**role_data) - asyncio.run(self.cog.on_guild_role_create(role)) + await self.cog.on_guild_role_create(role) self.bot.api_client.post.assert_called_once_with("bot/roles", json=role_data) - def test_sync_cog_on_guild_role_delete(self): + @helpers.async_test + async def test_sync_cog_on_guild_role_delete(self): """A DELETE request should be sent.""" self.assertTrue(self.cog.on_guild_role_delete.__cog_listener__) role = helpers.MockRole(id=99) - asyncio.run(self.cog.on_guild_role_delete(role)) + await self.cog.on_guild_role_delete(role) self.bot.api_client.delete.assert_called_once_with("bot/roles/99") - def test_sync_cog_on_guild_role_update(self): + @helpers.async_test + async def test_sync_cog_on_guild_role_update(self): """A PUT request should be sent if the colour, name, permissions, or position changes.""" self.assertTrue(self.cog.on_guild_role_update.__cog_listener__) @@ -197,7 +202,7 @@ class SyncCogListenerTests(SyncCogTestCase): before_role = helpers.MockRole(**role_data) after_role = helpers.MockRole(**after_role_data) - asyncio.run(self.cog.on_guild_role_update(before_role, after_role)) + await self.cog.on_guild_role_update(before_role, after_role) if should_put: self.bot.api_client.put.assert_called_once_with( @@ -207,19 +212,21 @@ class SyncCogListenerTests(SyncCogTestCase): else: self.bot.api_client.put.assert_not_called() - def test_sync_cog_on_member_remove(self): + @helpers.async_test + async def test_sync_cog_on_member_remove(self): """Member should patched to set in_guild as False.""" self.assertTrue(self.cog.on_member_remove.__cog_listener__) member = helpers.MockMember() - asyncio.run(self.cog.on_member_remove(member)) + await self.cog.on_member_remove(member) self.cog.patch_user.assert_called_once_with( member.id, updated_information={"in_guild": False} ) - def test_sync_cog_on_member_update_roles(self): + @helpers.async_test + async def test_sync_cog_on_member_update_roles(self): """Members should be patched if their roles have changed.""" self.assertTrue(self.cog.on_member_update.__cog_listener__) @@ -228,12 +235,13 @@ class SyncCogListenerTests(SyncCogTestCase): before_member = helpers.MockMember(roles=before_roles) after_member = helpers.MockMember(roles=before_roles[1:]) - asyncio.run(self.cog.on_member_update(before_member, after_member)) + await self.cog.on_member_update(before_member, after_member) data = {"roles": sorted(role.id for role in after_member.roles)} self.cog.patch_user.assert_called_once_with(after_member.id, updated_information=data) - def test_sync_cog_on_member_update_other(self): + @helpers.async_test + async def test_sync_cog_on_member_update_other(self): """Members should not be patched if other attributes have changed.""" self.assertTrue(self.cog.on_member_update.__cog_listener__) @@ -250,11 +258,12 @@ class SyncCogListenerTests(SyncCogTestCase): before_member = helpers.MockMember(**{attribute: old_value}) after_member = helpers.MockMember(**{attribute: new_value}) - asyncio.run(self.cog.on_member_update(before_member, after_member)) + await self.cog.on_member_update(before_member, after_member) self.cog.patch_user.assert_not_called() - def test_sync_cog_on_user_update(self): + @helpers.async_test + async def test_sync_cog_on_user_update(self): """A user should be patched only if the name, discriminator, or avatar changes.""" self.assertTrue(self.cog.on_user_update.__cog_listener__) @@ -281,7 +290,7 @@ class SyncCogListenerTests(SyncCogTestCase): before_user = helpers.MockUser(**before_data) after_user = helpers.MockUser(**after_data) - asyncio.run(self.cog.on_user_update(before_user, after_user)) + await self.cog.on_user_update(before_user, after_user) if should_patch: self.cog.patch_user.assert_called_once() @@ -297,7 +306,7 @@ class SyncCogListenerTests(SyncCogTestCase): else: self.cog.patch_user.assert_not_called() - def on_member_join_helper(self, side_effect: Exception) -> dict: + async def on_member_join_helper(self, side_effect: Exception) -> dict: """ Helper to set `side_effect` for on_member_join and assert a PUT request was sent. @@ -321,7 +330,7 @@ class SyncCogListenerTests(SyncCogTestCase): self.bot.api_client.put.side_effect = side_effect try: - asyncio.run(self.cog.on_member_join(member)) + await self.cog.on_member_join(member) except Exception: raise finally: @@ -332,38 +341,44 @@ class SyncCogListenerTests(SyncCogTestCase): return data - def test_sync_cog_on_member_join(self): + @helpers.async_test + async def test_sync_cog_on_member_join(self): """Should PUT user's data or POST it if the user doesn't exist.""" for side_effect in (None, self.response_error(404)): with self.subTest(side_effect=side_effect): self.bot.api_client.post.reset_mock() - data = self.on_member_join_helper(side_effect) + data = await self.on_member_join_helper(side_effect) if side_effect: self.bot.api_client.post.assert_called_once_with("bot/users", json=data) else: self.bot.api_client.post.assert_not_called() - def test_sync_cog_on_member_join_non_404(self): + @helpers.async_test + async def test_sync_cog_on_member_join_non_404(self): """ResponseCodeError should be re-raised if status code isn't a 404.""" - self.assertRaises(ResponseCodeError, self.on_member_join_helper, self.response_error(500)) + with self.assertRaises(ResponseCodeError): + await self.on_member_join_helper(self.response_error(500)) + self.bot.api_client.post.assert_not_called() class SyncCogCommandTests(SyncCogTestCase, CommandTestCase): """Tests for the commands in the Sync cog.""" - def test_sync_roles_command(self): + @helpers.async_test + async def test_sync_roles_command(self): """sync() should be called on the RoleSyncer.""" ctx = helpers.MockContext() - asyncio.run(self.cog.sync_roles_command.callback(self.cog, ctx)) + await self.cog.sync_roles_command.callback(self.cog, ctx) self.cog.role_syncer.sync.assert_called_once_with(ctx.guild, ctx) - def test_sync_users_command(self): + @helpers.async_test + async def test_sync_users_command(self): """sync() should be called on the UserSyncer.""" ctx = helpers.MockContext() - asyncio.run(self.cog.sync_users_command.callback(self.cog, ctx)) + await self.cog.sync_users_command.callback(self.cog, ctx) self.cog.user_syncer.sync.assert_called_once_with(ctx.guild, ctx) diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/cogs/sync/test_roles.py index 8324b99cd..14fb2577a 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/cogs/sync/test_roles.py @@ -1,4 +1,3 @@ -import asyncio import unittest from unittest import mock @@ -40,53 +39,58 @@ class RoleSyncerDiffTests(unittest.TestCase): return guild - def test_empty_diff_for_identical_roles(self): + @helpers.async_test + async def test_empty_diff_for_identical_roles(self): """No differences should be found if the roles in the guild and DB are identical.""" self.bot.api_client.get.return_value = [fake_role()] guild = self.get_guild(fake_role()) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = (set(), set(), set()) self.assertEqual(actual_diff, expected_diff) - def test_diff_for_updated_roles(self): + @helpers.async_test + async def test_diff_for_updated_roles(self): """Only updated roles should be added to the 'updated' set of the diff.""" updated_role = fake_role(id=41, name="new") self.bot.api_client.get.return_value = [fake_role(id=41, name="old"), fake_role()] guild = self.get_guild(updated_role, fake_role()) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = (set(), {_Role(**updated_role)}, set()) self.assertEqual(actual_diff, expected_diff) - def test_diff_for_new_roles(self): + @helpers.async_test + async def test_diff_for_new_roles(self): """Only new roles should be added to the 'created' set of the diff.""" new_role = fake_role(id=41, name="new") self.bot.api_client.get.return_value = [fake_role()] guild = self.get_guild(fake_role(), new_role) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = ({_Role(**new_role)}, set(), set()) self.assertEqual(actual_diff, expected_diff) - def test_diff_for_deleted_roles(self): + @helpers.async_test + async def test_diff_for_deleted_roles(self): """Only deleted roles should be added to the 'deleted' set of the diff.""" deleted_role = fake_role(id=61, name="deleted") self.bot.api_client.get.return_value = [fake_role(), deleted_role] guild = self.get_guild(fake_role()) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = (set(), set(), {_Role(**deleted_role)}) self.assertEqual(actual_diff, expected_diff) - def test_diff_for_new_updated_and_deleted_roles(self): + @helpers.async_test + async def test_diff_for_new_updated_and_deleted_roles(self): """When roles are added, updated, and removed, all of them are returned properly.""" new = fake_role(id=41, name="new") updated = fake_role(id=71, name="updated") @@ -99,7 +103,7 @@ class RoleSyncerDiffTests(unittest.TestCase): ] guild = self.get_guild(fake_role(), new, updated) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = ({_Role(**new)}, {_Role(**updated)}, {_Role(**deleted)}) self.assertEqual(actual_diff, expected_diff) @@ -112,13 +116,14 @@ class RoleSyncerSyncTests(unittest.TestCase): self.bot = helpers.MockBot() self.syncer = RoleSyncer(self.bot) - def test_sync_created_roles(self): + @helpers.async_test + async def test_sync_created_roles(self): """Only POST requests should be made with the correct payload.""" roles = [fake_role(id=111), fake_role(id=222)] role_tuples = {_Role(**role) for role in roles} diff = _Diff(role_tuples, set(), set()) - asyncio.run(self.syncer._sync(diff)) + await self.syncer._sync(diff) calls = [mock.call("bot/roles", json=role) for role in roles] self.bot.api_client.post.assert_has_calls(calls, any_order=True) @@ -127,13 +132,14 @@ class RoleSyncerSyncTests(unittest.TestCase): self.bot.api_client.put.assert_not_called() self.bot.api_client.delete.assert_not_called() - def test_sync_updated_roles(self): + @helpers.async_test + async def test_sync_updated_roles(self): """Only PUT requests should be made with the correct payload.""" roles = [fake_role(id=111), fake_role(id=222)] role_tuples = {_Role(**role) for role in roles} diff = _Diff(set(), role_tuples, set()) - asyncio.run(self.syncer._sync(diff)) + await self.syncer._sync(diff) calls = [mock.call(f"bot/roles/{role['id']}", json=role) for role in roles] self.bot.api_client.put.assert_has_calls(calls, any_order=True) @@ -142,13 +148,14 @@ class RoleSyncerSyncTests(unittest.TestCase): self.bot.api_client.post.assert_not_called() self.bot.api_client.delete.assert_not_called() - def test_sync_deleted_roles(self): + @helpers.async_test + async def test_sync_deleted_roles(self): """Only DELETE requests should be made with the correct payload.""" roles = [fake_role(id=111), fake_role(id=222)] role_tuples = {_Role(**role) for role in roles} diff = _Diff(set(), set(), role_tuples) - asyncio.run(self.syncer._sync(diff)) + await self.syncer._sync(diff) calls = [mock.call(f"bot/roles/{role['id']}") for role in roles] self.bot.api_client.delete.assert_has_calls(calls, any_order=True) diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/cogs/sync/test_users.py index e9f9db2ea..421bf6bb6 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/cogs/sync/test_users.py @@ -1,4 +1,3 @@ -import asyncio import unittest from unittest import mock @@ -43,62 +42,68 @@ class UserSyncerDiffTests(unittest.TestCase): return guild - def test_empty_diff_for_no_users(self): + @helpers.async_test + async def test_empty_diff_for_no_users(self): """When no users are given, an empty diff should be returned.""" guild = self.get_guild() - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = (set(), set(), None) self.assertEqual(actual_diff, expected_diff) - def test_empty_diff_for_identical_users(self): + @helpers.async_test + async def test_empty_diff_for_identical_users(self): """No differences should be found if the users in the guild and DB are identical.""" self.bot.api_client.get.return_value = [fake_user()] guild = self.get_guild(fake_user()) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = (set(), set(), None) self.assertEqual(actual_diff, expected_diff) - def test_diff_for_updated_users(self): + @helpers.async_test + async def test_diff_for_updated_users(self): """Only updated users should be added to the 'updated' set of the diff.""" updated_user = fake_user(id=99, name="new") self.bot.api_client.get.return_value = [fake_user(id=99, name="old"), fake_user()] guild = self.get_guild(updated_user, fake_user()) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = (set(), {_User(**updated_user)}, None) self.assertEqual(actual_diff, expected_diff) - def test_diff_for_new_users(self): + @helpers.async_test + async def test_diff_for_new_users(self): """Only new users should be added to the 'created' set of the diff.""" new_user = fake_user(id=99, name="new") self.bot.api_client.get.return_value = [fake_user()] guild = self.get_guild(fake_user(), new_user) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = ({_User(**new_user)}, set(), None) self.assertEqual(actual_diff, expected_diff) - def test_diff_sets_in_guild_false_for_leaving_users(self): + @helpers.async_test + async def test_diff_sets_in_guild_false_for_leaving_users(self): """When a user leaves the guild, the `in_guild` flag is updated to `False`.""" leaving_user = fake_user(id=63, in_guild=False) self.bot.api_client.get.return_value = [fake_user(), fake_user(id=63)] guild = self.get_guild(fake_user()) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = (set(), {_User(**leaving_user)}, None) self.assertEqual(actual_diff, expected_diff) - def test_diff_for_new_updated_and_leaving_users(self): + @helpers.async_test + async def test_diff_for_new_updated_and_leaving_users(self): """When users are added, updated, and removed, all of them are returned properly.""" new_user = fake_user(id=99, name="new") updated_user = fake_user(id=55, name="updated") @@ -107,17 +112,18 @@ class UserSyncerDiffTests(unittest.TestCase): self.bot.api_client.get.return_value = [fake_user(), fake_user(id=55), fake_user(id=63)] guild = self.get_guild(fake_user(), new_user, updated_user) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = ({_User(**new_user)}, {_User(**updated_user), _User(**leaving_user)}, None) self.assertEqual(actual_diff, expected_diff) - def test_empty_diff_for_db_users_not_in_guild(self): + @helpers.async_test + async def test_empty_diff_for_db_users_not_in_guild(self): """When the DB knows a user the guild doesn't, no difference is found.""" self.bot.api_client.get.return_value = [fake_user(), fake_user(id=63, in_guild=False)] guild = self.get_guild(fake_user()) - actual_diff = asyncio.run(self.syncer._get_diff(guild)) + actual_diff = await self.syncer._get_diff(guild) expected_diff = (set(), set(), None) self.assertEqual(actual_diff, expected_diff) @@ -130,13 +136,14 @@ class UserSyncerSyncTests(unittest.TestCase): self.bot = helpers.MockBot() self.syncer = UserSyncer(self.bot) - def test_sync_created_users(self): + @helpers.async_test + async def test_sync_created_users(self): """Only POST requests should be made with the correct payload.""" users = [fake_user(id=111), fake_user(id=222)] user_tuples = {_User(**user) for user in users} diff = _Diff(user_tuples, set(), None) - asyncio.run(self.syncer._sync(diff)) + await self.syncer._sync(diff) calls = [mock.call("bot/users", json=user) for user in users] self.bot.api_client.post.assert_has_calls(calls, any_order=True) @@ -145,13 +152,14 @@ class UserSyncerSyncTests(unittest.TestCase): self.bot.api_client.put.assert_not_called() self.bot.api_client.delete.assert_not_called() - def test_sync_updated_users(self): + @helpers.async_test + async def test_sync_updated_users(self): """Only PUT requests should be made with the correct payload.""" users = [fake_user(id=111), fake_user(id=222)] user_tuples = {_User(**user) for user in users} diff = _Diff(set(), user_tuples, None) - asyncio.run(self.syncer._sync(diff)) + await self.syncer._sync(diff) calls = [mock.call(f"bot/users/{user['id']}", json=user) for user in users] self.bot.api_client.put.assert_has_calls(calls, any_order=True) -- cgit v1.2.3 From 22a55534ef13990815a6f69d361e2a12693075d5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 12 Feb 2020 09:16:46 -0800 Subject: Tests: fix unawaited error for MockAPIClient This error is due to the use of an actual instance of APIClient as the spec for the mock. recreate() is called in __init__ which in turn creates a task for the _create_session coroutine. The approach to the solution is to use the type for the spec rather than and instance, thus avoiding any call of __init__. However, without an instance, instance attributes will not be included in the spec. Therefore, they are defined as class attributes on the actual APIClient class definition and given default values. Alternatively, a subclass of APIClient could have been made in the tests.helpers module to define those class attributes. However, it seems easier to maintain if the attributes are in the original class definition. --- bot/api.py | 5 ++++- tests/helpers.py | 6 +----- 2 files changed, 5 insertions(+), 6 deletions(-) (limited to 'tests') diff --git a/bot/api.py b/bot/api.py index a9d2baa4d..d5880ba18 100644 --- a/bot/api.py +++ b/bot/api.py @@ -32,6 +32,9 @@ class ResponseCodeError(ValueError): class APIClient: """Django Site API wrapper.""" + session: Optional[aiohttp.ClientSession] = None + loop: asyncio.AbstractEventLoop = None + def __init__(self, loop: asyncio.AbstractEventLoop, **kwargs): auth_headers = { 'Authorization': f"Token {Keys.site_api}" @@ -42,7 +45,7 @@ class APIClient: else: kwargs['headers'] = auth_headers - self.session: Optional[aiohttp.ClientSession] = None + self.session = None self.loop = loop self._ready = asyncio.Event(loop=loop) diff --git a/tests/helpers.py b/tests/helpers.py index a40673bb9..9d9dd5da6 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -337,10 +337,6 @@ class MockUser(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): self.mention = f"@{self.name}" -# Create an APIClient instance to get a realistic MagicMock of `bot.api.APIClient` -api_client_instance = APIClient(loop=unittest.mock.MagicMock()) - - class MockAPIClient(CustomMockMixin, unittest.mock.MagicMock): """ A MagicMock subclass to mock APIClient objects. @@ -350,7 +346,7 @@ class MockAPIClient(CustomMockMixin, unittest.mock.MagicMock): """ def __init__(self, **kwargs) -> None: - super().__init__(spec_set=api_client_instance, **kwargs) + super().__init__(spec_set=APIClient, **kwargs) # Create a Bot instance to get a realistic MagicMock of `discord.ext.commands.Bot` -- cgit v1.2.3 From e5a7af3811f7f2687026254f44194b3a16459ca2 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 19 Feb 2020 09:26:42 -0800 Subject: Sync: add confirmation timeout and max diff to config --- bot/cogs/sync/syncers.py | 25 +++++++++++-------------- bot/constants.py | 7 +++++++ config-default.yml | 4 ++++ tests/bot/cogs/sync/test_base.py | 4 ++-- 4 files changed, 24 insertions(+), 16 deletions(-) (limited to 'tests') diff --git a/bot/cogs/sync/syncers.py b/bot/cogs/sync/syncers.py index 23039d1fc..43a8f2b62 100644 --- a/bot/cogs/sync/syncers.py +++ b/bot/cogs/sync/syncers.py @@ -26,9 +26,6 @@ class Syncer(abc.ABC): _CORE_DEV_MENTION = f"<@&{constants.Roles.core_developer}> " _REACTION_EMOJIS = (constants.Emojis.check_mark, constants.Emojis.cross_mark) - CONFIRM_TIMEOUT = 60 * 5 # 5 minutes - MAX_DIFF = 10 - def __init__(self, bot: Bot) -> None: self.bot = bot @@ -50,7 +47,7 @@ class Syncer(abc.ABC): msg_content = ( f'Possible cache issue while syncing {self.name}s. ' - f'More than {self.MAX_DIFF} {self.name}s were changed. ' + f'More than {constants.Sync.max_diff} {self.name}s were changed. ' f'React to confirm or abort the sync.' ) @@ -110,8 +107,8 @@ class Syncer(abc.ABC): Uses the `_reaction_check` function to determine if a reaction is valid. - If there is no reaction within `CONFIRM_TIMEOUT` seconds, return False. To acknowledge the - reaction (or lack thereof), `message` will be edited. + If there is no reaction within `bot.constants.Sync.confirm_timeout` seconds, return False. + To acknowledge the reaction (or lack thereof), `message` will be edited. """ # Preserve the core-dev role mention in the message edits so users aren't confused about # where notifications came from. @@ -123,7 +120,7 @@ class Syncer(abc.ABC): reaction, _ = await self.bot.wait_for( 'reaction_add', check=partial(self._reaction_check, author, message), - timeout=self.CONFIRM_TIMEOUT + timeout=constants.Sync.confirm_timeout ) except TimeoutError: # reaction will remain none thus sync will be aborted in the finally block below. @@ -159,15 +156,15 @@ class Syncer(abc.ABC): """ Prompt for confirmation and return a tuple of the result and the prompt message. - `diff_size` is the size of the diff of the sync. If it is greater than `MAX_DIFF`, the - prompt will be sent. The `author` is the invoked of the sync and the `message` is an extant - message to edit to display the prompt. + `diff_size` is the size of the diff of the sync. If it is greater than + `bot.constants.Sync.max_diff`, the prompt will be sent. The `author` is the invoked of the + sync and the `message` is an extant message to edit to display the prompt. If confirmed or no confirmation was needed, the result is True. The returned message will either be the given `message` or a new one which was created when sending the prompt. """ log.trace(f"Determining if confirmation prompt should be sent for {self.name} syncer.") - if diff_size > self.MAX_DIFF: + if diff_size > constants.Sync.max_diff: message = await self._send_prompt(message) if not message: return False, None # Couldn't get channel. @@ -182,9 +179,9 @@ class Syncer(abc.ABC): """ Synchronise the database with the cache of `guild`. - If the differences between the cache and the database are greater than `MAX_DIFF`, then - a confirmation prompt will be sent to the dev-core channel. The confirmation can be - optionally redirect to `ctx` instead. + If the differences between the cache and the database are greater than + `bot.constants.Sync.max_diff`, then a confirmation prompt will be sent to the dev-core + channel. The confirmation can be optionally redirect to `ctx` instead. """ log.info(f"Starting {self.name} syncer.") diff --git a/bot/constants.py b/bot/constants.py index 6279388de..81ce3e903 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -539,6 +539,13 @@ class RedirectOutput(metaclass=YAMLGetter): delete_delay: int +class Sync(metaclass=YAMLGetter): + section = 'sync' + + confirm_timeout: int + max_diff: int + + class Event(Enum): """ Event names. This does not include every event (for example, raw diff --git a/config-default.yml b/config-default.yml index 74dcc1862..0ebdc4080 100644 --- a/config-default.yml +++ b/config-default.yml @@ -430,6 +430,10 @@ redirect_output: delete_invocation: true delete_delay: 15 +sync: + confirm_timeout: 300 + max_diff: 10 + duck_pond: threshold: 5 custom_emojis: [*DUCKY_YELLOW, *DUCKY_BLURPLE, *DUCKY_CAMO, *DUCKY_DEVIL, *DUCKY_NINJA, *DUCKY_REGAL, *DUCKY_TUBE, *DUCKY_HUNT, *DUCKY_WIZARD, *DUCKY_PARTY, *DUCKY_ANGEL, *DUCKY_MAUL, *DUCKY_SANTA] diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py index 0539f5683..e6a6f9688 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/cogs/sync/test_base.py @@ -361,10 +361,10 @@ class SyncerSyncTests(unittest.TestCase): self.assertEqual(self.syncer._get_confirmation_result.call_args[0][1], author) self.assertEqual(self.syncer._get_confirmation_result.call_args[0][2], message) + @mock.patch.object(constants.Sync, "max_diff", new=3) @helpers.async_test async def test_confirmation_result_small_diff(self): """Should always return True and the given message if the diff size is too small.""" - self.syncer.MAX_DIFF = 3 author = helpers.MockMember() expected_message = helpers.MockMessage() @@ -381,10 +381,10 @@ class SyncerSyncTests(unittest.TestCase): self.syncer._send_prompt.assert_not_called() self.syncer._wait_for_confirmation.assert_not_called() + @mock.patch.object(constants.Sync, "max_diff", new=3) @helpers.async_test async def test_confirmation_result_large_diff(self): """Should return True if confirmed and False if _send_prompt fails or aborted.""" - self.syncer.MAX_DIFF = 3 author = helpers.MockMember() mock_message = helpers.MockMessage() -- cgit v1.2.3