diff --git a/src/webhooks/handlers/academy.py b/src/webhooks/handlers/academy.py index 19b2bfa..c8782f1 100644 --- a/src/webhooks/handlers/academy.py +++ b/src/webhooks/handlers/academy.py @@ -28,6 +28,7 @@ async def _handle_certificate_awarded(self, body: WebhookBody, bot: Bot) -> dict certificate_id = self.validate_property( self.get_property_or_trait(body, "certificate_id"), "certificate_id" ) + certificate_name = self.get_property_or_trait(body, "certificate_name") self.logger.info(f"Handling certificate awarded event for {discord_id} with certificate {certificate_id}") @@ -44,6 +45,20 @@ async def _handle_certificate_awarded(self, body: WebhookBody, bot: Bot) -> dict await member.add_roles( bot.guilds[0].get_role(certificate_role_id), atomic=True # type: ignore ) # type: ignore + + # Safely attempt to send verification log only after role addition + verify_channel = bot.guilds[0].get_channel(settings.channels.VERIFY_LOGS) + if verify_channel: + message = f"Certification linked: {certificate_name} with Certificate ID: {certificate_id} -> {member.mention} ({member.id})" + if not certificate_name: + message = f"Certification linked: Certificate ID: {certificate_id} -> {member.mention} ({member.id})" + + await verify_channel.send( # type: ignore + message, + ) + else: + self.logger.warning(f"Verify logs channel {settings.channels.VERIFY_LOGS} not found") + except Exception as e: self.logger.error(f"Error adding certificate role {certificate_role_id} to member {member.id}: {e}") raise e diff --git a/tests/src/webhooks/handlers/test_academy.py b/tests/src/webhooks/handlers/test_academy.py index fc34c63..3d0c336 100644 --- a/tests/src/webhooks/handlers/test_academy.py +++ b/tests/src/webhooks/handlers/test_academy.py @@ -12,7 +12,7 @@ async def test_handle_certificate_awarded_success(self, bot): discord_id = 123456789 account_id = 987654321 certificate_id = 42 - mock_member = helpers.MockMember(id=discord_id) + mock_member = helpers.MockMember(id=discord_id, name="123456789") mock_member.add_roles = AsyncMock() body = WebhookBody( platform=Platform.ACADEMY, @@ -21,22 +21,66 @@ async def test_handle_certificate_awarded_success(self, bot): "discord_id": discord_id, "account_id": account_id, "certificate_id": certificate_id, + "certificate_name": "Test Certificate", }, traits={}, ) with ( patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), - patch.object(handler, "validate_property", return_value=certificate_id), + patch.object(handler, "validate_property", side_effect=[certificate_id]), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.academy.settings") as mock_settings, patch.object(handler.logger, "info") as mock_log, ): mock_settings.get_academy_cert_role.return_value = 555 + mock_settings.channels.VERIFY_LOGS = 777 mock_guild = helpers.MockGuild(id=1) - mock_guild.get_role.return_value = 555 + mock_guild.get_role.return_value = MagicMock() + mock_channel = AsyncMock() + mock_guild.get_channel.return_value = mock_channel bot.guilds = [mock_guild] result = await handler._handle_certificate_awarded(body, bot) mock_member.add_roles.assert_awaited() + mock_channel.send.assert_awaited_once_with("Certification linked: Test Certificate with Certificate ID: 42 -> @123456789 (123456789)") + mock_log.assert_called() + assert result == handler.success() + + @pytest.mark.asyncio + async def test_handle_certificate_awarded_success_no_name(self, bot): + handler = AcademyHandler() + discord_id = 123456789 + account_id = 987654321 + certificate_id = 42 + mock_member = helpers.MockMember(id=discord_id, name="123456789") + mock_member.add_roles = AsyncMock() + body = WebhookBody( + platform=Platform.ACADEMY, + event=WebhookEvent.CERTIFICATE_AWARDED, + properties={ + "discord_id": discord_id, + "account_id": account_id, + "certificate_id": certificate_id, + # certificate_name missing + }, + traits={}, + ) + with ( + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), + patch.object(handler, "validate_property", side_effect=[certificate_id]), + patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), + patch("src.webhooks.handlers.academy.settings") as mock_settings, + patch.object(handler.logger, "info") as mock_log, + ): + mock_settings.get_academy_cert_role.return_value = 555 + mock_settings.channels.VERIFY_LOGS = 777 + mock_guild = helpers.MockGuild(id=1) + mock_guild.get_role.return_value = MagicMock() + mock_channel = AsyncMock() + mock_guild.get_channel.return_value = mock_channel + bot.guilds = [mock_guild] + result = await handler._handle_certificate_awarded(body, bot) + mock_member.add_roles.assert_awaited() + mock_channel.send.assert_awaited_once_with("Certification linked: Certificate ID: 42 -> @123456789 (123456789)") mock_log.assert_called() assert result == handler.success() @@ -46,7 +90,7 @@ async def test_handle_certificate_awarded_no_role(self, bot): discord_id = 123456789 account_id = 987654321 certificate_id = 42 - mock_member = helpers.MockMember(id=discord_id) + mock_member = helpers.MockMember(id=discord_id, name="123456789") body = WebhookBody( platform=Platform.ACADEMY, event=WebhookEvent.CERTIFICATE_AWARDED, @@ -54,12 +98,13 @@ async def test_handle_certificate_awarded_no_role(self, bot): "discord_id": discord_id, "account_id": account_id, "certificate_id": certificate_id, + "certificate_name": "Test Certificate", }, traits={}, ) with ( patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), - patch.object(handler, "validate_property", return_value=certificate_id), + patch.object(handler, "validate_property", side_effect=[certificate_id]), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.academy.settings") as mock_settings, patch.object(handler.logger, "warning") as mock_log, @@ -75,7 +120,7 @@ async def test_handle_certificate_awarded_add_roles_error(self, bot): discord_id = 123456789 account_id = 987654321 certificate_id = 42 - mock_member = helpers.MockMember(id=discord_id) + mock_member = helpers.MockMember(id=discord_id, name="123456789") mock_member.add_roles = AsyncMock(side_effect=Exception("add_roles error")) body = WebhookBody( platform=Platform.ACADEMY, @@ -84,23 +129,28 @@ async def test_handle_certificate_awarded_add_roles_error(self, bot): "discord_id": discord_id, "account_id": account_id, "certificate_id": certificate_id, + "certificate_name": "Test Certificate", }, traits={}, ) with ( patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), - patch.object(handler, "validate_property", return_value=certificate_id), + patch.object(handler, "validate_property", side_effect=[certificate_id]), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.academy.settings") as mock_settings, patch.object(handler.logger, "error") as mock_log, ): mock_settings.get_academy_cert_role.return_value = 555 + mock_settings.channels.VERIFY_LOGS = 777 mock_guild = helpers.MockGuild(id=1) - mock_guild.get_role.return_value = 555 + mock_guild.get_role.return_value = MagicMock() + mock_channel = AsyncMock() + mock_guild.get_channel.return_value = mock_channel bot.guilds = [mock_guild] with pytest.raises(Exception, match="add_roles error"): await handler._handle_certificate_awarded(body, bot) mock_log.assert_called() + mock_channel.send.assert_not_called() @pytest.mark.asyncio async def test_handle_subscription_change_success(self, bot): @@ -108,7 +158,7 @@ async def test_handle_subscription_change_success(self, bot): discord_id = 123456789 account_id = 987654321 plan = "Silver Annual" - mock_member = helpers.MockMember(id=discord_id) + mock_member = helpers.MockMember(id=discord_id, name="123456789") mock_member.roles = [] mock_member.add_roles = AsyncMock() mock_member.remove_roles = AsyncMock() @@ -147,7 +197,7 @@ async def test_handle_subscription_change_no_role(self, bot): discord_id = 123456789 account_id = 987654321 plan = "invalid_plan" - mock_member = helpers.MockMember(id=discord_id) + mock_member = helpers.MockMember(id=discord_id, name="123456789") body = WebhookBody( platform=Platform.ACADEMY, event=WebhookEvent.SUBSCRIPTION_CHANGE, @@ -181,7 +231,7 @@ async def test_handle_subscription_change_role_swap(self, bot): # Mock member with an existing academy subscription role old_role = MagicMock() old_role.id = 666 - mock_member = helpers.MockMember(id=discord_id) + mock_member = helpers.MockMember(id=discord_id, name="123456789") mock_member.roles = [old_role] mock_member.add_roles = AsyncMock() mock_member.remove_roles = AsyncMock()