Repository: python-discord/bot

Issue:
Write unit tests for `bot/cogs/jams.py`
Write unit tests for [`bot/cogs/jams.py`](../blob/master/bot/cogs/jams.py).

## Implementation details
Please make sure to read the general information in the [meta issue](553) and the [testing README](../blob/master/tests/README.md). We are aiming for a 100% [branch coverage](https://coverage.readthedocs.io/en/stable/branch.html) for this file, but if you think that is not possible, please discuss that in this issue.

## Additional information
If you want to work on this issue, **please make sure that you get assigned to it** by one of the core devs before starting to work on it. We would like to prevent the situation that multiple people are working on the same issue. To get assigned, leave a comment showing your interesting in tackling this issue.



Discussion:
I'm gonna work with this issue.

Resolving patch:
diff --git a/bot/cogs/jams.py b/bot/cogs/jams.py
--- 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 @@ async def createteam(self, ctx: commands.Context, team_name: str, members: comma
             )
             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 @@ async def createteam(self, ctx: commands.Context, team_name: str, members: comma
                 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 @@ async def createteam(self, ctx: commands.Context, team_name: str, members: comma
                 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 @@ async def createteam(self, ctx: commands.Context, team_name: str, members: comma
         # 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."""


Test patch:
diff --git a/tests/bot/cogs/test_jams.py b/tests/bot/cogs/test_jams.py
new file mode 100644
--- /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()