diff options
| author | salmonik <salmonik@noreply.codeberg.org> | 2026-04-19 18:10:09 +0200 |
|---|---|---|
| committer | salmonik <salmonik@noreply.codeberg.org> | 2026-04-19 18:10:09 +0200 |
| commit | f8e1cf36e1efa0602bf2e450835f2a67654762a6 (patch) | |
| tree | fe6b12226d7eebe2a0dc6525de3414d0c3718cc8 /tests/cogs | |
| parent | add support for x^2+y^2=0,4 and alike (diff) | |
| parent | fix: proper aiohttp async context manager mocking in test (diff) | |
| download | senator-sperma-f8e1cf36e1efa0602bf2e450835f2a67654762a6.tar.gz senator-sperma-f8e1cf36e1efa0602bf2e450835f2a67654762a6.zip | |
Merge pull request 'massive changes' (#3) from dev into main
Reviewed-on: https://codeberg.org/salmonik/senator-sperma/pulls/3
stable enough
Diffstat (limited to '')
| -rw-r--r-- | tests/cogs/test_ai.py | 83 | ||||
| -rw-r--r-- | tests/cogs/test_assistant.py | 66 | ||||
| -rw-r--r-- | tests/cogs/test_compression_conversion.py | 139 | ||||
| -rw-r--r-- | tests/cogs/test_cron.py | 166 | ||||
| -rw-r--r-- | tests/cogs/test_downloader.py | 93 | ||||
| -rw-r--r-- | tests/cogs/test_math_commands.py | 55 | ||||
| -rw-r--r-- | tests/cogs/test_music_audio.py | 93 | ||||
| -rw-r--r-- | tests/cogs/test_music_external_play.py | 116 | ||||
| -rw-r--r-- | tests/cogs/test_music_join.py | 53 | ||||
| -rw-r--r-- | tests/cogs/test_music_play.py | 104 | ||||
| -rw-r--r-- | tests/cogs/test_qr.py | 75 | ||||
| -rw-r--r-- | tests/cogs/test_say.py | 38 | ||||
| -rw-r--r-- | tests/cogs/test_search.py | 83 | ||||
| -rw-r--r-- | tests/cogs/test_trigger.py | 106 | ||||
| -rw-r--r-- | tests/cogs/test_tts.py | 58 | ||||
| -rw-r--r-- | tests/cogs/test_vclogs.py | 83 | ||||
| -rw-r--r-- | tests/cogs/test_voice.py | 79 | ||||
| -rw-r--r-- | tests/cogs/test_web.py | 57 |
18 files changed, 1547 insertions, 0 deletions
diff --git a/tests/cogs/test_ai.py b/tests/cogs/test_ai.py new file mode 100644 index 0000000..a1c3fd4 --- /dev/null +++ b/tests/cogs/test_ai.py | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import discord | ||
| 4 | import httpx | ||
| 5 | from tests.conftest import MockApplicationContext | ||
| 6 | |||
| 7 | |||
| 8 | @pytest.fixture | ||
| 9 | def ai_cog(bot): | ||
| 10 | from cogs.ai import AI | ||
| 11 | cog = AI(bot) | ||
| 12 | return cog | ||
| 13 | |||
| 14 | |||
| 15 | @pytest.fixture | ||
| 16 | def mock_ai_response(): | ||
| 17 | return { | ||
| 18 | "choices": [ | ||
| 19 | { | ||
| 20 | "message": { | ||
| 21 | "content": "Test response from AI" | ||
| 22 | } | ||
| 23 | } | ||
| 24 | ] | ||
| 25 | } | ||
| 26 | |||
| 27 | |||
| 28 | @pytest.mark.asyncio | ||
| 29 | async def test_senator_command_success(ai_cog, mock_channel, mock_guild, bot, mock_ai_response): | ||
| 30 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 31 | |||
| 32 | mock_response = MagicMock() | ||
| 33 | mock_response.json = MagicMock(return_value=mock_ai_response) | ||
| 34 | |||
| 35 | mock_client = MagicMock() | ||
| 36 | mock_client.post = AsyncMock(return_value=mock_response) | ||
| 37 | mock_client.__aenter__ = AsyncMock(return_value=mock_response) | ||
| 38 | mock_client.__aexit__ = AsyncMock(return_value=None) | ||
| 39 | |||
| 40 | with patch('cogs.ai.httpx.AsyncClient') as mock_httpx: | ||
| 41 | instance = mock_httpx.return_value.__aenter__.return_value | ||
| 42 | instance.post = AsyncMock(return_value=mock_response) | ||
| 43 | with patch.object(ctx, 'send_followup', new_callable=AsyncMock) as mock_followup: | ||
| 44 | await ai_cog.senator.callback(ai_cog, ctx, text="Hello", tts=False) | ||
| 45 | mock_followup.assert_called_once_with("Test response from AI") | ||
| 46 | |||
| 47 | |||
| 48 | @pytest.mark.asyncio | ||
| 49 | async def test_senator_command_empty_choices(ai_cog, mock_channel, mock_guild, bot): | ||
| 50 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 51 | |||
| 52 | mock_response = MagicMock() | ||
| 53 | mock_response.json = MagicMock(return_value={"choices": []}) | ||
| 54 | |||
| 55 | mock_client = MagicMock() | ||
| 56 | mock_client.post = AsyncMock(return_value=mock_response) | ||
| 57 | mock_client.__aenter__ = AsyncMock(return_value=mock_response) | ||
| 58 | mock_client.__aexit__ = AsyncMock(return_value=None) | ||
| 59 | |||
| 60 | with patch('cogs.ai.httpx.AsyncClient') as mock_httpx: | ||
| 61 | instance = mock_httpx.return_value.__aenter__.return_value | ||
| 62 | instance.post = AsyncMock(return_value=mock_response) | ||
| 63 | with patch.object(ctx, 'send_followup', new_callable=AsyncMock) as mock_followup: | ||
| 64 | await ai_cog.senator.callback(ai_cog, ctx, text="Hello", tts=False) | ||
| 65 | mock_followup.assert_called_once_with("bot siem zesroł") | ||
| 66 | |||
| 67 | |||
| 68 | @pytest.mark.asyncio | ||
| 69 | async def test_senator_command_no_choices_key(ai_cog, mock_channel, mock_guild, bot): | ||
| 70 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 71 | |||
| 72 | mock_response = MagicMock() | ||
| 73 | mock_response.json = MagicMock(return_value={}) | ||
| 74 | |||
| 75 | with patch('cogs.ai.httpx.AsyncClient') as mock_httpx: | ||
| 76 | instance = mock_httpx.return_value.__aenter__.return_value | ||
| 77 | instance.post = AsyncMock(return_value=mock_response) | ||
| 78 | with patch('cogs.ai.logger') as mock_logger: | ||
| 79 | try: | ||
| 80 | await ai_cog.senator.callback(ai_cog, ctx, text="Hello", tts=False) | ||
| 81 | except KeyError: | ||
| 82 | pass | ||
| 83 | assert mock_logger.info.called | ||
diff --git a/tests/cogs/test_assistant.py b/tests/cogs/test_assistant.py new file mode 100644 index 0000000..9a33232 --- /dev/null +++ b/tests/cogs/test_assistant.py | |||
| @@ -0,0 +1,66 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | from tests.conftest import MockApplicationContext | ||
| 4 | |||
| 5 | |||
| 6 | @pytest.fixture | ||
| 7 | def assistant_cog(): | ||
| 8 | with patch('os.getenv', return_value="test_keyword"): | ||
| 9 | from cogs.assistant import Jarvis | ||
| 10 | |||
| 11 | mock_bot = MagicMock() | ||
| 12 | mock_bot.user = MagicMock() | ||
| 13 | mock_bot.user.id = 111 | ||
| 14 | |||
| 15 | cog = Jarvis(mock_bot) | ||
| 16 | return cog | ||
| 17 | |||
| 18 | |||
| 19 | def test_assistant_keyword_default(assistant_cog): | ||
| 20 | assert assistant_cog.keyword == "test_keyword" | ||
| 21 | |||
| 22 | |||
| 23 | @pytest.mark.asyncio | ||
| 24 | async def test_assistant_on_message_ignored_bot_message(assistant_cog): | ||
| 25 | mock_author = MagicMock() | ||
| 26 | mock_author.id = 456 | ||
| 27 | mock_author.bot = True | ||
| 28 | |||
| 29 | mock_message = MagicMock() | ||
| 30 | mock_message.author = mock_author | ||
| 31 | |||
| 32 | with patch.object(assistant_cog, 'assistant', new_callable=AsyncMock) as mock_assistant: | ||
| 33 | await assistant_cog.on_message(mock_message) | ||
| 34 | mock_assistant.assert_not_called() | ||
| 35 | |||
| 36 | |||
| 37 | @pytest.mark.asyncio | ||
| 38 | async def test_assistant_on_message_mentions_bot(assistant_cog): | ||
| 39 | mock_message = MagicMock() | ||
| 40 | mock_message.author = MagicMock() | ||
| 41 | mock_message.author.id = 123 | ||
| 42 | |||
| 43 | mock_mention = MagicMock() | ||
| 44 | mock_mention.id = 111 | ||
| 45 | mock_message.mentions = [mock_mention] | ||
| 46 | mock_message.content = "test" | ||
| 47 | |||
| 48 | with patch.object(assistant_cog, 'assistant', new_callable=AsyncMock) as mock_assistant: | ||
| 49 | await assistant_cog.on_message(mock_message) | ||
| 50 | mock_assistant.assert_called_once_with(mock_message) | ||
| 51 | |||
| 52 | |||
| 53 | @pytest.mark.asyncio | ||
| 54 | async def test_assistant_on_message_keyword_match(assistant_cog): | ||
| 55 | assistant_cog.keyword = "test" | ||
| 56 | |||
| 57 | mock_message = MagicMock() | ||
| 58 | mock_message.author = MagicMock() | ||
| 59 | mock_message.author.id = 123 | ||
| 60 | |||
| 61 | mock_message.mentions = [] | ||
| 62 | mock_message.content = "test message with test keyword" | ||
| 63 | |||
| 64 | with patch.object(assistant_cog, 'assistant', new_callable=AsyncMock) as mock_assistant: | ||
| 65 | await assistant_cog.on_message(mock_message) | ||
| 66 | mock_assistant.assert_called_once_with(mock_message) | ||
diff --git a/tests/cogs/test_compression_conversion.py b/tests/cogs/test_compression_conversion.py new file mode 100644 index 0000000..f9d3427 --- /dev/null +++ b/tests/cogs/test_compression_conversion.py | |||
| @@ -0,0 +1,139 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import os | ||
| 4 | import discord | ||
| 5 | from tests.conftest import MockApplicationContext | ||
| 6 | |||
| 7 | |||
| 8 | def test_compression_filetype_video(): | ||
| 9 | from cogs.compression import filetype | ||
| 10 | assert filetype("test.mp4") == "Video" | ||
| 11 | assert filetype("test.avi") == "Video" | ||
| 12 | |||
| 13 | |||
| 14 | def test_compression_filetype_audio(): | ||
| 15 | from cogs.compression import filetype | ||
| 16 | assert filetype("test.mp3") == "Audio" | ||
| 17 | assert filetype("test.wav") == "Audio" | ||
| 18 | |||
| 19 | |||
| 20 | def test_compression_filetype_image(): | ||
| 21 | from cogs.compression import filetype | ||
| 22 | assert filetype("test.jpg") == "Image" | ||
| 23 | assert filetype("test.png") == "Image" | ||
| 24 | |||
| 25 | |||
| 26 | def test_conversion_filetype(): | ||
| 27 | from cogs.conversion import filetype | ||
| 28 | assert filetype("test.mp4") == "Video" | ||
| 29 | assert filetype("test.mp3") == "Audio" | ||
| 30 | assert filetype("test.jpg") == "Image" | ||
| 31 | assert filetype("test.xyz") == "Unknown" | ||
| 32 | |||
| 33 | |||
| 34 | @pytest.mark.asyncio | ||
| 35 | async def test_build_conversion_command_video(): | ||
| 36 | with patch('os.getenv', return_value="https://example.com"): | ||
| 37 | with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"): | ||
| 38 | with patch('cogs.utils.settings.get_baseurl', return_value="https://example.com"): | ||
| 39 | import importlib | ||
| 40 | import cogs.conversion | ||
| 41 | importlib.reload(cogs.conversion) | ||
| 42 | from cogs.conversion import Conversion | ||
| 43 | |||
| 44 | cog = Conversion.__new__(Conversion) | ||
| 45 | cog._baseurl = "https://example.com" | ||
| 46 | cog._uploadurl = "https://example.com" | ||
| 47 | |||
| 48 | cmd = cog._build_conversion_command("Video", "/tmp/test.mp4", "/tmp/output.mp4", "") | ||
| 49 | assert cmd is not None | ||
| 50 | assert "ffmpeg" in cmd | ||
| 51 | |||
| 52 | |||
| 53 | @pytest.mark.asyncio | ||
| 54 | async def test_build_conversion_command_image(): | ||
| 55 | with patch('os.getenv', return_value="https://example.com"): | ||
| 56 | with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"): | ||
| 57 | with patch('cogs.utils.settings.get_baseurl', return_value="https://example.com"): | ||
| 58 | import importlib | ||
| 59 | import cogs.conversion | ||
| 60 | importlib.reload(cogs.conversion) | ||
| 61 | from cogs.conversion import Conversion | ||
| 62 | |||
| 63 | cog = Conversion.__new__(Conversion) | ||
| 64 | cog._baseurl = "https://example.com" | ||
| 65 | cog._uploadurl = "https://example.com" | ||
| 66 | |||
| 67 | cmd = cog._build_conversion_command("Image", "/tmp/test.jpg", "/tmp/output.png", "") | ||
| 68 | assert cmd is not None | ||
| 69 | assert "magick" in cmd | ||
| 70 | |||
| 71 | |||
| 72 | @pytest.mark.asyncio | ||
| 73 | async def test_build_conversion_command_unknown(): | ||
| 74 | with patch('os.getenv', return_value="https://example.com"): | ||
| 75 | with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"): | ||
| 76 | with patch('cogs.utils.settings.get_baseurl', return_value="https://example.com"): | ||
| 77 | import importlib | ||
| 78 | import cogs.conversion | ||
| 79 | importlib.reload(cogs.conversion) | ||
| 80 | from cogs.conversion import Conversion | ||
| 81 | |||
| 82 | cog = Conversion.__new__(Conversion) | ||
| 83 | cog._baseurl = "https://example.com" | ||
| 84 | cog._uploadurl = "https://example.com" | ||
| 85 | |||
| 86 | cmd = cog._build_conversion_command("Unknown", "/tmp/test.xyz", "/tmp/output.abc", "") | ||
| 87 | assert cmd is None | ||
| 88 | |||
| 89 | |||
| 90 | @pytest.mark.asyncio | ||
| 91 | async def test_compression_get_command_video(): | ||
| 92 | with patch('os.getenv', return_value="https://example.com"): | ||
| 93 | with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"): | ||
| 94 | with patch('cogs.utils.settings.get_baseurl', return_value="https://example.com"): | ||
| 95 | import importlib | ||
| 96 | import cogs.compression | ||
| 97 | importlib.reload(cogs.compression) | ||
| 98 | from cogs.compression import Compression | ||
| 99 | |||
| 100 | cog = Compression.__new__(Compression) | ||
| 101 | cog.temp_dir = "/tmp" | ||
| 102 | |||
| 103 | cmd = cog._get_compression_command("Video", "/tmp/test.mp4", "/tmp/output.mp4") | ||
| 104 | assert cmd is not None | ||
| 105 | assert "ffmpeg" in cmd | ||
| 106 | |||
| 107 | |||
| 108 | @pytest.mark.asyncio | ||
| 109 | async def test_compression_get_command_image(): | ||
| 110 | with patch('os.getenv', return_value="https://example.com"): | ||
| 111 | with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"): | ||
| 112 | with patch('cogs.utils.settings.get_baseurl', return_value="https://example.com"): | ||
| 113 | import importlib | ||
| 114 | import cogs.compression | ||
| 115 | importlib.reload(cogs.compression) | ||
| 116 | from cogs.compression import Compression | ||
| 117 | |||
| 118 | cog = Compression.__new__(Compression) | ||
| 119 | cog.temp_dir = "/tmp" | ||
| 120 | |||
| 121 | cmd = cog._get_compression_command("Image", "/tmp/test.jpg", "/tmp/output.jpg") | ||
| 122 | assert cmd is not None | ||
| 123 | |||
| 124 | |||
| 125 | @pytest.mark.asyncio | ||
| 126 | async def test_compression_get_command_unknown(): | ||
| 127 | with patch('os.getenv', return_value="https://example.com"): | ||
| 128 | with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"): | ||
| 129 | with patch('cogs.utils.settings.get_baseurl', return_value="https://example.com"): | ||
| 130 | import importlib | ||
| 131 | import cogs.compression | ||
| 132 | importlib.reload(cogs.compression) | ||
| 133 | from cogs.compression import Compression | ||
| 134 | |||
| 135 | cog = Compression.__new__(Compression) | ||
| 136 | cog.temp_dir = "/tmp" | ||
| 137 | |||
| 138 | cmd = cog._get_compression_command("Unknown", "/tmp/test.xyz", "/tmp/output.xyz") | ||
| 139 | assert cmd is None | ||
diff --git a/tests/cogs/test_cron.py b/tests/cogs/test_cron.py new file mode 100644 index 0000000..93031f8 --- /dev/null +++ b/tests/cogs/test_cron.py | |||
| @@ -0,0 +1,166 @@ | |||
| 1 | import json | ||
| 2 | import os | ||
| 3 | import pytest | ||
| 4 | from datetime import datetime | ||
| 5 | from unittest.mock import MagicMock, patch | ||
| 6 | |||
| 7 | from cogs.cron import Cron | ||
| 8 | |||
| 9 | |||
| 10 | @pytest.fixture | ||
| 11 | def mock_bot(): | ||
| 12 | bot = MagicMock() | ||
| 13 | bot.loop = MagicMock() | ||
| 14 | bot.is_closed = MagicMock(return_value=False) | ||
| 15 | bot.wait_until_ready = MagicMock() | ||
| 16 | bot.loop.create_task = MagicMock(return_value=MagicMock()) | ||
| 17 | return bot | ||
| 18 | |||
| 19 | |||
| 20 | def create_cog(mock_bot, tmp_path): | ||
| 21 | with patch.object(Cron, "__init__", lambda self, bot: None): | ||
| 22 | cog = Cron.__new__(Cron) | ||
| 23 | cog.bot = mock_bot | ||
| 24 | cog.scheduled_tasks = [] | ||
| 25 | cog.cron_file = str(tmp_path / "cron_jobs.json") | ||
| 26 | cog.task = MagicMock() | ||
| 27 | return cog | ||
| 28 | |||
| 29 | |||
| 30 | class TestLoadTasks: | ||
| 31 | def test_load_tasks_file_not_exists(self, mock_bot, tmp_path): | ||
| 32 | cog = create_cog(mock_bot, tmp_path) | ||
| 33 | cog.cron_file = str(tmp_path / "nonexistent.json") | ||
| 34 | cog.load_tasks() | ||
| 35 | assert cog.scheduled_tasks == [] | ||
| 36 | |||
| 37 | def test_load_tasks_empty_file(self, mock_bot, tmp_path): | ||
| 38 | cron_file = tmp_path / "empty.json" | ||
| 39 | cron_file.write_text("") | ||
| 40 | cog = create_cog(mock_bot, tmp_path) | ||
| 41 | cog.cron_file = str(cron_file) | ||
| 42 | cog.load_tasks() | ||
| 43 | assert cog.scheduled_tasks == [] | ||
| 44 | |||
| 45 | def test_load_tasks_valid_file(self, mock_bot, tmp_path): | ||
| 46 | cron_file = tmp_path / "valid.json" | ||
| 47 | tasks = [{"id": 1, "cron_expression": "* * * * *", "type": "audio", "channel_id": 123, "active": True}] | ||
| 48 | cron_file.write_text(json.dumps(tasks)) | ||
| 49 | cog = create_cog(mock_bot, tmp_path) | ||
| 50 | cog.cron_file = str(cron_file) | ||
| 51 | cog.load_tasks() | ||
| 52 | assert len(cog.scheduled_tasks) == 1 | ||
| 53 | assert cog.scheduled_tasks[0]["id"] == 1 | ||
| 54 | |||
| 55 | def test_load_tasks_io_error(self, mock_bot, tmp_path): | ||
| 56 | cog = create_cog(mock_bot, tmp_path) | ||
| 57 | with patch("builtins.open", side_effect=IOError("test error")): | ||
| 58 | cog.load_tasks() | ||
| 59 | assert cog.scheduled_tasks == [] | ||
| 60 | |||
| 61 | |||
| 62 | class TestSaveTasks: | ||
| 63 | def test_save_tasks_with_real_file(self, mock_bot, tmp_path): | ||
| 64 | cron_file = tmp_path / "cron_jobs.json" | ||
| 65 | cog = create_cog(mock_bot, tmp_path) | ||
| 66 | cog.cron_file = str(cron_file) | ||
| 67 | cog.scheduled_tasks = [{"id": 1, "cron_expression": "* * * * *"}] | ||
| 68 | cog.save_tasks() | ||
| 69 | assert os.path.exists(cron_file) | ||
| 70 | with open(cron_file) as f: | ||
| 71 | loaded = json.load(f) | ||
| 72 | assert len(loaded) == 1 | ||
| 73 | |||
| 74 | def test_save_tasks_creates_directory_structure(self, mock_bot, tmp_path): | ||
| 75 | cog = create_cog(mock_bot, tmp_path) | ||
| 76 | new_dir = tmp_path / "new_dir" | ||
| 77 | cron_file = new_dir / "cron_jobs.json" | ||
| 78 | cog.cron_file = str(cron_file) | ||
| 79 | cog.scheduled_tasks = [{"id": 1}] | ||
| 80 | |||
| 81 | cog.save_tasks() | ||
| 82 | |||
| 83 | assert os.path.exists(new_dir) | ||
| 84 | |||
| 85 | |||
| 86 | class TestShouldRunTask: | ||
| 87 | def test_should_run_task_invalid_cron(self, mock_bot, tmp_path): | ||
| 88 | cog = create_cog(mock_bot, tmp_path) | ||
| 89 | task = {"cron_expression": "invalid"} | ||
| 90 | now = datetime.now() | ||
| 91 | result = cog.should_run_task(task, now) | ||
| 92 | assert result is False | ||
| 93 | |||
| 94 | def test_should_run_task_missing_cron_expression(self, mock_bot, tmp_path): | ||
| 95 | cog = create_cog(mock_bot, tmp_path) | ||
| 96 | task = {} | ||
| 97 | now = datetime.now() | ||
| 98 | result = cog.should_run_task(task, now) | ||
| 99 | assert result is False | ||
| 100 | |||
| 101 | |||
| 102 | class TestCogUnload: | ||
| 103 | def test_cog_unload_cancels_task(self, mock_bot, tmp_path): | ||
| 104 | with patch.object(Cron, "__init__", lambda self, bot: None): | ||
| 105 | cog = Cron.__new__(Cron) | ||
| 106 | cog.bot = mock_bot | ||
| 107 | cog.task = MagicMock() | ||
| 108 | |||
| 109 | cog.cog_unload() | ||
| 110 | cog.task.cancel.assert_called_once() | ||
| 111 | |||
| 112 | |||
| 113 | class TestTaskManagement: | ||
| 114 | def test_add_task_increments_id(self, mock_bot, tmp_path): | ||
| 115 | cog = create_cog(mock_bot, tmp_path) | ||
| 116 | cog.scheduled_tasks = [ | ||
| 117 | {"id": 1, "cron_expression": "* * * * *"}, | ||
| 118 | {"id": 2, "cron_expression": "0 * * * *"}, | ||
| 119 | ] | ||
| 120 | |||
| 121 | new_id = max([t["id"] for t in cog.scheduled_tasks], default=0) + 1 | ||
| 122 | assert new_id == 3 | ||
| 123 | |||
| 124 | def test_find_task_by_id(self, mock_bot, tmp_path): | ||
| 125 | cog = create_cog(mock_bot, tmp_path) | ||
| 126 | cog.scheduled_tasks = [ | ||
| 127 | {"id": 1, "cron_expression": "* * * * *"}, | ||
| 128 | {"id": 2, "cron_expression": "0 * * * *"}, | ||
| 129 | ] | ||
| 130 | |||
| 131 | found_task = next((t for t in cog.scheduled_tasks if t["id"] == 2), None) | ||
| 132 | assert found_task is not None | ||
| 133 | assert found_task["cron_expression"] == "0 * * * *" | ||
| 134 | |||
| 135 | def test_remove_task_by_index(self, mock_bot, tmp_path): | ||
| 136 | cog = create_cog(mock_bot, tmp_path) | ||
| 137 | cog.scheduled_tasks = [ | ||
| 138 | {"id": 1, "cron_expression": "* * * * *"}, | ||
| 139 | {"id": 2, "cron_expression": "0 * * * *"}, | ||
| 140 | ] | ||
| 141 | |||
| 142 | cog.scheduled_tasks.pop(0) | ||
| 143 | assert len(cog.scheduled_tasks) == 1 | ||
| 144 | assert cog.scheduled_tasks[0]["id"] == 2 | ||
| 145 | |||
| 146 | def test_toggle_task_active(self, mock_bot, tmp_path): | ||
| 147 | cog = create_cog(mock_bot, tmp_path) | ||
| 148 | cog.scheduled_tasks = [{"id": 1, "active": True}] | ||
| 149 | |||
| 150 | cog.scheduled_tasks[0]["active"] = False | ||
| 151 | assert cog.scheduled_tasks[0]["active"] is False | ||
| 152 | |||
| 153 | cog.scheduled_tasks[0]["active"] = True | ||
| 154 | assert cog.scheduled_tasks[0]["active"] is True | ||
| 155 | |||
| 156 | |||
| 157 | class TestTaskTypes: | ||
| 158 | def test_task_type_audio_aliases(self, mock_bot, tmp_path): | ||
| 159 | audio_types = ("zagraj_audio", "audio") | ||
| 160 | for task_type in audio_types: | ||
| 161 | assert task_type in ("zagraj_audio", "audio") | ||
| 162 | |||
| 163 | def test_task_type_message_aliases(self, mock_bot, tmp_path): | ||
| 164 | message_types = ("wyślij_wiadomość", "send_message") | ||
| 165 | for task_type in message_types: | ||
| 166 | assert task_type in ("wyślij_wiadomość", "send_message") | ||
diff --git a/tests/cogs/test_downloader.py b/tests/cogs/test_downloader.py new file mode 100644 index 0000000..6ac8ae7 --- /dev/null +++ b/tests/cogs/test_downloader.py | |||
| @@ -0,0 +1,93 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import discord | ||
| 4 | from tests.conftest import MockApplicationContext, MockMessage | ||
| 5 | |||
| 6 | |||
| 7 | @pytest.fixture | ||
| 8 | def downloader_cog(bot): | ||
| 9 | from cogs.downloader import Downloader | ||
| 10 | cog = Downloader(bot) | ||
| 11 | return cog | ||
| 12 | |||
| 13 | |||
| 14 | @pytest.mark.asyncio | ||
| 15 | async def test_download_command_success_file(downloader_cog, mock_channel, mock_guild, bot): | ||
| 16 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 17 | |||
| 18 | mock_file_path = "/tmp/test_video.mp4" | ||
| 19 | |||
| 20 | with patch('cogs.downloader.Downloader.download_standalone', return_value={ | ||
| 21 | "status": "success", | ||
| 22 | "type": "file", | ||
| 23 | "content": mock_file_path, | ||
| 24 | "filename": "test_video.mp4" | ||
| 25 | }): | ||
| 26 | with patch('discord.File', return_value=MagicMock()) as mock_file: | ||
| 27 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 28 | await downloader_cog.download.callback(downloader_cog, ctx, url="https://example.com/video", catbox=False, fformat="mp4", filename_user=None) | ||
| 29 | mock_respond.assert_called_once() | ||
| 30 | call_args = mock_respond.call_args | ||
| 31 | assert 'file' in str(call_args) or mock_file.called | ||
| 32 | |||
| 33 | |||
| 34 | @pytest.mark.asyncio | ||
| 35 | async def test_download_command_success_url(downloader_cog, mock_channel, mock_guild, bot): | ||
| 36 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 37 | |||
| 38 | with patch('cogs.downloader.Downloader.download_standalone', return_value={ | ||
| 39 | "status": "success", | ||
| 40 | "type": "url", | ||
| 41 | "content": "https://catbox.2137697.xyz/sperma/test.mp4" | ||
| 42 | }): | ||
| 43 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 44 | await downloader_cog.download.callback(downloader_cog, ctx, url="https://example.com/video", catbox=True, fformat="mp4", filename_user=None) | ||
| 45 | mock_respond.assert_called_once() | ||
| 46 | call_args = mock_respond.call_args | ||
| 47 | assert "pobioron" in str(call_args) | ||
| 48 | |||
| 49 | |||
| 50 | @pytest.mark.asyncio | ||
| 51 | async def test_download_command_error(downloader_cog, mock_channel, mock_guild, bot): | ||
| 52 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 53 | |||
| 54 | with patch('cogs.downloader.Downloader.download_standalone', return_value={ | ||
| 55 | "status": "error", | ||
| 56 | "type": "error", | ||
| 57 | "content": "Download failed", | ||
| 58 | "details": "Connection timeout" | ||
| 59 | }): | ||
| 60 | with patch('cogs.downloader.logger') as mock_logger: | ||
| 61 | await downloader_cog.download.callback(downloader_cog, ctx, url="https://badurl.com", catbox=False, fformat="mp4", filename_user=None) | ||
| 62 | assert mock_logger.error.called | ||
| 63 | |||
| 64 | |||
| 65 | @pytest.mark.asyncio | ||
| 66 | async def test_message_download_with_url(downloader_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 67 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 68 | |||
| 69 | mock_message = MagicMock() | ||
| 70 | mock_message.content = "Check this out https://example.com/video" | ||
| 71 | |||
| 72 | with patch('cogs.downloader.Downloader.download_standalone', return_value={ | ||
| 73 | "status": "success", | ||
| 74 | "type": "file", | ||
| 75 | "content": "/tmp/test.mp4" | ||
| 76 | }): | ||
| 77 | with patch.object(downloader_cog, 'download', new_callable=AsyncMock) as mock_download: | ||
| 78 | await downloader_cog.message_download.callback(downloader_cog, ctx, message=mock_message) | ||
| 79 | mock_download.assert_called_once() | ||
| 80 | |||
| 81 | |||
| 82 | @pytest.mark.asyncio | ||
| 83 | async def test_message_download_no_url(downloader_cog, mock_channel, mock_guild, bot): | ||
| 84 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 85 | |||
| 86 | mock_message = MagicMock() | ||
| 87 | mock_message.content = "No URL here" | ||
| 88 | |||
| 89 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 90 | await downloader_cog.message_download.callback(downloader_cog, ctx, message=mock_message) | ||
| 91 | mock_respond.assert_called_once() | ||
| 92 | call_args = mock_respond.call_args | ||
| 93 | assert "Brak" in str(call_args) | ||
diff --git a/tests/cogs/test_math_commands.py b/tests/cogs/test_math_commands.py new file mode 100644 index 0000000..5c78027 --- /dev/null +++ b/tests/cogs/test_math_commands.py | |||
| @@ -0,0 +1,55 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | from tests.conftest import MockApplicationContext | ||
| 4 | |||
| 5 | |||
| 6 | @pytest.fixture | ||
| 7 | def math_cog(bot): | ||
| 8 | from cogs.math_commands import Math | ||
| 9 | cog = Math(bot) | ||
| 10 | return cog | ||
| 11 | |||
| 12 | |||
| 13 | @pytest.mark.asyncio | ||
| 14 | async def test_math_command_valid_expression(math_cog, mock_channel, mock_guild, bot): | ||
| 15 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 16 | |||
| 17 | mock_process = MagicMock() | ||
| 18 | mock_process.returncode = 0 | ||
| 19 | mock_process.communicate = AsyncMock(return_value=(b"4", b"")) | ||
| 20 | |||
| 21 | with patch('cogs.math_commands.asyncio.create_subprocess_exec', return_value=mock_process) as mock_subprocess: | ||
| 22 | with patch('cogs.math_commands.re.search', return_value=None): | ||
| 23 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 24 | await math_cog.math.callback(math_cog, ctx, equation="2+2") | ||
| 25 | mock_subprocess.assert_called_once() | ||
| 26 | mock_respond.assert_called_once() | ||
| 27 | |||
| 28 | |||
| 29 | @pytest.mark.asyncio | ||
| 30 | async def test_math_command_invalid_expression(math_cog, mock_channel, mock_guild, bot): | ||
| 31 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 32 | |||
| 33 | with patch('cogs.math_commands.re.search', return_value=MagicMock()): | ||
| 34 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 35 | await math_cog.math.callback(math_cog, ctx, equation="eval(1)") | ||
| 36 | mock_respond.assert_called_once() | ||
| 37 | args = mock_respond.call_args | ||
| 38 | assert "Invalid" in str(args) | ||
| 39 | |||
| 40 | |||
| 41 | @pytest.mark.asyncio | ||
| 42 | async def test_math_command_process_error(math_cog, mock_channel, mock_guild, bot): | ||
| 43 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 44 | |||
| 45 | mock_process = MagicMock() | ||
| 46 | mock_process.returncode = 1 | ||
| 47 | mock_process.communicate = AsyncMock(return_value=(b"", b"Error: invalid expression")) | ||
| 48 | |||
| 49 | with patch('cogs.math_commands.asyncio.create_subprocess_exec', return_value=mock_process): | ||
| 50 | with patch('cogs.math_commands.re.search', return_value=None): | ||
| 51 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 52 | await math_cog.math.callback(math_cog, ctx, equation="2++") | ||
| 53 | mock_respond.assert_called_once() | ||
| 54 | args = mock_respond.call_args | ||
| 55 | assert "error" in str(args).lower() or "Error" in str(args) | ||
diff --git a/tests/cogs/test_music_audio.py b/tests/cogs/test_music_audio.py new file mode 100644 index 0000000..7ea902e --- /dev/null +++ b/tests/cogs/test_music_audio.py | |||
| @@ -0,0 +1,93 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import os | ||
| 4 | from tests.conftest import MockApplicationContext | ||
| 5 | |||
| 6 | |||
| 7 | @pytest.mark.asyncio | ||
| 8 | async def test_validate_voice_channel_not_in_voice(): | ||
| 9 | from cogs.music.audio_playback import AudioPlaybackManager | ||
| 10 | |||
| 11 | mock_ctx = MagicMock() | ||
| 12 | mock_ctx.author.voice = None | ||
| 13 | mock_ctx.respond = AsyncMock() | ||
| 14 | |||
| 15 | result = await AudioPlaybackManager.validate_voice_channel(mock_ctx) | ||
| 16 | |||
| 17 | assert result is None | ||
| 18 | mock_ctx.respond.assert_called_once() | ||
| 19 | |||
| 20 | |||
| 21 | @pytest.mark.asyncio | ||
| 22 | async def test_validate_voice_channel_in_voice(): | ||
| 23 | from cogs.music.audio_playback import AudioPlaybackManager | ||
| 24 | |||
| 25 | mock_channel = MagicMock() | ||
| 26 | mock_ctx = MagicMock() | ||
| 27 | mock_ctx.author.voice = MagicMock() | ||
| 28 | mock_ctx.author.voice.channel = mock_channel | ||
| 29 | mock_ctx.respond = AsyncMock() | ||
| 30 | |||
| 31 | result = await AudioPlaybackManager.validate_voice_channel(mock_ctx) | ||
| 32 | |||
| 33 | assert result == mock_channel | ||
| 34 | |||
| 35 | |||
| 36 | @pytest.mark.asyncio | ||
| 37 | async def test_prepare_file_source_file_not_found(): | ||
| 38 | from cogs.music.audio_playback import AudioPlaybackManager | ||
| 39 | |||
| 40 | display_name, absolute_path = await AudioPlaybackManager.prepare_file_source("/nonexistent/file.mp3") | ||
| 41 | |||
| 42 | assert display_name is None | ||
| 43 | assert absolute_path is None | ||
| 44 | |||
| 45 | |||
| 46 | @pytest.mark.asyncio | ||
| 47 | async def test_prepare_file_source_absolute_path(): | ||
| 48 | from cogs.music.audio_playback import AudioPlaybackManager | ||
| 49 | |||
| 50 | with patch('os.path.exists', return_value=True): | ||
| 51 | with patch('os.path.isabs', return_value=True): | ||
| 52 | display_name, absolute_path = await AudioPlaybackManager.prepare_file_source("/tmp/test.mp3") | ||
| 53 | |||
| 54 | assert display_name == "test" | ||
| 55 | assert absolute_path == "/tmp/test.mp3" | ||
| 56 | |||
| 57 | |||
| 58 | @pytest.mark.asyncio | ||
| 59 | async def test_prepare_source_url(): | ||
| 60 | from cogs.music.audio_playback import AudioPlaybackManager | ||
| 61 | |||
| 62 | source, _, is_url = await AudioPlaybackManager.prepare_source(MagicMock(), "https://example.com/stream") | ||
| 63 | |||
| 64 | assert is_url == True | ||
| 65 | assert source == "https://example.com/stream" | ||
| 66 | |||
| 67 | |||
| 68 | @pytest.mark.asyncio | ||
| 69 | async def test_prepare_source_local_file(): | ||
| 70 | from cogs.music.audio_playback import AudioPlaybackManager | ||
| 71 | |||
| 72 | with patch('os.path.exists', return_value=True): | ||
| 73 | source, display_name, is_url = await AudioPlaybackManager.prepare_source(MagicMock(), "/tmp/test.mp3") | ||
| 74 | |||
| 75 | assert is_url == False | ||
| 76 | assert display_name == "test" | ||
| 77 | |||
| 78 | |||
| 79 | @pytest.mark.asyncio | ||
| 80 | async def test_cleanup_temp_file(): | ||
| 81 | from cogs.music.audio_playback import AudioPlaybackManager | ||
| 82 | |||
| 83 | with patch('os.path.exists', return_value=True): | ||
| 84 | with patch('os.remove'): | ||
| 85 | await AudioPlaybackManager.cleanup_temp_file("/tmp/test.mp3") | ||
| 86 | |||
| 87 | |||
| 88 | @pytest.mark.asyncio | ||
| 89 | async def test_cleanup_temp_file_not_exists(): | ||
| 90 | from cogs.music.audio_playback import AudioPlaybackManager | ||
| 91 | |||
| 92 | with patch('os.path.exists', return_value=False): | ||
| 93 | await AudioPlaybackManager.cleanup_temp_file("/nonexistent/file.mp3") | ||
diff --git a/tests/cogs/test_music_external_play.py b/tests/cogs/test_music_external_play.py new file mode 100644 index 0000000..fd4b625 --- /dev/null +++ b/tests/cogs/test_music_external_play.py | |||
| @@ -0,0 +1,116 @@ | |||
| 1 | import pytest | ||
| 2 | import os | ||
| 3 | import asyncio | ||
| 4 | import tempfile | ||
| 5 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 6 | from tests.conftest import MockApplicationContext | ||
| 7 | |||
| 8 | |||
| 9 | @pytest.fixture | ||
| 10 | def play_commands_cog(bot): | ||
| 11 | with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"): | ||
| 12 | from cogs.music.play_commands import PlayCommands | ||
| 13 | cog = PlayCommands(bot) | ||
| 14 | return cog | ||
| 15 | |||
| 16 | |||
| 17 | @pytest.fixture | ||
| 18 | def temp_mp3_file(): | ||
| 19 | with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f: | ||
| 20 | f.write(b'fake mp3 content') | ||
| 21 | temp_path = f.name | ||
| 22 | yield temp_path | ||
| 23 | if os.path.exists(temp_path): | ||
| 24 | os.remove(temp_path) | ||
| 25 | |||
| 26 | |||
| 27 | @pytest.mark.asyncio | ||
| 28 | async def test_external_url_pattern_matches_youtube(play_commands_cog): | ||
| 29 | from cogs.music.play_commands import EXTERNAL_URL_PATTERN | ||
| 30 | |||
| 31 | assert EXTERNAL_URL_PATTERN.match("https://youtube.com/watch?v=test") | ||
| 32 | assert EXTERNAL_URL_PATTERN.match("https://youtu.be/test") | ||
| 33 | assert EXTERNAL_URL_PATTERN.match("https://www.youtube.com/watch?v=test") | ||
| 34 | |||
| 35 | |||
| 36 | @pytest.mark.asyncio | ||
| 37 | async def test_external_url_pattern_matches_soundcloud(play_commands_cog): | ||
| 38 | from cogs.music.play_commands import EXTERNAL_URL_PATTERN | ||
| 39 | |||
| 40 | assert EXTERNAL_URL_PATTERN.match("https://soundcloud.com/artist/track") | ||
| 41 | assert EXTERNAL_URL_PATTERN.match("https://api.soundcloud.com/tracks") | ||
| 42 | |||
| 43 | |||
| 44 | @pytest.mark.asyncio | ||
| 45 | async def test_external_url_pattern_matches_bandcamp(play_commands_cog): | ||
| 46 | from cogs.music.play_commands import EXTERNAL_URL_PATTERN | ||
| 47 | |||
| 48 | assert EXTERNAL_URL_PATTERN.match("https://artist.bandcamp.com/track") | ||
| 49 | |||
| 50 | |||
| 51 | @pytest.mark.asyncio | ||
| 52 | async def test_external_url_pattern_rejects_copyparty(play_commands_cog): | ||
| 53 | from cogs.music.play_commands import EXTERNAL_URL_PATTERN | ||
| 54 | |||
| 55 | assert not EXTERNAL_URL_PATTERN.match("https://catbox.2137697.xyz/sperma/file.mp3") | ||
| 56 | assert not EXTERNAL_URL_PATTERN.match("some search query") | ||
| 57 | |||
| 58 | |||
| 59 | @pytest.mark.asyncio | ||
| 60 | async def test_play_command_calls_external_handler(play_commands_cog, mock_channel, mock_guild, bot): | ||
| 61 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 62 | |||
| 63 | with patch.object(play_commands_cog, '_handle_external_url_play', new_callable=AsyncMock) as mock_handler: | ||
| 64 | mock_handler.return_value = None | ||
| 65 | mock_defer = AsyncMock() | ||
| 66 | ctx.defer = mock_defer | ||
| 67 | ctx.respond = AsyncMock() | ||
| 68 | |||
| 69 | await play_commands_cog.play.callback( | ||
| 70 | play_commands_cog, ctx, | ||
| 71 | melodia="https://youtube.com/watch?v=test", | ||
| 72 | file=None, compressed=False | ||
| 73 | ) | ||
| 74 | |||
| 75 | mock_handler.assert_called_once_with(ctx, "https://youtube.com/watch?v=test") | ||
| 76 | |||
| 77 | |||
| 78 | @pytest.mark.asyncio | ||
| 79 | async def test_play_command_calls_local_handler(play_commands_cog, mock_channel, mock_guild, bot): | ||
| 80 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 81 | |||
| 82 | with patch.object(play_commands_cog, '_handle_local_file_play', new_callable=AsyncMock) as mock_handler: | ||
| 83 | mock_handler.return_value = None | ||
| 84 | mock_defer = AsyncMock() | ||
| 85 | ctx.defer = mock_defer | ||
| 86 | ctx.respond = AsyncMock() | ||
| 87 | |||
| 88 | await play_commands_cog.play.callback( | ||
| 89 | play_commands_cog, ctx, | ||
| 90 | melodia="some search query", | ||
| 91 | file=None, compressed=False | ||
| 92 | ) | ||
| 93 | |||
| 94 | mock_handler.assert_called_once_with(ctx, "some search query", False) | ||
| 95 | |||
| 96 | |||
| 97 | @pytest.mark.asyncio | ||
| 98 | async def test_upload_to_copyparty_async(play_commands_cog, temp_mp3_file, monkeypatch): | ||
| 99 | monkeypatch.setenv("COPYPARTY_URL_UPLOAD", "https://catbox.2137697.xyz/sperma/") | ||
| 100 | monkeypatch.setenv("COPYPARTY_PASSWORD", "testpassword") | ||
| 101 | |||
| 102 | mock_response = AsyncMock() | ||
| 103 | mock_response.status = 200 | ||
| 104 | mock_response.text = AsyncMock(return_value="") | ||
| 105 | mock_response.json = AsyncMock(return_value={"fileurl": "https://example.com/file.mp3"}) | ||
| 106 | mock_response.__aenter__ = AsyncMock(return_value=mock_response) | ||
| 107 | mock_response.__aexit__ = AsyncMock() | ||
| 108 | |||
| 109 | mock_session_context = AsyncMock() | ||
| 110 | mock_session_context.__aenter__ = AsyncMock(return_value=mock_session_context) | ||
| 111 | mock_session_context.__aexit__ = AsyncMock() | ||
| 112 | mock_session_context.post = MagicMock(return_value=mock_response) | ||
| 113 | |||
| 114 | with patch('aiohttp.ClientSession', return_value=mock_session_context): | ||
| 115 | await play_commands_cog._upload_to_copyparty_async(temp_mp3_file) | ||
| 116 | mock_session_context.post.assert_called_once() | ||
diff --git a/tests/cogs/test_music_join.py b/tests/cogs/test_music_join.py new file mode 100644 index 0000000..3c7ca6e --- /dev/null +++ b/tests/cogs/test_music_join.py | |||
| @@ -0,0 +1,53 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | from tests.conftest import MockApplicationContext | ||
| 4 | |||
| 5 | |||
| 6 | @pytest.fixture | ||
| 7 | def join_cog(bot): | ||
| 8 | from cogs.music.join import Join | ||
| 9 | cog = Join(bot) | ||
| 10 | return cog | ||
| 11 | |||
| 12 | |||
| 13 | @pytest.fixture | ||
| 14 | def stop_cog(bot): | ||
| 15 | from cogs.music.stop import Stop | ||
| 16 | cog = Stop(bot) | ||
| 17 | return cog | ||
| 18 | |||
| 19 | |||
| 20 | @pytest.mark.asyncio | ||
| 21 | async def test_join_command_success(join_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 22 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 23 | ctx.user = mock_user | ||
| 24 | ctx.author = mock_user | ||
| 25 | |||
| 26 | mock_voice_channel = MagicMock() | ||
| 27 | mock_voice = MagicMock() | ||
| 28 | mock_voice_channel.connect = AsyncMock(return_value=mock_voice) | ||
| 29 | |||
| 30 | ctx.author.voice = MagicMock() | ||
| 31 | ctx.author.voice.channel = mock_voice_channel | ||
| 32 | |||
| 33 | with patch.object(mock_voice_channel, 'connect', new_callable=AsyncMock, return_value=mock_voice): | ||
| 34 | with patch.object(ctx, 'respond', new_callable=AsyncMock): | ||
| 35 | with patch.object(ctx.interaction, 'delete_original_response', new_callable=AsyncMock): | ||
| 36 | await join_cog.join.callback(join_cog, ctx) | ||
| 37 | |||
| 38 | |||
| 39 | @pytest.mark.asyncio | ||
| 40 | async def test_stop_command_success(stop_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 41 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 42 | ctx.user = mock_user | ||
| 43 | ctx.author = mock_user | ||
| 44 | |||
| 45 | mock_voice_client = MagicMock() | ||
| 46 | mock_voice_client.disconnect = AsyncMock() | ||
| 47 | ctx.voice_client = mock_voice_client | ||
| 48 | |||
| 49 | with patch.object(ctx, 'respond', new_callable=AsyncMock): | ||
| 50 | with patch.object(ctx.interaction, 'delete_original_response', new_callable=AsyncMock): | ||
| 51 | await stop_cog.stop.callback(stop_cog, ctx) | ||
| 52 | |||
| 53 | mock_voice_client.disconnect.assert_called_once() | ||
diff --git a/tests/cogs/test_music_play.py b/tests/cogs/test_music_play.py new file mode 100644 index 0000000..e720c9a --- /dev/null +++ b/tests/cogs/test_music_play.py | |||
| @@ -0,0 +1,104 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import os | ||
| 4 | from tests.conftest import MockApplicationContext | ||
| 5 | |||
| 6 | |||
| 7 | @pytest.fixture | ||
| 8 | def play_commands_cog(bot): | ||
| 9 | with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"): | ||
| 10 | from cogs.music.play_commands import PlayCommands | ||
| 11 | cog = PlayCommands(bot) | ||
| 12 | return cog | ||
| 13 | |||
| 14 | |||
| 15 | @pytest.fixture | ||
| 16 | def loop_cog(bot): | ||
| 17 | from cogs.music.loop import Loop | ||
| 18 | cog = Loop(bot) | ||
| 19 | return cog | ||
| 20 | |||
| 21 | |||
| 22 | @pytest.mark.asyncio | ||
| 23 | async def test_create_search_embed_empty(play_commands_cog): | ||
| 24 | embed, view = await play_commands_cog._create_search_embed([]) | ||
| 25 | |||
| 26 | assert embed.title == "Wyniki wyszukiwania" | ||
| 27 | assert len(view.children) == 0 | ||
| 28 | |||
| 29 | |||
| 30 | @pytest.mark.asyncio | ||
| 31 | async def test_create_search_embed_with_candidates(play_commands_cog): | ||
| 32 | candidates = [ | ||
| 33 | "https://example.com/test1.mp3", | ||
| 34 | "https://example.com/test2.mp3" | ||
| 35 | ] | ||
| 36 | |||
| 37 | embed, view = await play_commands_cog._create_search_embed(candidates) | ||
| 38 | |||
| 39 | assert embed.title == "Wyniki wyszukiwania" | ||
| 40 | assert len(embed.fields) == 2 | ||
| 41 | |||
| 42 | |||
| 43 | @pytest.mark.asyncio | ||
| 44 | async def test_play_command_no_file_or_search(play_commands_cog, mock_channel, mock_guild, bot): | ||
| 45 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 46 | |||
| 47 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 48 | await play_commands_cog.play.callback(play_commands_cog, ctx, melodia=None, file=None, compressed=False) | ||
| 49 | mock_respond.assert_called_once() | ||
| 50 | args = mock_respond.call_args | ||
| 51 | assert "Musisz podać" in str(args) | ||
| 52 | |||
| 53 | |||
| 54 | @pytest.mark.asyncio | ||
| 55 | async def test_play_command_both_file_and_search(play_commands_cog, mock_channel, mock_guild, bot): | ||
| 56 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 57 | |||
| 58 | mock_file = MagicMock() | ||
| 59 | mock_file.filename = "test.mp3" | ||
| 60 | |||
| 61 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 62 | await play_commands_cog.play.callback(play_commands_cog, ctx, melodia="test", file=mock_file, compressed=False) | ||
| 63 | mock_respond.assert_called_once() | ||
| 64 | args = mock_respond.call_args | ||
| 65 | assert "nie oba" in str(args) or "LUB" in str(args) | ||
| 66 | |||
| 67 | |||
| 68 | def test_loop_cog_is_looping_initial(loop_cog): | ||
| 69 | assert loop_cog.is_looping(123) == False | ||
| 70 | |||
| 71 | |||
| 72 | def test_loop_cog_set_looping(loop_cog): | ||
| 73 | loop_cog.set_looping(123, True) | ||
| 74 | assert loop_cog.is_looping(123) == True | ||
| 75 | |||
| 76 | |||
| 77 | @pytest.mark.asyncio | ||
| 78 | async def test_loop_command_toggle_on(loop_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 79 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 80 | ctx.user = mock_user | ||
| 81 | ctx.author = mock_user | ||
| 82 | ctx.guild.id = 123 | ||
| 83 | |||
| 84 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 85 | await loop_cog.loop.callback(loop_cog, ctx) | ||
| 86 | mock_respond.assert_called_once() | ||
| 87 | args = mock_respond.call_args | ||
| 88 | assert "włączony" in str(args) | ||
| 89 | |||
| 90 | |||
| 91 | @pytest.mark.asyncio | ||
| 92 | async def test_loop_command_toggle_off(loop_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 93 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 94 | ctx.user = mock_user | ||
| 95 | ctx.author = mock_user | ||
| 96 | ctx.guild.id = 456 | ||
| 97 | |||
| 98 | loop_cog.set_looping(456, True) | ||
| 99 | |||
| 100 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 101 | await loop_cog.loop.callback(loop_cog, ctx) | ||
| 102 | mock_respond.assert_called_once() | ||
| 103 | args = mock_respond.call_args | ||
| 104 | assert "wyłączony" in str(args) | ||
diff --git a/tests/cogs/test_qr.py b/tests/cogs/test_qr.py new file mode 100644 index 0000000..2a132ca --- /dev/null +++ b/tests/cogs/test_qr.py | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock, mock_open | ||
| 3 | import os | ||
| 4 | from cogs.qr import QR | ||
| 5 | from tests.conftest import MockApplicationContext | ||
| 6 | |||
| 7 | |||
| 8 | @pytest.fixture | ||
| 9 | def qr_cog(bot): | ||
| 10 | cog = QR(bot) | ||
| 11 | return cog | ||
| 12 | |||
| 13 | |||
| 14 | @pytest.fixture | ||
| 15 | def mock_temp_file(): | ||
| 16 | with patch('tempfile.NamedTemporaryFile') as mock: | ||
| 17 | mock_file = MagicMock() | ||
| 18 | mock_file.name = "/tmp/test_qr.png" | ||
| 19 | mock.return_value = mock_file | ||
| 20 | yield mock_file | ||
| 21 | |||
| 22 | |||
| 23 | @pytest.mark.asyncio | ||
| 24 | async def test_qr_generate_success(qr_cog, mock_channel, mock_guild, bot, mock_temp_file): | ||
| 25 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 26 | |||
| 27 | with patch('cogs.qr.QR._generate_qr') as mock_generate: | ||
| 28 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 29 | with patch('os.path.exists', return_value=True): | ||
| 30 | with patch('os.unlink'): | ||
| 31 | await qr_cog.qr.callback(qr_cog, ctx, text="https://example.com") | ||
| 32 | mock_generate.assert_called_once() | ||
| 33 | mock_respond.assert_called_once() | ||
| 34 | |||
| 35 | |||
| 36 | @pytest.mark.asyncio | ||
| 37 | async def test_qr_generate_empty_text(qr_cog, mock_channel, mock_guild, bot): | ||
| 38 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 39 | |||
| 40 | with patch('cogs.qr.QR._generate_qr'): | ||
| 41 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 42 | with patch('os.path.exists', return_value=False): | ||
| 43 | await qr_cog.qr.callback(qr_cog, ctx, text="") | ||
| 44 | mock_respond.assert_called_once() | ||
| 45 | |||
| 46 | |||
| 47 | @pytest.mark.asyncio | ||
| 48 | async def test_qr_decode_not_available(qr_cog, mock_channel, mock_guild, bot): | ||
| 49 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 50 | |||
| 51 | mock_attachment = MagicMock() | ||
| 52 | mock_attachment.url = "https://example.com/qr.png" | ||
| 53 | |||
| 54 | with patch('cogs.qr._ensure_qr', return_value=False): | ||
| 55 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 56 | await qr_cog.decode.callback(qr_cog, ctx, image=mock_attachment) | ||
| 57 | mock_respond.assert_called_once() | ||
| 58 | args = mock_respond.call_args | ||
| 59 | assert "nie jest dostępne" in str(args) or "not available" in str(args).lower() | ||
| 60 | |||
| 61 | |||
| 62 | @pytest.mark.asyncio | ||
| 63 | async def test_qr_decode_download_failure(qr_cog, mock_channel, mock_guild, bot): | ||
| 64 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 65 | |||
| 66 | mock_attachment = MagicMock() | ||
| 67 | mock_attachment.url = "https://example.com/qr.png" | ||
| 68 | |||
| 69 | with patch('cogs.qr._ensure_qr', return_value=True): | ||
| 70 | with patch.object(qr_cog, '_download_image', return_value=None): | ||
| 71 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 72 | await qr_cog.decode.callback(qr_cog, ctx, image=mock_attachment) | ||
| 73 | mock_respond.assert_called_once() | ||
| 74 | args = mock_respond.call_args | ||
| 75 | assert "Nie udało się" in str(args) or "failed" in str(args).lower() | ||
diff --git a/tests/cogs/test_say.py b/tests/cogs/test_say.py new file mode 100644 index 0000000..13cad40 --- /dev/null +++ b/tests/cogs/test_say.py | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch | ||
| 3 | from cogs.say import Say | ||
| 4 | from tests.conftest import MockApplicationContext | ||
| 5 | |||
| 6 | |||
| 7 | @pytest.fixture | ||
| 8 | def say_cog(bot): | ||
| 9 | cog = Say(bot) | ||
| 10 | return cog | ||
| 11 | |||
| 12 | |||
| 13 | @pytest.mark.asyncio | ||
| 14 | async def test_say_command(say_cog, mock_channel, mock_guild, bot): | ||
| 15 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 16 | |||
| 17 | with patch.object(ctx, 'send', new_callable=AsyncMock) as mock_send: | ||
| 18 | await say_cog.say.callback(say_cog, ctx, "Hello World") | ||
| 19 | mock_send.assert_called_once_with(content="Hello World") | ||
| 20 | |||
| 21 | |||
| 22 | @pytest.mark.asyncio | ||
| 23 | async def test_say_command_empty_text(say_cog, mock_channel, mock_guild, bot): | ||
| 24 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 25 | |||
| 26 | with patch.object(ctx, 'send', new_callable=AsyncMock) as mock_send: | ||
| 27 | await say_cog.say.callback(say_cog, ctx, "") | ||
| 28 | mock_send.assert_called_once_with(content="") | ||
| 29 | |||
| 30 | |||
| 31 | @pytest.mark.asyncio | ||
| 32 | async def test_say_command_long_text(say_cog, mock_channel, mock_guild, bot): | ||
| 33 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 34 | long_text = "A" * 2000 | ||
| 35 | |||
| 36 | with patch.object(ctx, 'send', new_callable=AsyncMock) as mock_send: | ||
| 37 | await say_cog.say.callback(say_cog, ctx, long_text) | ||
| 38 | mock_send.assert_called_once_with(content=long_text) | ||
diff --git a/tests/cogs/test_search.py b/tests/cogs/test_search.py new file mode 100644 index 0000000..8c04dfc --- /dev/null +++ b/tests/cogs/test_search.py | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import discord | ||
| 4 | from tests.conftest import MockApplicationContext | ||
| 5 | |||
| 6 | |||
| 7 | @pytest.fixture | ||
| 8 | def search_cog(bot): | ||
| 9 | from cogs.search import Search | ||
| 10 | cog = Search(bot) | ||
| 11 | return cog | ||
| 12 | |||
| 13 | |||
| 14 | @pytest.mark.asyncio | ||
| 15 | async def test_search_command_success(search_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 16 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 17 | ctx.user = mock_user | ||
| 18 | ctx.author = mock_user | ||
| 19 | |||
| 20 | search_cog._cached_results[mock_user.id] = { | ||
| 21 | "testfile.mp4": "https://example.com/files/testfile.mp4" | ||
| 22 | } | ||
| 23 | |||
| 24 | with patch.object(ctx, 'send_followup', new_callable=AsyncMock) as mock_followup: | ||
| 25 | await search_cog.search.callback(search_cog, ctx, filename="testfile.mp4") | ||
| 26 | mock_followup.assert_called_once_with("https://example.com/files/testfile.mp4") | ||
| 27 | assert mock_user.id not in search_cog._cached_results | ||
| 28 | |||
| 29 | |||
| 30 | @pytest.mark.asyncio | ||
| 31 | async def test_search_command_no_cache(search_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 32 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 33 | ctx.user = mock_user | ||
| 34 | ctx.author = mock_user | ||
| 35 | |||
| 36 | with patch.object(ctx, 'send_followup', new_callable=AsyncMock) as mock_followup: | ||
| 37 | await search_cog.search.callback(search_cog, ctx, filename="testfile.mp4") | ||
| 38 | mock_followup.assert_called_once_with("Search results expired. Please search again.") | ||
| 39 | |||
| 40 | |||
| 41 | @pytest.mark.asyncio | ||
| 42 | async def test_search_command_file_not_found(search_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 43 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 44 | ctx.user = mock_user | ||
| 45 | ctx.author = mock_user | ||
| 46 | |||
| 47 | search_cog._cached_results[mock_user.id] = { | ||
| 48 | "testfile.mp4": "https://example.com/files/testfile.mp4" | ||
| 49 | } | ||
| 50 | |||
| 51 | with patch.object(ctx, 'send_followup', new_callable=AsyncMock) as mock_followup: | ||
| 52 | await search_cog.search.callback(search_cog, ctx, filename="nonexistent.mp4") | ||
| 53 | mock_followup.assert_called_once_with("File 'nonexistent.mp4' not found.") | ||
| 54 | |||
| 55 | |||
| 56 | @pytest.mark.asyncio | ||
| 57 | async def test_search_command_empty_filename(search_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 58 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 59 | ctx.user = mock_user | ||
| 60 | ctx.author = mock_user | ||
| 61 | |||
| 62 | with patch.object(ctx, 'reply', new_callable=AsyncMock) as mock_reply: | ||
| 63 | await search_cog.search.callback(search_cog, ctx, filename="") | ||
| 64 | mock_reply.assert_called_once_with("Please select a file from the autocomplete options.") | ||
| 65 | |||
| 66 | |||
| 67 | @pytest.mark.asyncio | ||
| 68 | async def test_file_autocomplete(search_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 69 | mock_autocomplete_ctx = MagicMock() | ||
| 70 | mock_autocomplete_ctx.value = "test" | ||
| 71 | mock_autocomplete_ctx.interaction = MagicMock() | ||
| 72 | mock_autocomplete_ctx.interaction.user = mock_user | ||
| 73 | |||
| 74 | with patch('cogs.utils.file_search.UniversalSearcher.search_copyparty_autocomplete', new_callable=AsyncMock) as mock_search: | ||
| 75 | mock_search.return_value = [ | ||
| 76 | "https://example.com/files/test1.mp4", | ||
| 77 | "https://example.com/files/test2.mp4" | ||
| 78 | ] | ||
| 79 | |||
| 80 | choices = await search_cog.file_autocomplete(mock_autocomplete_ctx) | ||
| 81 | |||
| 82 | assert len(choices) == 2 | ||
| 83 | assert mock_user.id in search_cog._cached_results | ||
diff --git a/tests/cogs/test_trigger.py b/tests/cogs/test_trigger.py new file mode 100644 index 0000000..6aaaa11 --- /dev/null +++ b/tests/cogs/test_trigger.py | |||
| @@ -0,0 +1,106 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import sqlite3 | ||
| 4 | from tests.conftest import MockApplicationContext | ||
| 5 | |||
| 6 | |||
| 7 | @pytest.fixture | ||
| 8 | def trigger_cog(bot): | ||
| 9 | with patch('cogs.trigger.get_connection') as mock_conn: | ||
| 10 | mock_conn.return_value.__enter__ = MagicMock(return_value=MagicMock()) | ||
| 11 | mock_conn.return_value.__exit__ = MagicMock(return_value=False) | ||
| 12 | from cogs.trigger import Trigger | ||
| 13 | cog = Trigger(bot) | ||
| 14 | return cog | ||
| 15 | |||
| 16 | |||
| 17 | @pytest.fixture | ||
| 18 | def mock_db_connection(): | ||
| 19 | with patch('cogs.trigger.get_connection') as mock_get_conn: | ||
| 20 | mock_conn = MagicMock() | ||
| 21 | mock_cursor = MagicMock() | ||
| 22 | mock_conn.cursor.return_value = mock_cursor | ||
| 23 | mock_get_conn.return_value.__enter__ = MagicMock(return_value=mock_conn) | ||
| 24 | mock_get_conn.return_value.__exit__ = MagicMock(return_value=False) | ||
| 25 | yield mock_conn, mock_cursor | ||
| 26 | |||
| 27 | |||
| 28 | @pytest.mark.asyncio | ||
| 29 | async def test_trigger_create_command(trigger_cog, mock_channel, mock_guild, bot, mock_db_connection): | ||
| 30 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 31 | |||
| 32 | mock_conn, mock_cursor = mock_db_connection | ||
| 33 | mock_cursor.fetchone.return_value = None | ||
| 34 | |||
| 35 | with patch('cogs.trigger.get_connection') as mock_get_conn: | ||
| 36 | mock_conn = MagicMock() | ||
| 37 | mock_cursor = MagicMock() | ||
| 38 | mock_conn.cursor.return_value = mock_cursor | ||
| 39 | mock_get_conn.return_value.__enter__ = MagicMock(return_value=mock_conn) | ||
| 40 | mock_get_conn.return_value.__exit__ = MagicMock(return_value=False) | ||
| 41 | |||
| 42 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 43 | await trigger_cog.create.callback(trigger_cog, ctx, name="test_trigger") | ||
| 44 | mock_respond.assert_called_once_with("trigger added") | ||
| 45 | |||
| 46 | |||
| 47 | @pytest.mark.asyncio | ||
| 48 | async def test_trigger_add_command(trigger_cog, mock_channel, mock_guild, bot, mock_db_connection): | ||
| 49 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 50 | |||
| 51 | with patch('cogs.trigger.get_connection') as mock_get_conn: | ||
| 52 | mock_conn = MagicMock() | ||
| 53 | mock_cursor = MagicMock() | ||
| 54 | mock_cursor.fetchone.return_value = (1,) # trigger exists | ||
| 55 | mock_conn.cursor.return_value = mock_cursor | ||
| 56 | mock_get_conn.return_value.__enter__ = MagicMock(return_value=mock_conn) | ||
| 57 | mock_get_conn.return_value.__exit__ = MagicMock(return_value=False) | ||
| 58 | |||
| 59 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 60 | await trigger_cog.add.callback(trigger_cog, ctx, name="test_trigger", trigger="hello", response="Hi there!") | ||
| 61 | mock_respond.assert_called_once_with("trigger updated") | ||
| 62 | |||
| 63 | |||
| 64 | @pytest.mark.asyncio | ||
| 65 | async def test_trigger_add_command_not_found(trigger_cog, mock_channel, mock_guild, bot, mock_db_connection): | ||
| 66 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 67 | |||
| 68 | with patch('cogs.trigger.get_connection') as mock_get_conn: | ||
| 69 | mock_conn = MagicMock() | ||
| 70 | mock_cursor = MagicMock() | ||
| 71 | mock_cursor.fetchone.return_value = None # trigger not found | ||
| 72 | mock_conn.cursor.return_value = mock_cursor | ||
| 73 | mock_get_conn.return_value.__enter__ = MagicMock(return_value=mock_conn) | ||
| 74 | mock_get_conn.return_value.__exit__ = MagicMock(return_value=False) | ||
| 75 | |||
| 76 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 77 | await trigger_cog.add.callback(trigger_cog, ctx, name="nonexistent", trigger="hello", response="Hi") | ||
| 78 | mock_respond.assert_called_once_with("Trigger not found.") | ||
| 79 | |||
| 80 | |||
| 81 | @pytest.mark.asyncio | ||
| 82 | async def test_trigger_delete_command(trigger_cog, mock_channel, mock_guild, bot, mock_db_connection): | ||
| 83 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 84 | |||
| 85 | with patch('cogs.trigger.get_connection') as mock_get_conn: | ||
| 86 | mock_conn = MagicMock() | ||
| 87 | mock_cursor = MagicMock() | ||
| 88 | mock_cursor.fetchone.return_value = (1,) # trigger exists | ||
| 89 | mock_conn.cursor.return_value = mock_cursor | ||
| 90 | mock_get_conn.return_value.__enter__ = MagicMock(return_value=mock_conn) | ||
| 91 | mock_get_conn.return_value.__exit__ = MagicMock(return_value=False) | ||
| 92 | |||
| 93 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 94 | await trigger_cog.delete.callback(trigger_cog, ctx, name="test_trigger") | ||
| 95 | mock_respond.assert_called_once_with("trigger deleted") | ||
| 96 | |||
| 97 | |||
| 98 | @pytest.mark.asyncio | ||
| 99 | async def test_trigger_help_command(trigger_cog, mock_channel, mock_guild, bot): | ||
| 100 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 101 | |||
| 102 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 103 | await trigger_cog.help.callback(trigger_cog, ctx) | ||
| 104 | mock_respond.assert_called_once() | ||
| 105 | args = mock_respond.call_args | ||
| 106 | assert "Trigger create" in str(args) | ||
diff --git a/tests/cogs/test_tts.py b/tests/cogs/test_tts.py new file mode 100644 index 0000000..de68580 --- /dev/null +++ b/tests/cogs/test_tts.py | |||
| @@ -0,0 +1,58 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import discord | ||
| 4 | from tests.conftest import MockApplicationContext | ||
| 5 | |||
| 6 | |||
| 7 | @pytest.fixture | ||
| 8 | def tts_cog(bot): | ||
| 9 | from cogs.tts import TTS | ||
| 10 | cog = TTS(bot) | ||
| 11 | return cog | ||
| 12 | |||
| 13 | |||
| 14 | @pytest.mark.asyncio | ||
| 15 | async def test_tts_file_output(tts_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 16 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 17 | ctx.user = mock_user | ||
| 18 | ctx.author = mock_user | ||
| 19 | |||
| 20 | "/tmp/test_tts.mp3" | ||
| 21 | |||
| 22 | with patch('cogs.tts.gTTS') as _: | ||
| 23 | with patch('os.path.exists', return_value=True): | ||
| 24 | with patch('os.unlink'): | ||
| 25 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 26 | with patch('cogs.tts.get_volume', return_value=1.0): | ||
| 27 | await tts_cog.tts.callback(tts_cog, ctx, text="Hello", output="file") | ||
| 28 | mock_respond.assert_called_once() | ||
| 29 | |||
| 30 | |||
| 31 | @pytest.mark.asyncio | ||
| 32 | async def test_tts_voice_output_not_in_channel(tts_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 33 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 34 | ctx.user = mock_user | ||
| 35 | ctx.author = mock_user | ||
| 36 | ctx.author.voice = None | ||
| 37 | |||
| 38 | with patch('cogs.tts.gTTS'): | ||
| 39 | with patch('os.path.exists', return_value=True): | ||
| 40 | with patch('os.unlink'): | ||
| 41 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 42 | with patch('cogs.tts.get_volume', return_value=1.0): | ||
| 43 | await tts_cog.tts.callback(tts_cog, ctx, text="Hello", output="vc") | ||
| 44 | |||
| 45 | |||
| 46 | @pytest.mark.asyncio | ||
| 47 | async def test_tts_error_handling(tts_cog, mock_channel, mock_guild, bot, mock_user): | ||
| 48 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 49 | ctx.user = mock_user | ||
| 50 | ctx.author = mock_user | ||
| 51 | |||
| 52 | with patch('cogs.tts.gTTS', side_effect=Exception("TTS Error")): | ||
| 53 | with patch('os.path.exists', return_value=False): | ||
| 54 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 55 | await tts_cog.tts.callback(tts_cog, ctx, text="Hello", output="file") | ||
| 56 | mock_respond.assert_called_once() | ||
| 57 | args = mock_respond.call_args | ||
| 58 | assert "Error" in str(args) or "eror" in str(args) | ||
diff --git a/tests/cogs/test_vclogs.py b/tests/cogs/test_vclogs.py new file mode 100644 index 0000000..2003478 --- /dev/null +++ b/tests/cogs/test_vclogs.py | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import json | ||
| 4 | from tests.conftest import MockApplicationContext | ||
| 5 | |||
| 6 | |||
| 7 | def test_format_duration(): | ||
| 8 | from cogs.vclogs import format_duration | ||
| 9 | |||
| 10 | assert format_duration(30) == "30s" | ||
| 11 | assert format_duration(90) == "1m 30s" | ||
| 12 | assert format_duration(3661) == "1h 1m 1s" | ||
| 13 | |||
| 14 | |||
| 15 | def test_load_activity_data(): | ||
| 16 | with patch('builtins.open', MagicMock()): | ||
| 17 | with patch('os.path.exists', return_value=True): | ||
| 18 | with patch('json.load', return_value={"123": {"active": True}}): | ||
| 19 | from cogs.vclogs import load_activity_data | ||
| 20 | data = load_activity_data() | ||
| 21 | assert isinstance(data, dict) | ||
| 22 | |||
| 23 | |||
| 24 | def test_save_activity_data(): | ||
| 25 | with patch('builtins.open', MagicMock()) as mock_open: | ||
| 26 | with patch('json.dump'): | ||
| 27 | from cogs.vclogs import save_activity_data | ||
| 28 | save_activity_data({"123": {"active": True}}) | ||
| 29 | mock_open.assert_called_once() | ||
| 30 | |||
| 31 | |||
| 32 | @pytest.fixture | ||
| 33 | def vclogs_cog(bot): | ||
| 34 | with patch('cogs.vclogs.get_voice_manager'): | ||
| 35 | with patch('cogs.vclogs.get_volume', return_value=1.0): | ||
| 36 | with patch('cogs.vclogs.save_settings'): | ||
| 37 | from cogs.vclogs import VcLogs | ||
| 38 | cog = VcLogs(bot) | ||
| 39 | return cog | ||
| 40 | |||
| 41 | |||
| 42 | @pytest.mark.asyncio | ||
| 43 | async def test_handle_state_change_deaf_true(vclogs_cog): | ||
| 44 | mock_member = MagicMock() | ||
| 45 | mock_member.display_name = "TestUser" | ||
| 46 | mock_channel = MagicMock() | ||
| 47 | |||
| 48 | tts_text, channel = await vclogs_cog.handle_state_change(mock_member, mock_channel, 'deaf', True, None) | ||
| 49 | |||
| 50 | assert "jest bity przez ojca" in tts_text | ||
| 51 | |||
| 52 | |||
| 53 | @pytest.mark.asyncio | ||
| 54 | async def test_handle_state_change_deaf_false(vclogs_cog): | ||
| 55 | mock_member = MagicMock() | ||
| 56 | mock_member.display_name = "TestUser" | ||
| 57 | mock_channel = MagicMock() | ||
| 58 | |||
| 59 | tts_text, channel = await vclogs_cog.handle_state_change(mock_member, mock_channel, 'deaf', False, None) | ||
| 60 | |||
| 61 | assert "uciek od ojca" in tts_text | ||
| 62 | |||
| 63 | |||
| 64 | @pytest.mark.asyncio | ||
| 65 | async def test_handle_state_change_mute_true(vclogs_cog): | ||
| 66 | mock_member = MagicMock() | ||
| 67 | mock_member.display_name = "TestUser" | ||
| 68 | mock_channel = MagicMock() | ||
| 69 | |||
| 70 | tts_text, channel = await vclogs_cog.handle_state_change(mock_member, mock_channel, 'mute', True, None) | ||
| 71 | |||
| 72 | assert "idzie robić jajecznice" in tts_text | ||
| 73 | |||
| 74 | |||
| 75 | @pytest.mark.asyncio | ||
| 76 | async def test_handle_state_change_mute_false(vclogs_cog): | ||
| 77 | mock_member = MagicMock() | ||
| 78 | mock_member.display_name = "TestUser" | ||
| 79 | mock_channel = MagicMock() | ||
| 80 | |||
| 81 | tts_text, channel = await vclogs_cog.handle_state_change(mock_member, mock_channel, 'mute', False, None) | ||
| 82 | |||
| 83 | assert "zrobił jajecznice" in tts_text | ||
diff --git a/tests/cogs/test_voice.py b/tests/cogs/test_voice.py new file mode 100644 index 0000000..ade29cb --- /dev/null +++ b/tests/cogs/test_voice.py | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import asyncio | ||
| 4 | from tests.conftest import MockApplicationContext | ||
| 5 | |||
| 6 | |||
| 7 | @pytest.mark.asyncio | ||
| 8 | async def test_get_voice_manager(): | ||
| 9 | from cogs.voice import get_voice_manager, VoiceManager | ||
| 10 | |||
| 11 | mock_bot = MagicMock() | ||
| 12 | mock_bot.user = MagicMock() | ||
| 13 | mock_bot.user.id = 12345 | ||
| 14 | |||
| 15 | manager1 = get_voice_manager(mock_bot) | ||
| 16 | manager2 = get_voice_manager(mock_bot) | ||
| 17 | |||
| 18 | assert isinstance(manager1, VoiceManager) | ||
| 19 | assert manager1 is manager2 | ||
| 20 | |||
| 21 | |||
| 22 | @pytest.mark.asyncio | ||
| 23 | async def test_voice_manager_join_channel_not_found(): | ||
| 24 | from cogs.voice import VoiceManager | ||
| 25 | |||
| 26 | mock_bot = MagicMock() | ||
| 27 | mock_bot.get_channel.return_value = None | ||
| 28 | |||
| 29 | manager = VoiceManager(mock_bot) | ||
| 30 | result = await manager.join_by_channel_id(999) | ||
| 31 | |||
| 32 | assert result is None | ||
| 33 | |||
| 34 | |||
| 35 | @pytest.mark.asyncio | ||
| 36 | async def test_voice_manager_join_invalid_channel(): | ||
| 37 | from cogs.voice import VoiceManager | ||
| 38 | |||
| 39 | mock_bot = MagicMock() | ||
| 40 | mock_channel = MagicMock() | ||
| 41 | mock_bot.get_channel.return_value = mock_channel | ||
| 42 | |||
| 43 | manager = VoiceManager(mock_bot) | ||
| 44 | result = await manager.join_by_channel_id(999) | ||
| 45 | |||
| 46 | assert result is None | ||
| 47 | |||
| 48 | |||
| 49 | @pytest.mark.asyncio | ||
| 50 | async def test_voice_manager_get_voice_status_not_connected(): | ||
| 51 | from cogs.voice import VoiceManager | ||
| 52 | |||
| 53 | mock_bot = MagicMock() | ||
| 54 | mock_guild = MagicMock() | ||
| 55 | |||
| 56 | with patch('cogs.voice.VoiceManager.get_voice_client', return_value=None): | ||
| 57 | manager = VoiceManager(mock_bot) | ||
| 58 | voice_client, is_connected, is_playing = await manager.get_voice_status(mock_guild) | ||
| 59 | |||
| 60 | assert voice_client is None | ||
| 61 | assert is_connected == False | ||
| 62 | assert is_playing == False | ||
| 63 | |||
| 64 | |||
| 65 | @pytest.mark.asyncio | ||
| 66 | async def test_voice_manager_stop(): | ||
| 67 | from cogs.voice import VoiceManager | ||
| 68 | |||
| 69 | mock_bot = MagicMock() | ||
| 70 | mock_guild = MagicMock() | ||
| 71 | mock_voice_client = MagicMock() | ||
| 72 | mock_voice_client.disconnect = AsyncMock() | ||
| 73 | |||
| 74 | with patch('cogs.voice.VoiceManager.get_voice_client', return_value=mock_voice_client): | ||
| 75 | manager = VoiceManager(mock_bot) | ||
| 76 | await manager.stop(mock_guild) | ||
| 77 | |||
| 78 | mock_voice_client.stop.assert_called_once() | ||
| 79 | mock_voice_client.disconnect.assert_called_once() | ||
diff --git a/tests/cogs/test_web.py b/tests/cogs/test_web.py new file mode 100644 index 0000000..e0d2208 --- /dev/null +++ b/tests/cogs/test_web.py | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | from cogs.web import Web | ||
| 4 | from tests.conftest import MockApplicationContext | ||
| 5 | |||
| 6 | |||
| 7 | @pytest.fixture | ||
| 8 | def web_cog(bot): | ||
| 9 | cog = Web(bot) | ||
| 10 | return cog | ||
| 11 | |||
| 12 | |||
| 13 | @pytest.mark.asyncio | ||
| 14 | async def test_web_command_success(web_cog, mock_channel, mock_guild, bot): | ||
| 15 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 16 | |||
| 17 | mock_response = MagicMock() | ||
| 18 | mock_response.status_code = 200 | ||
| 19 | mock_response.text = ''' | ||
| 20 | <html> | ||
| 21 | <a class="result__a" href="https://example.com">Test Result</a> | ||
| 22 | <a class="result__a" href="https://example.com/2">Test Result 2</a> | ||
| 23 | </html> | ||
| 24 | ''' | ||
| 25 | |||
| 26 | with patch('cogs.web.requests.get', return_value=mock_response) as mock_get: | ||
| 27 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 28 | await web_cog.web.callback(web_cog, ctx, query="test query") | ||
| 29 | mock_get.assert_called_once() | ||
| 30 | mock_respond.assert_called_once() | ||
| 31 | |||
| 32 | |||
| 33 | @pytest.mark.asyncio | ||
| 34 | async def test_web_command_no_results(web_cog, mock_channel, mock_guild, bot): | ||
| 35 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 36 | |||
| 37 | mock_response = MagicMock() | ||
| 38 | mock_response.status_code = 200 | ||
| 39 | mock_response.text = '<html><body></body></html>' | ||
| 40 | |||
| 41 | with patch('cogs.web.requests.get', return_value=mock_response): | ||
| 42 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 43 | await web_cog.web.callback(web_cog, ctx, query="test") | ||
| 44 | mock_respond.assert_called_once_with(content="No results found.", ephemeral=True) | ||
| 45 | |||
| 46 | |||
| 47 | @pytest.mark.asyncio | ||
| 48 | async def test_web_command_http_error(web_cog, mock_channel, mock_guild, bot): | ||
| 49 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 50 | |||
| 51 | mock_response = MagicMock() | ||
| 52 | mock_response.status_code = 500 | ||
| 53 | |||
| 54 | with patch('cogs.web.requests.get', return_value=mock_response): | ||
| 55 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 56 | await web_cog.web.callback(web_cog, ctx, query="test") | ||
| 57 | mock_respond.assert_called_once_with(content="Error: 500", ephemeral=True) | ||
