aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Dockerfile22
-rw-r--r--bot/exts/help_channels/_channel.py3
-rw-r--r--bot/exts/info/help.py8
-rw-r--r--bot/exts/info/pypi.py5
-rw-r--r--bot/exts/moderation/infraction/_scheduler.py55
-rw-r--r--bot/exts/moderation/infraction/infractions.py41
-rw-r--r--bot/exts/moderation/infraction/superstarify.py11
-rw-r--r--bot/exts/utils/reminders.py64
-rw-r--r--bot/pagination.py2
-rw-r--r--pyproject.toml6
-rw-r--r--tests/bot/exts/moderation/infraction/test_infractions.py38
-rw-r--r--tests/helpers.py2
12 files changed, 160 insertions, 97 deletions
diff --git a/Dockerfile b/Dockerfile
index 9cf9c7b27..205b66209 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,27 +1,13 @@
-FROM --platform=linux/amd64 python:3.10-slim
+FROM --platform=linux/amd64 ghcr.io/chrislovering/python-poetry-base:3.10-slim
# Define Git SHA build argument for sentry
ARG git_sha="development"
-
-ENV POETRY_VERSION=1.2.0 \
- POETRY_HOME="/opt/poetry" \
- POETRY_NO_INTERACTION=1 \
- APP_DIR="/bot" \
- GIT_SHA=$git_sha
-
-ENV PATH="$POETRY_HOME/bin:$PATH"
-
-RUN apt-get update \
- && apt-get -y upgrade \
- && apt-get install --no-install-recommends -y curl \
- && apt-get clean && rm -rf /var/lib/apt/lists/*
-
-RUN curl -sSL https://install.python-poetry.org | python
+ENV GIT_SHA=$git_sha
# Install project dependencies
-WORKDIR $APP_DIR
+WORKDIR /bot
COPY pyproject.toml poetry.lock ./
-RUN poetry install --no-dev
+RUN poetry install --without dev
# Copy the source code in last to optimize rebuilding the image
COPY . .
diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py
index d9cebf215..cfe774f4c 100644
--- a/bot/exts/help_channels/_channel.py
+++ b/bot/exts/help_channels/_channel.py
@@ -183,7 +183,8 @@ async def ensure_cached_claimant(channel: discord.TextChannel) -> None:
log.info("Hit the dormant message embed before finding a claimant in %s (%d).", channel, channel.id)
break
# Only set the claimant if the first embed matches the claimed channel embed regex
- if match := CLAIMED_BY_RE.match(message.embeds[0].description):
+ description = message.embeds[0].description
+ if (description is not None) and (match := CLAIMED_BY_RE.match(description)):
await _caches.claimants.set(channel.id, int(match.group("user_id")))
return
diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py
index 282f8c97a..48f840e51 100644
--- a/bot/exts/info/help.py
+++ b/bot/exts/info/help.py
@@ -307,7 +307,7 @@ class CustomHelpCommand(HelpCommand):
# Remove line breaks from docstrings, if not used to separate paragraphs.
# Allow overriding this behaviour via putting \u2003 at the start of a line.
formatted_doc = re.sub("(?<!\n)\n(?![\n\u2003])", " ", command.help)
- command_details += f"*{formatted_doc or 'No details provided.'}*\n"
+ command_details += f"{formatted_doc or 'No details provided.'}\n"
embed.description = command_details
# If the help is invoked in the context of an error, don't show subcommand navigation.
@@ -331,7 +331,7 @@ class CustomHelpCommand(HelpCommand):
for command in commands_:
signature = f" {command.signature}" if command.signature else ""
details.append(
- f"\n**`{PREFIX}{command.qualified_name}{signature}`**\n*{command.short_doc or 'No details provided'}*"
+ f"\n**`{PREFIX}{command.qualified_name}{signature}`**\n{command.short_doc or 'No details provided'}"
)
if return_as_list:
return details
@@ -372,7 +372,7 @@ class CustomHelpCommand(HelpCommand):
embed = Embed()
embed.set_author(name="Command Help", icon_url=constants.Icons.questionmark)
- embed.description = f"**{cog.qualified_name}**\n*{cog.description}*"
+ embed.description = f"**{cog.qualified_name}**\n{cog.description}"
command_details = self.get_commands_brief_details(commands_)
if command_details:
@@ -412,7 +412,7 @@ class CustomHelpCommand(HelpCommand):
filtered_commands = await self.filter_commands(all_commands, sort=True)
command_detail_lines = self.get_commands_brief_details(filtered_commands, return_as_list=True)
- description = f"**{category.name}**\n*{category.description}*"
+ description = f"**{category.name}**\n{category.description}"
if command_detail_lines:
description += "\n\n**Commands:**"
diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py
index 2d387df3d..bac7d2389 100644
--- a/bot/exts/info/pypi.py
+++ b/bot/exts/info/pypi.py
@@ -54,11 +54,12 @@ class PyPi(Cog):
embed.url = info["package_url"]
embed.colour = next(PYPI_COLOURS)
- summary = escape_markdown(info["summary"])
+ # Summary can be None if not provided by the package
+ summary: str | None = info["summary"]
# Summary could be completely empty, or just whitespace.
if summary and not summary.isspace():
- embed.description = summary
+ embed.description = escape_markdown(summary)
else:
embed.description = "No summary provided."
diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py
index 655290559..4c275a1f0 100644
--- a/bot/exts/moderation/infraction/_scheduler.py
+++ b/bot/exts/moderation/infraction/_scheduler.py
@@ -1,6 +1,7 @@
import textwrap
import typing as t
from abc import abstractmethod
+from collections.abc import Awaitable, Callable
from gettext import ngettext
import arrow
@@ -79,9 +80,14 @@ class InfractionScheduler:
async def reapply_infraction(
self,
infraction: _utils.Infraction,
- apply_coro: t.Optional[t.Awaitable]
+ action: t.Optional[Callable[[], Awaitable[None]]]
) -> None:
- """Reapply an infraction if it's still active or deactivate it if less than 60 sec left."""
+ """
+ Reapply an infraction if it's still active or deactivate it if less than 60 sec left.
+
+ Note: The `action` provided is an async function rather than a coroutine
+ to prevent getting a RuntimeWarning if it is not used (e.g. in mocked tests).
+ """
if infraction["expires_at"] is not None:
# Calculate the time remaining, in seconds, for the mute.
expiry = dateutil.parser.isoparse(infraction["expires_at"])
@@ -101,7 +107,7 @@ class InfractionScheduler:
# Allowing mod log since this is a passive action that should be logged.
try:
- await apply_coro
+ await action()
except discord.HTTPException as e:
# When user joined and then right after this left again before action completed, this can't apply roles
if e.code == 10007 or e.status == 404:
@@ -111,7 +117,7 @@ class InfractionScheduler:
else:
log.exception(
f"Got unexpected HTTPException (HTTP {e.status}, Discord code {e.code})"
- f"when awaiting {infraction['type']} coroutine for {infraction['user']}."
+ f"when running {infraction['type']} action for {infraction['user']}."
)
else:
log.info(f"Re-applied {infraction['type']} to user {infraction['user']} upon rejoining.")
@@ -121,17 +127,20 @@ class InfractionScheduler:
ctx: Context,
infraction: _utils.Infraction,
user: MemberOrUser,
- action_coro: t.Optional[t.Awaitable] = None,
+ action: t.Optional[Callable[[], Awaitable[None]]] = None,
user_reason: t.Optional[str] = None,
additional_info: str = "",
) -> bool:
"""
Apply an infraction to the user, log the infraction, and optionally notify the user.
- `action_coro`, if not provided, will result in the infraction not getting scheduled for deletion.
+ `action`, if not provided, will result in the infraction not getting scheduled for deletion.
`user_reason`, if provided, will be sent to the user in place of the infraction reason.
`additional_info` will be attached to the text field in the mod-log embed.
+ Note: The `action` provided is an async function rather than just a coroutine
+ to prevent getting a RuntimeWarning if it is not used (e.g. in mocked tests).
+
Returns whether or not the infraction succeeded.
"""
infr_type = infraction["type"]
@@ -200,10 +209,10 @@ class InfractionScheduler:
purge = infraction.get("purge", "")
# Execute the necessary actions to apply the infraction on Discord.
- if action_coro:
- log.trace(f"Awaiting the infraction #{id_} application action coroutine.")
+ if action:
+ log.trace(f"Running the infraction #{id_} application action.")
try:
- await action_coro
+ await action()
if expiry:
# Schedule the expiration of the infraction.
self.schedule_expiration(infraction)
@@ -278,6 +287,7 @@ class InfractionScheduler:
ctx: Context,
infr_type: str,
user: MemberOrUser,
+ pardon_reason: t.Optional[str] = None,
*,
send_msg: bool = True,
notify: bool = True
@@ -285,6 +295,9 @@ class InfractionScheduler:
"""
Prematurely end an infraction for a user and log the action in the mod log.
+ If `pardon_reason` is None, then the database will not receive
+ appended text explaining why the infraction was pardoned.
+
If `send_msg` is True, then a pardoning confirmation message will be sent to
the context channel. Otherwise, no such message will be sent.
@@ -309,7 +322,7 @@ class InfractionScheduler:
return
# Deactivate the infraction and cancel its scheduled expiration task.
- log_text = await self.deactivate_infraction(response[0], send_log=False, notify=notify)
+ log_text = await self.deactivate_infraction(response[0], pardon_reason, send_log=False, notify=notify)
log_text["Member"] = messages.format_user(user)
log_text["Actor"] = ctx.author.mention
@@ -362,6 +375,7 @@ class InfractionScheduler:
async def deactivate_infraction(
self,
infraction: _utils.Infraction,
+ pardon_reason: t.Optional[str] = None,
*,
send_log: bool = True,
notify: bool = True
@@ -370,8 +384,12 @@ class InfractionScheduler:
Deactivate an active infraction and return a dictionary of lines to send in a mod log.
The infraction is removed from Discord, marked as inactive in the database, and has its
- expiration task cancelled. If `send_log` is True, a mod log is sent for the
- deactivation of the infraction.
+ expiration task cancelled.
+
+ If `pardon_reason` is None, then the database will not receive
+ appended text explaining why the infraction was pardoned.
+
+ If `send_log` is True, a mod log is sent for the deactivation of the infraction.
If `notify` is True, notify the user of the pardon via DM where applicable.
@@ -441,9 +459,20 @@ class InfractionScheduler:
try:
# Mark infraction as inactive in the database.
log.trace(f"Marking infraction #{id_} as inactive in the database.")
+
+ data = {"active": False}
+
+ if pardon_reason is not None:
+ data["reason"] = ""
+ # Append pardon reason to infraction in database.
+ if (punish_reason := infraction["reason"]) is not None:
+ data["reason"] = punish_reason + " | "
+
+ data["reason"] += f"Pardoned: {pardon_reason}"
+
await self.bot.api_client.patch(
f"bot/infractions/{id_}",
- json={"active": False}
+ json=data
)
except ResponseCodeError as e:
log.exception(f"Failed to deactivate infraction #{id_} ({type_})")
diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py
index 05cc74a03..60b4428b7 100644
--- a/bot/exts/moderation/infraction/infractions.py
+++ b/bot/exts/moderation/infraction/infractions.py
@@ -54,8 +54,9 @@ class Infractions(InfractionScheduler, commands.Cog):
if active_mutes:
reason = f"Re-applying active mute: {active_mutes[0]['id']}"
- action = member.add_roles(self._muted_role, reason=reason)
+ async def action() -> None:
+ await member.add_roles(self._muted_role, reason=reason)
await self.reapply_infraction(active_mutes[0], action)
# region: Permanent infractions
@@ -125,8 +126,6 @@ class Infractions(InfractionScheduler, commands.Cog):
infraction = await self.apply_ban(ctx, user, reason, duration_or_expiry=duration)
if not infraction or not infraction.get("id"):
# Ban was unsuccessful, quit early.
- await ctx.send(":x: Failed to apply ban.")
- log.error("Failed to apply ban to user %d", user.id)
return
# Calling commands directly skips discord.py's convertors, so we need to convert args manually.
@@ -339,14 +338,20 @@ class Infractions(InfractionScheduler, commands.Cog):
# region: Remove infractions (un- commands)
@command()
- async def unmute(self, ctx: Context, user: UnambiguousMemberOrUser) -> None:
+ async def unmute(
+ self,
+ ctx: Context,
+ user: UnambiguousMemberOrUser,
+ *,
+ pardon_reason: t.Optional[str] = None
+ ) -> None:
"""Prematurely end the active mute infraction for the user."""
- await self.pardon_infraction(ctx, "mute", user)
+ await self.pardon_infraction(ctx, "mute", user, pardon_reason)
@command()
- async def unban(self, ctx: Context, user: UnambiguousMemberOrUser) -> None:
+ async def unban(self, ctx: Context, user: UnambiguousMemberOrUser, *, pardon_reason: str) -> None:
"""Prematurely end the active ban infraction for the user."""
- await self.pardon_infraction(ctx, "ban", user)
+ await self.pardon_infraction(ctx, "ban", user, pardon_reason)
@command(aliases=("uvban",))
async def unvoiceban(self, ctx: Context) -> None:
@@ -358,9 +363,15 @@ class Infractions(InfractionScheduler, commands.Cog):
await ctx.send(":x: This command is not yet implemented. Maybe you meant to use `unvoicemute`?")
@command(aliases=("uvmute",))
- async def unvoicemute(self, ctx: Context, user: UnambiguousMemberOrUser) -> None:
+ async def unvoicemute(
+ self,
+ ctx: Context,
+ user: UnambiguousMemberOrUser,
+ *,
+ pardon_reason: t.Optional[str] = None
+ ) -> None:
"""Prematurely end the active voice mute infraction for the user."""
- await self.pardon_infraction(ctx, "voice_mute", user)
+ await self.pardon_infraction(ctx, "voice_mute", user, pardon_reason)
# endregion
# region: Base apply functions
@@ -397,7 +408,7 @@ class Infractions(InfractionScheduler, commands.Cog):
log.trace(f"Attempting to kick {user} from voice because they've been muted.")
await user.move_to(None, reason=reason)
- await self.apply_infraction(ctx, infraction, user, action())
+ await self.apply_infraction(ctx, infraction, user, action)
@respect_role_hierarchy(member_arg=2)
async def apply_kick(self, ctx: Context, user: Member, reason: t.Optional[str], **kwargs) -> None:
@@ -415,7 +426,9 @@ class Infractions(InfractionScheduler, commands.Cog):
if reason:
reason = textwrap.shorten(reason, width=512, placeholder="...")
- action = user.kick(reason=reason)
+ async def action() -> None:
+ await user.kick(reason=reason)
+
await self.apply_infraction(ctx, infraction, user, action)
@respect_role_hierarchy(member_arg=2)
@@ -464,7 +477,9 @@ class Infractions(InfractionScheduler, commands.Cog):
if reason:
reason = textwrap.shorten(reason, width=512, placeholder="...")
- action = ctx.guild.ban(user, reason=reason, delete_message_days=purge_days)
+ async def action() -> None:
+ await ctx.guild.ban(user, reason=reason, delete_message_days=purge_days)
+
await self.apply_infraction(ctx, infraction, user, action)
bb_cog: t.Optional[BigBrother] = self.bot.get_cog("Big Brother")
@@ -502,7 +517,7 @@ class Infractions(InfractionScheduler, commands.Cog):
await user.move_to(None, reason="Disconnected from voice to apply voice mute.")
await user.remove_roles(self._voice_verified_role, reason=reason)
- await self.apply_infraction(ctx, infraction, user, action())
+ await self.apply_infraction(ctx, infraction, user, action)
# endregion
# region: Base pardon functions
diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py
index f2aab7a92..6cb2c3354 100644
--- a/bot/exts/moderation/infraction/superstarify.py
+++ b/bot/exts/moderation/infraction/superstarify.py
@@ -96,11 +96,12 @@ class Superstarify(InfractionScheduler, Cog):
if active_superstarifies:
infraction = active_superstarifies[0]
- action = member.edit(
- nick=self.get_nick(infraction["id"], member.id),
- reason=f"Superstarified member tried to escape the prison: {infraction['id']}"
- )
+ async def action() -> None:
+ await member.edit(
+ nick=self.get_nick(infraction["id"], member.id),
+ reason=f"Superstarified member tried to escape the prison: {infraction['id']}"
+ )
await self.reapply_infraction(infraction, action)
@command(name="superstarify", aliases=("force_nick", "star", "starify", "superstar"))
@@ -175,7 +176,7 @@ class Superstarify(InfractionScheduler, Cog):
).format
successful = await self.apply_infraction(
- ctx, infraction, member, action(),
+ ctx, infraction, member, action,
user_reason=user_message(reason=f'**Additional details:** {reason}\n\n' if reason else ''),
additional_info=nickname_info
)
diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py
index 45cddd7a2..c65785314 100644
--- a/bot/exts/utils/reminders.py
+++ b/bot/exts/utils/reminders.py
@@ -209,6 +209,29 @@ class Reminders(Cog):
log.debug(f"Deleting reminder #{reminder['id']} (the user has been reminded).")
await self.bot.api_client.delete(f"bot/reminders/{reminder['id']}")
+ @staticmethod
+ async def try_get_content_from_reply(ctx: Context) -> t.Optional[str]:
+ """
+ Attempts to get content from the referenced message, if applicable.
+
+ Differs from botcore.utils.commands.clean_text_or_reply as allows for messages with no content.
+ """
+ content = None
+ if reference := ctx.message.reference:
+ if isinstance((resolved_message := reference.resolved), discord.Message):
+ content = resolved_message.content
+
+ # If we weren't able to get the content of a replied message
+ if content is None:
+ await send_denial(ctx, "Your reminder must have a content and/or reply to a message.")
+ return
+
+ # If the replied message has no content (e.g. only attachments/embeds)
+ if content == "":
+ content = "*See referenced message.*"
+
+ return content
+
@group(name="remind", aliases=("reminder", "reminders", "remindme"), invoke_without_command=True)
async def remind_group(
self, ctx: Context, mentions: Greedy[ReminderMention], expiration: Duration, *, content: t.Optional[str] = None
@@ -282,18 +305,11 @@ class Reminders(Cog):
# If `content` isn't provided then we try to get message content of a replied message
if not content:
- if reference := ctx.message.reference:
- if isinstance((resolved_message := reference.resolved), discord.Message):
- content = resolved_message.content
- # If we weren't able to get the content of a replied message
- if content is None:
- await send_denial(ctx, "Your reminder must have a content and/or reply to a message.")
+ content = await self.try_get_content_from_reply(ctx)
+ if not content:
+ # Couldn't get content from reply
return
- # If the replied message has no content (e.g. only attachments/embeds)
- if content == "":
- content = "See referenced message."
-
# Now we can attempt to actually set the reminder.
reminder = await self.bot.api_client.post(
'bot/reminders',
@@ -382,20 +398,7 @@ class Reminders(Cog):
@remind_group.group(name="edit", aliases=("change", "modify"), invoke_without_command=True)
async def edit_reminder_group(self, ctx: Context) -> None:
- """
- Commands for modifying your current reminders.
-
- The `expiration` duration supports the following symbols for each unit of time:
- - years: `Y`, `y`, `year`, `years`
- - months: `m`, `month`, `months`
- - weeks: `w`, `W`, `week`, `weeks`
- - days: `d`, `D`, `day`, `days`
- - hours: `H`, `h`, `hour`, `hours`
- - minutes: `M`, `minute`, `minutes`
- - seconds: `S`, `s`, `second`, `seconds`
-
- For example, to edit a reminder to expire in 3 days and 1 minute, you can do `!remind edit duration 1234 3d1M`.
- """
+ """Commands for modifying your current reminders."""
await ctx.send_help(ctx.command)
@edit_reminder_group.command(name="duration", aliases=("time",))
@@ -417,8 +420,17 @@ class Reminders(Cog):
await self.edit_reminder(ctx, id_, {'expiration': expiration.isoformat()})
@edit_reminder_group.command(name="content", aliases=("reason",))
- async def edit_reminder_content(self, ctx: Context, id_: int, *, content: str) -> None:
- """Edit one of your reminder's content."""
+ async def edit_reminder_content(self, ctx: Context, id_: int, *, content: t.Optional[str] = None) -> None:
+ """
+ Edit one of your reminder's content.
+
+ You can either supply the new content yourself, or reply to a message to use its content.
+ """
+ if not content:
+ content = await self.try_get_content_from_reply(ctx)
+ if not content:
+ # Message doesn't have a reply to get content from
+ return
await self.edit_reminder(ctx, id_, {"content": content})
@edit_reminder_group.command(name="mentions", aliases=("pings",))
diff --git a/bot/pagination.py b/bot/pagination.py
index 8f4353eb1..10bef1c9f 100644
--- a/bot/pagination.py
+++ b/bot/pagination.py
@@ -236,7 +236,7 @@ class LinePaginator(Paginator):
raise EmptyPaginatorEmbedError("No lines to paginate")
log.debug("No lines to add to paginator, adding '(nothing to display)' message")
- lines.append("(nothing to display)")
+ lines.append("*(nothing to display)*")
for line in lines:
try:
diff --git a/pyproject.toml b/pyproject.toml
index 43eb799b6..36c3b5392 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -85,3 +85,9 @@ case_sensitive = true
combine_as_imports = true
line_length = 120
atomic = true
+
+[tool.pytest.ini_options]
+# We don't use nose style tests so disable them in pytest.
+# This stops pytest from running functions named `setup` in test files.
+# See https://github.com/python-discord/bot/pull/2229#issuecomment-1204436420
+addopts = "-p no:nose"
diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py
index a18a4d23b..b78328137 100644
--- a/tests/bot/exts/moderation/infraction/test_infractions.py
+++ b/tests/bot/exts/moderation/infraction/test_infractions.py
@@ -35,17 +35,20 @@ class TruncationTests(unittest.IsolatedAsyncioTestCase):
self.cog.apply_infraction = AsyncMock()
self.bot.get_cog.return_value = AsyncMock()
self.cog.mod_log.ignore = Mock()
- self.ctx.guild.ban = Mock()
+ self.ctx.guild.ban = AsyncMock()
await self.cog.apply_ban(self.ctx, self.target, "foo bar" * 3000)
- self.ctx.guild.ban.assert_called_once_with(
+ self.cog.apply_infraction.assert_awaited_once_with(
+ self.ctx, {"foo": "bar", "purge": ""}, self.target, ANY
+ )
+
+ action = self.cog.apply_infraction.call_args.args[-1]
+ await action()
+ self.ctx.guild.ban.assert_awaited_once_with(
self.target,
reason=textwrap.shorten("foo bar" * 3000, 512, placeholder="..."),
delete_message_days=0
)
- self.cog.apply_infraction.assert_awaited_once_with(
- self.ctx, {"foo": "bar", "purge": ""}, self.target, self.ctx.guild.ban.return_value
- )
@patch("bot.exts.moderation.infraction._utils.post_infraction")
async def test_apply_kick_reason_truncation(self, post_infraction_mock):
@@ -54,14 +57,17 @@ class TruncationTests(unittest.IsolatedAsyncioTestCase):
self.cog.apply_infraction = AsyncMock()
self.cog.mod_log.ignore = Mock()
- self.target.kick = Mock()
+ self.target.kick = AsyncMock()
await self.cog.apply_kick(self.ctx, self.target, "foo bar" * 3000)
- self.target.kick.assert_called_once_with(reason=textwrap.shorten("foo bar" * 3000, 512, placeholder="..."))
self.cog.apply_infraction.assert_awaited_once_with(
- self.ctx, {"foo": "bar"}, self.target, self.target.kick.return_value
+ self.ctx, {"foo": "bar"}, self.target, ANY
)
+ action = self.cog.apply_infraction.call_args.args[-1]
+ await action()
+ self.target.kick.assert_awaited_once_with(reason=textwrap.shorten("foo bar" * 3000, 512, placeholder="..."))
+
@patch("bot.exts.moderation.infraction.infractions.constants.Roles.voice_verified", new=123456)
class VoiceMuteTests(unittest.IsolatedAsyncioTestCase):
@@ -90,8 +96,14 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase):
async def test_voice_unmute(self):
"""Should call infraction pardoning function."""
self.cog.pardon_infraction = AsyncMock()
+ self.assertIsNone(await self.cog.unvoicemute(self.cog, self.ctx, self.user, pardon_reason="foobar"))
+ self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_mute", self.user, "foobar")
+
+ async def test_voice_unmute_reasonless(self):
+ """Should call infraction pardoning function without a pardon reason."""
+ self.cog.pardon_infraction = AsyncMock()
self.assertIsNone(await self.cog.unvoicemute(self.cog, self.ctx, self.user))
- self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_mute", self.user)
+ self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_mute", self.user, None)
@patch("bot.exts.moderation.infraction.infractions._utils.post_infraction")
@patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction")
@@ -141,8 +153,8 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase):
async def action_tester(self, action, reason: str) -> None:
"""Helper method to test voice mute action."""
- self.assertTrue(inspect.iscoroutine(action))
- await action
+ self.assertTrue(inspect.iscoroutinefunction(action))
+ await action()
self.user.move_to.assert_called_once_with(None, reason=ANY)
self.user.remove_roles.assert_called_once_with(self.cog._voice_verified_role, reason=reason)
@@ -195,8 +207,8 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase):
# Test action
action = self.cog.apply_infraction.call_args[0][-1]
- self.assertTrue(inspect.iscoroutine(action))
- await action
+ self.assertTrue(inspect.iscoroutinefunction(action))
+ await action()
async def test_voice_unmute_user_not_found(self):
"""Should include info to return dict when user was not found from guild."""
diff --git a/tests/helpers.py b/tests/helpers.py
index 687e15b96..a4b919dcb 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -317,7 +317,7 @@ class MockBot(CustomMockMixin, unittest.mock.MagicMock):
guild_id=1,
intents=discord.Intents.all(),
)
- additional_spec_asyncs = ("wait_for", "redis_ready")
+ additional_spec_asyncs = ("wait_for",)
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)