from __future__ import annotations import unittest from unittest.mock import AsyncMock, MagicMock, patch import discord from zyrarbot.settings import Settings from zyrarbot.ui.bot.client import MusicBot class ShutdownVoiceCleanupTests(unittest.IsolatedAsyncioTestCase): def setUp(self): self.settings = Settings(discord_token="fake_token") self.tracks = MagicMock() self.uploads = MagicMock() self.config = MagicMock() self.history = MagicMock() self.bot = MusicBot( self.settings, self.tracks, self.uploads, self.config, self.history, ) self.player = MagicMock() self.bot.player = self.player async def test_disconnect_all_voice_disconnects_vcs_and_cleans_guild_state(self): mock_vc = MagicMock() mock_vc.guild.id = 12345 mock_vc.channel.name = "Test Channel" mock_vc.is_connected.return_value = True mock_vc.disconnect = AsyncMock() mock_guild = MagicMock(spec=discord.Guild) mock_guild.id = 12345 mock_guild.name = "Test Guild" mock_guild.me.voice.channel = MagicMock() mock_guild.change_voice_state = AsyncMock() with ( patch.object(type(self.bot), "voice_clients", [mock_vc]), patch.object(type(self.bot), "guilds", [mock_guild]), ): await self.bot.disconnect_all_voice() self.player.save_queue.assert_called_once_with(12345) self.player.clear.assert_called_once_with(12345) mock_vc.disconnect.assert_awaited_once_with(force=False) mock_guild.change_voice_state.assert_awaited_once_with(channel=None) async def test_close_calls_disconnect_all_voice_before_super_close(self): disconnect_mock = AsyncMock() self.bot.disconnect_all_voice = disconnect_mock with patch("discord.ext.commands.Bot.close", new_callable=AsyncMock) as super_close_mock: await self.bot.close() disconnect_mock.assert_awaited_once() super_close_mock.assert_awaited_once() # Calling close again should be a no-op due to _is_closing guard await self.bot.close() self.assertEqual(disconnect_mock.await_count, 1) async def test_on_ready_cleans_dangling_voice_connection(self): mock_guild = MagicMock(spec=discord.Guild) mock_guild.id = 12345 mock_guild.name = "Test Guild" mock_guild.me.voice.channel.name = "Ghost Channel" mock_guild.voice_client = MagicMock(spec=discord.VoiceClient) mock_guild.voice_client.disconnect = AsyncMock() mock_guild.change_voice_state = AsyncMock() with patch.object(type(self.bot), "guilds", [mock_guild]): await self.bot.on_ready() mock_guild.voice_client.disconnect.assert_awaited_once_with(force=True)