diff --git a/.test.env b/.test.env index 959024d..bc1a7ec 100644 --- a/.test.env +++ b/.test.env @@ -60,6 +60,8 @@ ROLE_NOOB=8377301011276950695 ROLE_VIP=9583772810112769506 ROLE_VIP_PLUS=9583772910112769506 +ROLE_SILVER_ANNUAL = 1432409954337296384 +ROLE_GOLD_ANNUAL = 1432410047782064232 ROLE_CHALLENGE_CREATOR=8215461011276950716 ROLE_BOX_CREATOR=8215471011276950716 diff --git a/src/core/config.py b/src/core/config.py index f2a5e42..8c33e0d 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -113,8 +113,12 @@ class Roles(BaseSettings): HACKER: int SCRIPT_KIDDIE: int NOOB: int + + # Subscriptions VIP: int VIP_PLUS: int + SILVER_ANNUAL: int + GOLD_ANNUAL: int # Content Creation CHALLENGE_CREATOR: int @@ -249,6 +253,8 @@ def get_post_or_rank(self, what: str) -> Optional[int]: "Challenge Creator": self.roles.CHALLENGE_CREATOR, "Box Creator": self.roles.BOX_CREATOR, "Sherlock Creator": self.roles.SHERLOCK_CREATOR, + "Silver Annual": self.roles.SILVER_ANNUAL, + "Gold Annual": self.roles.GOLD_ANNUAL, }.get(what) def get_season(self, what: str): @@ -336,8 +342,6 @@ def load_settings(env_file: str | None = None): global_settings.roles.HACKER, global_settings.roles.SCRIPT_KIDDIE, global_settings.roles.NOOB, - global_settings.roles.VIP, - global_settings.roles.VIP_PLUS, ], "ALL_SEASON_RANKS": [ global_settings.roles.SEASON_HOLO, @@ -355,6 +359,14 @@ def load_settings(env_file: str | None = None): global_settings.roles.RANK_ONE, global_settings.roles.RANK_TEN, ], + "ALL_LABS_SUBSCRIPTIONS": [ + global_settings.roles.VIP, + global_settings.roles.VIP_PLUS, + ], + "ALL_ACADEMY_SUBSCRIPTIONS": [ + global_settings.roles.SILVER_ANNUAL, + global_settings.roles.GOLD_ANNUAL, + ] } return global_settings diff --git a/src/webhooks/handlers/academy.py b/src/webhooks/handlers/academy.py index a57785c..19b2bfa 100644 --- a/src/webhooks/handlers/academy.py +++ b/src/webhooks/handlers/academy.py @@ -15,6 +15,8 @@ async def handle(self, body: WebhookBody, bot: Bot): """ if body.event == WebhookEvent.CERTIFICATE_AWARDED: return await self._handle_certificate_awarded(body, bot) + if body.event == WebhookEvent.SUBSCRIPTION_CHANGE: + return await self._handle_subscription_change(body, bot) else: raise ValueError(f"Invalid event: {body.event}") @@ -22,8 +24,7 @@ async def _handle_certificate_awarded(self, body: WebhookBody, bot: Bot) -> dict """ Handles the certificate awarded event. """ - discord_id = self.validate_discord_id(self.get_property_or_trait(body, "discord_id")) - _ = self.validate_account_id(self.get_property_or_trait(body, "account_id")) + discord_id, _ = self.validate_common_properties(body) certificate_id = self.validate_property( self.get_property_or_trait(body, "certificate_id"), "certificate_id" ) @@ -48,3 +49,24 @@ async def _handle_certificate_awarded(self, body: WebhookBody, bot: Bot) -> dict raise e return self.success() + + async def _handle_subscription_change(self, body: WebhookBody, bot: Bot) -> dict: + """ + Handles the subscription change event. + """ + discord_id, _ = self.validate_common_properties(body) + plan = self.validate_property(self.get_property_or_trait(body, "plan"), "plan") + + self.logger.info(f"Handling subscription change event for {discord_id} with plan {plan}") + + member = await self.get_guild_member(discord_id, bot) + subscription_role_id = settings.get_post_or_rank(plan) + if not subscription_role_id: + self.logger.warning(f"No subscription role found for plan {plan}") + return self.fail() + + # Use the base handler's role swapping method + role_group = [int(r) for r in settings.role_groups["ALL_ACADEMY_SUBSCRIPTIONS"]] + await self.swap_role_in_group(member, subscription_role_id, role_group, bot) + + return self.success() \ No newline at end of file diff --git a/src/webhooks/handlers/account.py b/src/webhooks/handlers/account.py index 58591e7..517f4a3 100644 --- a/src/webhooks/handlers/account.py +++ b/src/webhooks/handlers/account.py @@ -33,8 +33,7 @@ async def _handle_account_linked(self, body: WebhookBody, bot: Bot) -> dict: """ Handles the account linked event. """ - discord_id = self.validate_discord_id(self.get_property_or_trait(body, "discord_id")) - account_id = self.validate_account_id(self.get_property_or_trait(body, "account_id")) + discord_id, account_id = self.validate_common_properties(body) member = await self.get_guild_member(discord_id, bot) await process_account_identification( @@ -67,8 +66,7 @@ async def _handle_account_unlinked(self, body: WebhookBody, bot: Bot) -> dict: """ Handles the account unlinked event. """ - discord_id = self.validate_discord_id(self.get_property_or_trait(body, "discord_id")) - account_id = self.validate_account_id(self.get_property_or_trait(body, "account_id")) + discord_id, account_id = self.validate_common_properties(body) member = await self.get_guild_member(discord_id, bot) @@ -95,8 +93,7 @@ async def _handle_account_banned(self, body: WebhookBody, bot: Bot) -> dict: """ Handles the account banned event. """ - discord_id = self.validate_discord_id(self.get_property_or_trait(body, "discord_id")) - account_id = self.validate_account_id(self.get_property_or_trait(body, "account_id")) + discord_id, account_id = self.validate_common_properties(body) expires_at = self.validate_property(self.get_property_or_trait(body, "expires_at"), "expires_at") reason = body.properties.get("reason") notes = body.properties.get("notes") diff --git a/src/webhooks/handlers/base.py b/src/webhooks/handlers/base.py index b604f01..aebcacb 100644 --- a/src/webhooks/handlers/base.py +++ b/src/webhooks/handlers/base.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from typing import TypeVar -from discord import Bot, Member +from discord import Bot, Member, Role from discord.errors import NotFound from fastapi import HTTPException @@ -41,7 +41,7 @@ async def get_guild_member(self, discord_id: int | str, bot: Bot) -> Member: Raises: HTTPException: If the user is not in the Discord server (400) """ - + try: guild = await bot.fetch_guild(settings.guild_ids[0]) member = await guild.fetch_member(int(discord_id)) @@ -119,10 +119,113 @@ def get_platform_properties(self, body: WebhookBody) -> dict[str, int | None]: } return properties + async def swap_role_in_group( + self, + member: Member, + new_role_id: int | None, + role_group: list[int], + bot: Bot, + allow_no_role: bool = False, + ) -> bool: + """ + Swaps a member's role within a specific role group. + + This method removes any existing role from the specified group and adds the new role. + + Args: + member: The Discord member to modify + new_role_id: ID of the new role to assign (None to remove all roles from group) + role_group: List of role IDs that are mutually exclusive + bot: The Discord bot instance + allow_no_role: If True, allows removing all roles without adding a new one + + Returns: + bool: True if changes were made, False if no changes needed + + Raises: + ValueError: If new_role_id is invalid or not in the role group + """ + # Get all roles from the group as Discord Role objects + group_roles = [ + bot.guilds[0].get_role(role_id) + for role_id in role_group + if bot.guilds[0].get_role(role_id) + ] + + # Find current role from this group that the member has + current_role = next( + (role for role in member.roles if role in group_roles), None + ) + + # Get the new role object if specified + new_role = None + if new_role_id: + new_role = bot.guilds[0].get_role(new_role_id) + if not new_role: + raise ValueError(f"Invalid role ID: {new_role_id}") + + # Verify the new role is in the allowed group + if new_role not in group_roles: + raise ValueError( + f"Role {new_role_id} is not in the specified role group" + ) + + # If no change needed, return early + if current_role == new_role: + return False + + # If we're trying to remove all roles but it's not allowed + if not new_role and not allow_no_role and current_role: + raise ValueError( + "Cannot remove role without replacement when allow_no_role=False" + ) + + # Remove current role if it exists + if current_role: + await member.remove_roles(current_role, atomic=True) + + # Add new role if specified + if new_role: + await member.add_roles(new_role, atomic=True) + + return True + + def validate_common_properties( + self, body: WebhookBody + ) -> tuple[int | str, int | str]: + """ + Validates and returns the common discord_id and account_id properties. + + Args: + body: The webhook body containing properties/traits + + Returns: + tuple: (discord_id, account_id) + + Raises: + HTTPException: If either property is missing or invalid + """ + discord_id = self.validate_discord_id( + self.get_property_or_trait(body, "discord_id") + ) + account_id = self.validate_account_id( + self.get_property_or_trait(body, "account_id") + ) + return discord_id, account_id + + async def _find_user_with_role(self, bot: Bot, role: Role | None) -> Member | None: + """ + Finds the user with the given role. + """ + if not role: + return None + + return next((m for m in role.members), None) + @staticmethod def success(): return {"success": True} - + @staticmethod def fail(): - return {"success": False} \ No newline at end of file + return {"success": False} diff --git a/src/webhooks/handlers/mp.py b/src/webhooks/handlers/mp.py index 7221ba6..bc8a842 100644 --- a/src/webhooks/handlers/mp.py +++ b/src/webhooks/handlers/mp.py @@ -1,10 +1,6 @@ -import discord - -from datetime import datetime from discord import Bot, Member, Role from typing import Literal -from sqlalchemy import select from src.core import settings from src.webhooks.handlers.base import BaseHandler @@ -34,19 +30,21 @@ async def _handle_subscription_change(self, body: WebhookBody, bot: Bot) -> dict """ Handles the subscription change event. """ - discord_id = self.validate_discord_id(body.properties.get("discord_id")) - _ = self.validate_account_id(body.properties.get("account_id")) + discord_id, _ = self.validate_common_properties(body) subscription_name = self.validate_property( body.properties.get("subscription_name"), "subscription_name" ) member = await self.get_guild_member(discord_id, bot) - role = settings.get_post_or_rank(subscription_name) - if not role: + subscription_id = settings.get_post_or_rank(subscription_name) + if not subscription_id: raise ValueError(f"Invalid subscription name: {subscription_name}") - await member.add_roles(bot.guilds[0].get_role(role), atomic=True) # type: ignore + # Use the base handler's role swapping method + role_group = [int(r) for r in settings.role_groups["ALL_LABS_SUBSCRIPTIONS"]] + await self.swap_role_in_group(member, subscription_id, role_group, bot) + return self.success() async def _handle_hof_change(self, body: WebhookBody, bot: Bot) -> dict: @@ -54,12 +52,7 @@ async def _handle_hof_change(self, body: WebhookBody, bot: Bot) -> dict: Handles the HOF change event. """ self.logger.info("Handling HOF change event.") - discord_id = self.validate_discord_id( - self.get_property_or_trait(body, "discord_id") - ) - account_id = self.validate_account_id( - self.get_property_or_trait(body, "account_id") - ) + discord_id, account_id = self.validate_common_properties(body) hof_tier: Literal["1", "10"] = self.validate_property( self.get_property_or_trait(body, "hof_tier"), "hof_tier", # type: ignore @@ -126,12 +119,7 @@ async def _handle_rank_up(self, body: WebhookBody, bot: Bot) -> dict: """ Handles the rank up event. """ - discord_id = self.validate_discord_id( - self.get_property_or_trait(body, "discord_id") - ) - account_id = self.validate_account_id( - self.get_property_or_trait(body, "account_id") - ) + discord_id, account_id = self.validate_common_properties(body) rank = self.validate_property(self.get_property_or_trait(body, "rank"), "rank") member = await self.get_guild_member(discord_id, bot) @@ -149,38 +137,12 @@ async def _handle_rank_up(self, body: WebhookBody, bot: Bot) -> dict: ) raise err - rank_role = bot.guilds[0].get_role(rank_id) - rank_roles = [ - bot.guilds[0].get_role(int(r)) for r in settings.role_groups["ALL_RANKS"] - ] # All rank roles - new_role = next( - (r for r in rank_roles if r and r.id == rank_role.id), None - ) # Get passed rank as role from rank roles - old_role = next( - (r for r in member.roles if r in rank_roles), None - ) # Find existing rank role on user - - if old_role == new_role: - return self.success() - - if old_role: - await member.remove_roles(old_role, atomic=True) # Yeet the old role - - if new_role: - await member.add_roles(new_role, atomic=True) # Add the new role - - if not new_role: - # Why are you passing me BS roles? - err = ValueError(f"Cannot find role for '{rank}'") - self.logger.error( - err, - extra={ - "account_id": account_id, - "discord_id": discord_id, - "rank": rank, - }, - ) - raise err + # Use the base handler's role swapping method + role_group = [int(r) for r in settings.role_groups["ALL_RANKS"]] + changes_made = await self.swap_role_in_group(member, rank_id, role_group, bot) + + if not changes_made: + return self.success() # No changes needed return self.success() @@ -188,18 +150,13 @@ async def _handle_season_rank(self, body: WebhookBody, bot: Bot) -> dict: """ Handles the season rank event. """ - discord_id = self.validate_discord_id( - self.get_property_or_trait(body, "discord_id") - ) - account_id = self.validate_account_id( - self.get_property_or_trait(body, "account_id") - ) + discord_id, account_id = self.validate_common_properties(body) season_rank = self.validate_property( self.get_property_or_trait(body, "season_rank"), "season_rank" ) - season_role = settings.get_season(season_rank) - if not season_role: + season_role_id = settings.get_season(season_rank) + if not season_role_id: err = ValueError(f"Cannot find role for '{season_rank}'") self.logger.error( err, @@ -213,30 +170,8 @@ async def _handle_season_rank(self, body: WebhookBody, bot: Bot) -> dict: member = await self.get_guild_member(discord_id, bot) - all_season_roles = [ - bot.guilds[0].get_role(int(r)) for r in settings.role_groups["ALL_SEASON_RANKS"] - ] - new_role = next( - (r for r in all_season_roles if r and r.id == season_role.id), None - ) - old_role = next((r for r in member.roles if r in all_season_roles), None) - - if old_role == new_role: - return self.success() - - if old_role: - await member.remove_roles(old_role, atomic=True) - - if new_role: - await member.add_roles(new_role, atomic=True) - - return self.success() - - async def _find_user_with_role(self, bot: Bot, role: Role | None) -> Member | None: - """ - Finds the user with the given role. - """ - if not role: - return None + # Use the base handler's role swapping method + role_group = [int(r) for r in settings.role_groups["ALL_SEASON_RANKS"]] + await self.swap_role_in_group(member, season_role_id, role_group, bot) - return next((m for m in role.members), None) + return self.success() \ No newline at end of file diff --git a/tests/src/webhooks/handlers/test_academy.py b/tests/src/webhooks/handlers/test_academy.py index 2c76af4..fc34c63 100644 --- a/tests/src/webhooks/handlers/test_academy.py +++ b/tests/src/webhooks/handlers/test_academy.py @@ -1,6 +1,5 @@ import pytest -from unittest.mock import AsyncMock, patch -from fastapi import HTTPException +from unittest.mock import AsyncMock, patch, MagicMock from src.webhooks.handlers.academy import AcademyHandler from src.webhooks.types import WebhookBody, Platform, WebhookEvent @@ -26,8 +25,7 @@ async def test_handle_certificate_awarded_success(self, bot): traits={}, ) with ( - patch.object(handler, "validate_discord_id", return_value=discord_id), - patch.object(handler, "validate_account_id", return_value=account_id), + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), patch.object(handler, "validate_property", return_value=certificate_id), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.academy.settings") as mock_settings, @@ -60,8 +58,7 @@ async def test_handle_certificate_awarded_no_role(self, bot): traits={}, ) with ( - patch.object(handler, "validate_discord_id", return_value=discord_id), - patch.object(handler, "validate_account_id", return_value=account_id), + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), patch.object(handler, "validate_property", return_value=certificate_id), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.academy.settings") as mock_settings, @@ -91,8 +88,7 @@ async def test_handle_certificate_awarded_add_roles_error(self, bot): traits={}, ) with ( - patch.object(handler, "validate_discord_id", return_value=discord_id), - patch.object(handler, "validate_account_id", return_value=account_id), + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), patch.object(handler, "validate_property", return_value=certificate_id), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.academy.settings") as mock_settings, @@ -106,6 +102,131 @@ async def test_handle_certificate_awarded_add_roles_error(self, bot): await handler._handle_certificate_awarded(body, bot) mock_log.assert_called() + @pytest.mark.asyncio + async def test_handle_subscription_change_success(self, bot): + handler = AcademyHandler() + discord_id = 123456789 + account_id = 987654321 + plan = "Silver Annual" + mock_member = helpers.MockMember(id=discord_id) + mock_member.roles = [] + mock_member.add_roles = AsyncMock() + mock_member.remove_roles = AsyncMock() + body = WebhookBody( + platform=Platform.ACADEMY, + event=WebhookEvent.SUBSCRIPTION_CHANGE, + properties={ + "discord_id": discord_id, + "account_id": account_id, + "plan": plan, + }, + traits={}, + ) + mock_role = MagicMock() + mock_role.id = 555 + with ( + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), + patch.object(handler, "validate_property", return_value=plan), + 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_post_or_rank.return_value = 555 + mock_settings.role_groups = {"ALL_ACADEMY_SUBSCRIPTIONS": [555, 666, 777]} + mock_guild = helpers.MockGuild(id=1) + mock_guild.get_role.return_value = mock_role + bot.guilds = [mock_guild] + result = await handler._handle_subscription_change(body, bot) + mock_member.add_roles.assert_awaited_once() + mock_log.assert_called() + assert result == handler.success() + + @pytest.mark.asyncio + async def test_handle_subscription_change_no_role(self, bot): + handler = AcademyHandler() + discord_id = 123456789 + account_id = 987654321 + plan = "invalid_plan" + mock_member = helpers.MockMember(id=discord_id) + body = WebhookBody( + platform=Platform.ACADEMY, + event=WebhookEvent.SUBSCRIPTION_CHANGE, + properties={ + "discord_id": discord_id, + "account_id": account_id, + "plan": plan, + }, + traits={}, + ) + with ( + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), + patch.object(handler, "validate_property", return_value=plan), + 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, + ): + mock_settings.get_post_or_rank.return_value = None + result = await handler._handle_subscription_change(body, bot) + mock_log.assert_called() + assert result == handler.fail() + + @pytest.mark.asyncio + async def test_handle_subscription_change_role_swap(self, bot): + """Test that subscription change properly swaps roles""" + handler = AcademyHandler() + discord_id = 123456789 + account_id = 987654321 + plan = "professional" + + # Mock member with an existing academy subscription role + old_role = MagicMock() + old_role.id = 666 + mock_member = helpers.MockMember(id=discord_id) + mock_member.roles = [old_role] + mock_member.add_roles = AsyncMock() + mock_member.remove_roles = AsyncMock() + + body = WebhookBody( + platform=Platform.ACADEMY, + event=WebhookEvent.SUBSCRIPTION_CHANGE, + properties={ + "discord_id": discord_id, + "account_id": account_id, + "plan": plan, + }, + traits={}, + ) + + new_role = MagicMock() + new_role.id = 555 + + with ( + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), + patch.object(handler, "validate_property", return_value=plan), + patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), + patch("src.webhooks.handlers.academy.settings") as mock_settings, + ): + mock_settings.get_post_or_rank.return_value = 555 + mock_settings.role_groups = {"ALL_ACADEMY_SUBSCRIPTIONS": [555, 666, 777]} + mock_guild = helpers.MockGuild(id=1) + + def get_role_mock(role_id): + if role_id == 555: + return new_role + elif role_id == 666: + return old_role + return None + + mock_guild.get_role.side_effect = get_role_mock + bot.guilds = [mock_guild] + + result = await handler._handle_subscription_change(body, bot) + + # Verify old role was removed and new role was added + mock_member.remove_roles.assert_awaited_once_with(old_role, atomic=True) + mock_member.add_roles.assert_awaited_once_with(new_role, atomic=True) + assert result == handler.success() + @pytest.mark.asyncio async def test_handle_invalid_event(self, bot): handler = AcademyHandler() diff --git a/tests/src/webhooks/handlers/test_mp.py b/tests/src/webhooks/handlers/test_mp.py index 36d4502..2dad297 100644 --- a/tests/src/webhooks/handlers/test_mp.py +++ b/tests/src/webhooks/handlers/test_mp.py @@ -38,18 +38,20 @@ async def test_handle_subscription_change_success(self, bot): traits={}, ) with ( - patch.object(handler, "validate_discord_id", return_value=discord_id), - patch.object(handler, "validate_account_id", return_value=account_id), + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), patch.object(handler, "validate_property", return_value=subscription_name), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.mp.settings") as mock_settings, ): mock_settings.get_post_or_rank.return_value = 555 + mock_settings.role_groups = {"ALL_LABS_SUBSCRIPTIONS": [555, 666, 777]} + mock_role = MagicMock() + mock_role.id = 555 mock_guild = helpers.MockGuild(id=1) - mock_guild.get_role.return_value = 555 + mock_guild.get_role.return_value = mock_role bot.guilds = [mock_guild] result = await handler._handle_subscription_change(body, bot) - mock_member.add_roles.assert_awaited() + mock_member.add_roles.assert_awaited_once() assert result == handler.success() @pytest.mark.asyncio @@ -70,8 +72,7 @@ async def test_handle_subscription_change_invalid_role(self, bot): traits={}, ) with ( - patch.object(handler, "validate_discord_id", return_value=discord_id), - patch.object(handler, "validate_account_id", return_value=account_id), + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), patch.object(handler, "validate_property", return_value=subscription_name), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.mp.settings") as mock_settings, @@ -103,8 +104,7 @@ async def test_handle_hof_change_success_top1(self, bot): mock_role_1 = MagicMock() mock_role_10 = MagicMock() with ( - patch.object(handler, "validate_discord_id", return_value=discord_id), - patch.object(handler, "validate_account_id", return_value=account_id), + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), patch.object(handler, "validate_property", return_value=hof_tier), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.mp.settings") as mock_settings, @@ -138,8 +138,7 @@ async def test_handle_hof_change_invalid_tier(self, bot): traits={}, ) with ( - patch.object(handler, "validate_discord_id", return_value=discord_id), - patch.object(handler, "validate_account_id", return_value=account_id), + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), patch.object(handler, "validate_property", return_value=hof_tier), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.mp.settings") as mock_settings, @@ -173,14 +172,15 @@ async def test_handle_rank_up_success(self, bot): traits={}, ) mock_role = MagicMock() + mock_role.id = 555 with ( - patch.object(handler, "validate_discord_id", return_value=discord_id), - patch.object(handler, "validate_account_id", return_value=account_id), + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), patch.object(handler, "validate_property", return_value=rank), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), patch("src.webhooks.handlers.mp.settings") as mock_settings, ): - mock_settings.role_groups = {"ALL_RANKS": [555]} + mock_settings.get_post_or_rank.return_value = 555 + mock_settings.role_groups = {"ALL_RANKS": [555, 666, 777]} mock_guild = helpers.MockGuild(id=1) mock_guild.get_role.return_value = mock_role bot.guilds = [mock_guild] @@ -209,11 +209,12 @@ async def test_handle_rank_up_invalid_role(self, bot): traits={}, ) with ( - patch.object(handler, "validate_discord_id", return_value=discord_id), - patch.object(handler, "validate_account_id", return_value=account_id), + patch.object(handler, "validate_common_properties", return_value=(discord_id, account_id)), patch.object(handler, "validate_property", return_value=rank), patch.object(handler, "get_guild_member", new_callable=AsyncMock, return_value=mock_member), + patch("src.webhooks.handlers.mp.settings") as mock_settings, ): + mock_settings.get_post_or_rank.return_value = None mock_guild = helpers.MockGuild(id=1) mock_guild.get_role.return_value = None bot.guilds = [mock_guild]