diff options
| -rw-r--r-- | bot/cogs/jams.py | 59 | ||||
| -rw-r--r-- | tests/bot/cogs/test_jams.py | 151 | 
2 files changed, 193 insertions, 17 deletions
| diff --git a/bot/cogs/jams.py b/bot/cogs/jams.py index 1d062b0c2..a48dbc49a 100644 --- a/bot/cogs/jams.py +++ b/bot/cogs/jams.py @@ -1,6 +1,7 @@  import logging +import typing as t -from discord import Member, PermissionOverwrite, utils +from discord import CategoryChannel, Guild, Member, PermissionOverwrite, Role, utils  from discord.ext import commands  from more_itertools import unique_everseen @@ -40,22 +41,39 @@ class CodeJams(commands.Cog):              )              return -        code_jam_category = utils.get(ctx.guild.categories, name="Code Jam") +        team_channel = await self.create_channels(ctx.guild, team_name, members) +        await self.add_roles(ctx.guild, members) + +        await ctx.send( +            f":ok_hand: Team created: {team_channel}\n" +            f"**Team Leader:** {members[0].mention}\n" +            f"**Team Members:** {' '.join(member.mention for member in members[1:])}" +        ) + +    @staticmethod +    async def get_category(guild: Guild) -> CategoryChannel: +        """Create a Code Jam category if it doesn't exist and return it.""" +        code_jam_category = utils.get(guild.categories, name="Code Jam")          if code_jam_category is None:              log.info("Code Jam category not found, creating it.")              category_overwrites = { -                ctx.guild.default_role: PermissionOverwrite(read_messages=False), -                ctx.guild.me: PermissionOverwrite(read_messages=True) +                guild.default_role: PermissionOverwrite(read_messages=False), +                guild.me: PermissionOverwrite(read_messages=True)              } -            code_jam_category = await ctx.guild.create_category_channel( +            code_jam_category = await guild.create_category_channel(                  "Code Jam",                  overwrites=category_overwrites,                  reason="It's code jam time!"              ) +        return code_jam_category + +    @staticmethod +    def get_overwrites(members: t.List[Member], guild: Guild) -> t.Dict[t.Union[Member, Role], PermissionOverwrite]: +        """Get Code Jam team channels permission overwrites."""          # First member is always the team leader          team_channel_overwrites = {              members[0]: PermissionOverwrite( @@ -64,8 +82,8 @@ class CodeJams(commands.Cog):                  manage_webhooks=True,                  connect=True              ), -            ctx.guild.default_role: PermissionOverwrite(read_messages=False, connect=False), -            ctx.guild.get_role(Roles.verified): PermissionOverwrite( +            guild.default_role: PermissionOverwrite(read_messages=False, connect=False), +            guild.get_role(Roles.verified): PermissionOverwrite(                  read_messages=False,                  connect=False              ) @@ -78,8 +96,16 @@ class CodeJams(commands.Cog):                  connect=True              ) +        return team_channel_overwrites + +    async def create_channels(self, guild: Guild, team_name: str, members: t.List[Member]) -> str: +        """Create team text and voice channels. Return the mention for the text channel.""" +        # Get permission overwrites and category +        team_channel_overwrites = self.get_overwrites(members, guild) +        code_jam_category = await self.get_category(guild) +          # Create a text channel for the team -        team_channel = await ctx.guild.create_text_channel( +        team_channel = await guild.create_text_channel(              team_name,              overwrites=team_channel_overwrites,              category=code_jam_category @@ -88,26 +114,25 @@ class CodeJams(commands.Cog):          # Create a voice channel for the team          team_voice_name = " ".join(team_name.split("-")).title() -        await ctx.guild.create_voice_channel( +        await guild.create_voice_channel(              team_voice_name,              overwrites=team_channel_overwrites,              category=code_jam_category          ) +        return team_channel.mention + +    @staticmethod +    async def add_roles(guild: Guild, members: t.List[Member]) -> None: +        """Assign team leader and jammer roles."""          # Assign team leader role -        await members[0].add_roles(ctx.guild.get_role(Roles.team_leaders)) +        await members[0].add_roles(guild.get_role(Roles.team_leaders))          # Assign rest of roles -        jammer_role = ctx.guild.get_role(Roles.jammers) +        jammer_role = guild.get_role(Roles.jammers)          for member in members:              await member.add_roles(jammer_role) -        await ctx.send( -            f":ok_hand: Team created: {team_channel.mention}\n" -            f"**Team Leader:** {members[0].mention}\n" -            f"**Team Members:** {' '.join(member.mention for member in members[1:])}" -        ) -  def setup(bot: Bot) -> None:      """Load the CodeJams cog.""" diff --git a/tests/bot/cogs/test_jams.py b/tests/bot/cogs/test_jams.py new file mode 100644 index 000000000..81fbcb798 --- /dev/null +++ b/tests/bot/cogs/test_jams.py @@ -0,0 +1,151 @@ +import unittest +from unittest.mock import AsyncMock, MagicMock, patch + +from bot.cogs.jams import CodeJams, setup +from bot.constants import Roles +from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockTextChannel + + +class JamCreateTeamTests(unittest.IsolatedAsyncioTestCase): +    """Tests for `createteam` command.""" + +    def setUp(self): +        self.bot = MockBot() +        self.admin_role = MockRole(name="Admins", id=Roles.admins) +        self.command_user = MockMember([self.admin_role]) +        self.guild = MockGuild([self.admin_role]) +        self.ctx = MockContext(bot=self.bot, author=self.command_user, guild=self.guild) +        self.cog = CodeJams(self.bot) + +        utils_patcher = patch("bot.cogs.jams.utils") +        self.utils_mock = utils_patcher.start() +        self.addCleanup(utils_patcher.stop) + +    async def test_too_small_amount_of_team_members_passed(self): +        """Should `ctx.send` and exit early when too small amount of members.""" +        for case in (1, 2): +            with self.subTest(amount_of_members=case): +                self.cog.create_channels = AsyncMock() +                self.cog.add_roles = AsyncMock() + +                self.ctx.reset_mock() +                self.utils_mock.reset_mock() +                members = (MockMember() for _ in range(case)) +                await self.cog.createteam(self.cog, self.ctx, "foo", members) + +                self.ctx.send.assert_awaited_once() +                self.cog.create_channels.assert_not_awaited() +                self.cog.add_roles.assert_not_awaited() + +    async def test_duplicate_members_provided(self): +        """Should `ctx.send` and exit early because duplicate members provided and total there is only 1 member.""" +        self.cog.create_channels = AsyncMock() +        self.cog.add_roles = AsyncMock() + +        member = MockMember() +        await self.cog.createteam(self.cog, self.ctx, "foo", (member for _ in range(5))) + +        self.ctx.send.assert_awaited_once() +        self.cog.create_channels.assert_not_awaited() +        self.cog.add_roles.assert_not_awaited() + +    async def test_result_sending(self): +        """Should call `ctx.send` when everything goes right.""" +        self.cog.create_channels = AsyncMock() +        self.cog.add_roles = AsyncMock() + +        members = [MockMember() for _ in range(5)] +        await self.cog.createteam(self.cog, self.ctx, "foo", members) + +        self.cog.create_channels.assert_awaited_once() +        self.cog.add_roles.assert_awaited_once() +        self.ctx.send.assert_awaited_once() + +    async def test_category_dont_exist(self): +        """Should create code jam category.""" +        self.utils_mock.get.return_value = None + +        await self.cog.get_category(self.guild) + +        self.guild.create_category_channel.assert_awaited_once() +        category_overwrites = self.guild.create_category_channel.call_args[1]["overwrites"] + +        self.assertFalse(category_overwrites[self.guild.default_role].read_messages) +        self.assertTrue(category_overwrites[self.guild.me].read_messages) + +    async def test_category_channel_exist(self): +        """Should not try to create category channel.""" +        await self.cog.get_category(self.guild) +        self.guild.create_category_channel.assert_not_awaited() + +    async def test_channel_overwrites(self): +        """Should have correct permission overwrites for users and roles.""" +        leader = MockMember() +        members = [leader] + [MockMember() for _ in range(4)] +        overwrites = self.cog.get_overwrites(members, self.guild) + +        # Leader permission overwrites +        self.assertTrue(overwrites[leader].manage_messages) +        self.assertTrue(overwrites[leader].read_messages) +        self.assertTrue(overwrites[leader].manage_webhooks) +        self.assertTrue(overwrites[leader].connect) + +        # Other members permission overwrites +        for member in members[1:]: +            self.assertTrue(overwrites[member].read_messages) +            self.assertTrue(overwrites[member].connect) + +        # Everyone and verified role overwrite +        self.assertFalse(overwrites[self.guild.default_role].read_messages) +        self.assertFalse(overwrites[self.guild.default_role].connect) +        self.assertFalse(overwrites[self.guild.get_role(Roles.verified)].read_messages) +        self.assertFalse(overwrites[self.guild.get_role(Roles.verified)].connect) + +    async def test_team_channels_creation(self): +        """Should create new voice and text channel for team.""" +        self.utils_mock.get.return_value = "foo" +        members = [MockMember() for _ in range(5)] + +        self.cog.get_overwrites = MagicMock() +        self.cog.get_category = AsyncMock() +        self.ctx.guild.create_text_channel.return_value = MockTextChannel(mention="foobar-channel") +        actual = await self.cog.create_channels(self.guild, "my-team", members) + +        self.assertEqual("foobar-channel", actual) +        self.cog.get_overwrites.assert_called_once_with(members, self.guild) +        self.cog.get_category.assert_awaited_once_with(self.guild) + +        self.guild.create_text_channel.assert_awaited_once_with( +            "my-team", +            overwrites=self.cog.get_overwrites.return_value, +            category=self.cog.get_category.return_value +        ) +        self.guild.create_voice_channel.assert_awaited_once_with( +            "My Team", +            overwrites=self.cog.get_overwrites.return_value, +            category=self.cog.get_category.return_value +        ) + +    async def test_jam_roles_adding(self): +        """Should add team leader role to leader and jam role to every team member.""" +        leader_role = MockRole(name="Team Leader") +        jam_role = MockRole(name="Jammer") +        self.guild.get_role.side_effect = [leader_role, jam_role] + +        leader = MockMember() +        members = [leader] + [MockMember() for _ in range(4)] +        await self.cog.add_roles(self.guild, members) + +        leader.add_roles.assert_any_await(leader_role) +        for member in members: +            member.add_roles.assert_any_await(jam_role) + + +class CodeJamSetup(unittest.TestCase): +    """Test for `setup` function of `CodeJam` cog.""" + +    def test_setup(self): +        """Should call `bot.add_cog`.""" +        bot = MockBot() +        setup(bot) +        bot.add_cog.assert_called_once() | 
