diff options
Diffstat (limited to '')
| -rw-r--r-- | pytest.ini | 7 | ||||
| -rw-r--r-- | requirements-dev.txt | 59 | ||||
| -rw-r--r-- | tests/README.md | 84 | ||||
| -rw-r--r-- | tests/cogs/test_ai.py | 69 | ||||
| -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_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_join.py | 53 | ||||
| -rw-r--r-- | tests/cogs/test_music_play.py | 104 | ||||
| -rw-r--r-- | tests/cogs/test_plot.py | 85 | ||||
| -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 | ||||
| -rw-r--r-- | tests/conftest.py | 198 |
21 files changed, 1684 insertions, 0 deletions
diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..03a070f --- /dev/null +++ b/pytest.ini | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | [pytest] | ||
| 2 | asyncio_default_fixture_loop_scope = function | ||
| 3 | python_files = test_*.py | ||
| 4 | python_classes = Test* | ||
| 5 | python_functions = test_* | ||
| 6 | testpaths = tests | ||
| 7 | pythonpath = . \ No newline at end of file | ||
diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..abba086 --- /dev/null +++ b/requirements-dev.txt | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | pytest==8.3.4 | ||
| 2 | pytest-asyncio==0.25.2 | ||
| 3 | pytest-mock==3.14.0 | ||
| 4 | aiofiles==25.1.0 | ||
| 5 | aiohappyeyeballs==2.6.1 | ||
| 6 | aiohttp==3.13.3 | ||
| 7 | aiosignal==1.4.0 | ||
| 8 | anyio==4.12.1 | ||
| 9 | attrs==25.4.0 | ||
| 10 | basedpyright==1.39.2 | ||
| 11 | beautifulsoup4==4.14.3 | ||
| 12 | brotli==1.2.0 | ||
| 13 | bs4==0.0.2 | ||
| 14 | certifi==2026.2.25 | ||
| 15 | cffi==2.0.0 | ||
| 16 | charset-normalizer==3.4.6 | ||
| 17 | click==8.1.8 | ||
| 18 | contourpy==1.3.3 | ||
| 19 | croniter==6.2.2 | ||
| 20 | cycler==0.12.1 | ||
| 21 | davey==0.1.4 | ||
| 22 | dotenv==0.9.9 | ||
| 23 | fonttools==4.62.1 | ||
| 24 | frozenlist==1.8.0 | ||
| 25 | gTTS==2.5.4 | ||
| 26 | h11==0.16.0 | ||
| 27 | httpcore==1.0.9 | ||
| 28 | httpx==0.28.1 | ||
| 29 | idna==3.11 | ||
| 30 | kiwisolver==1.5.0 | ||
| 31 | matplotlib==3.10.8 | ||
| 32 | multidict==6.7.1 | ||
| 33 | mutagen==1.47.0 | ||
| 34 | nodejs-wheel-binaries==24.14.1 | ||
| 35 | numpy==2.4.4 | ||
| 36 | packaging==26.1 | ||
| 37 | pillow==12.2.0 | ||
| 38 | propcache==0.4.1 | ||
| 39 | py-cord @ file:///home/salmon/Documents/botcode/senator-sperma/pycord | ||
| 40 | pycparser==3.0 | ||
| 41 | pycryptodomex==3.23.0 | ||
| 42 | PyNaCl==1.6.2 | ||
| 43 | pyparsing==3.3.2 | ||
| 44 | python-dateutil==2.9.0.post0 | ||
| 45 | python-dotenv==1.2.2 | ||
| 46 | pytube==15.0.0 | ||
| 47 | pyzbar==0.1.9 | ||
| 48 | qrcode==8.2 | ||
| 49 | RapidFuzz==3.14.3 | ||
| 50 | requests==2.32.5 | ||
| 51 | six==1.17.0 | ||
| 52 | soupsieve==2.8.3 | ||
| 53 | typing_extensions==4.15.0 | ||
| 54 | urllib3==2.6.3 | ||
| 55 | websockets==16.0 | ||
| 56 | yarl==1.23.0 | ||
| 57 | youtube-search-python==1.6.6 | ||
| 58 | yt-dlp==2026.3.17 | ||
| 59 | yt-dlp-ejs==0.8.0 | ||
diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..77b832c --- /dev/null +++ b/tests/README.md | |||
| @@ -0,0 +1,84 @@ | |||
| 1 | # Test Suite Coverage | ||
| 2 | |||
| 3 | This document tracks the test coverage for Senator Sperma bot. | ||
| 4 | |||
| 5 | ## Running Tests | ||
| 6 | |||
| 7 | ```bash | ||
| 8 | pytest tests/ -v | ||
| 9 | ``` | ||
| 10 | |||
| 11 | ## Coverage by Cog | ||
| 12 | |||
| 13 | | Cog | Test File | Tests | Coverage | | ||
| 14 | |-----|-----------|-------|----------| | ||
| 15 | | **say.py** | test_say.py | 3 | Basic command execution, empty text, long text | | ||
| 16 | | **web.py** | test_web.py | 3 | Success, no results, HTTP error | | ||
| 17 | | **qr.py** | test_qr.py | 4 | Generate, empty text, decode unavailable, download failure | | ||
| 18 | | **math_commands.py** | test_math_commands.py | 3 | Valid expression, invalid expression, process error | | ||
| 19 | | **ai.py** | test_ai.py | 3 | Success, empty choices, no choices key | | ||
| 20 | | **downloader.py** | test_downloader.py | 5 | File success, URL success, error, message with/without URL | | ||
| 21 | | **search.py** | test_search.py | 5 | Success, no cache, file not found, empty filename, autocomplete | | ||
| 22 | | **tts.py** | test_tts.py | 3 | File output, voice output not in channel, error handling | | ||
| 23 | | **trigger.py** | test_trigger.py | 5 | Create, add, add not found, delete, help | | ||
| 24 | | **compression.py** | test_compression_conversion.py | 4 | Filetype detection, get compression commands | | ||
| 25 | | **conversion.py** | test_compression_conversion.py | 5 | Build conversion commands, cleanup | | ||
| 26 | | **plot.py** | test_plot.py | 6 | Parse expression, is implicit, parse implicit, command execution | | ||
| 27 | | **vclogs.py** | test_vclogs.py | 7 | Format duration, load/save activity, state change handlers | | ||
| 28 | | **voice.py** | test_voice.py | 5 | Get manager, join channel, get status, stop | | ||
| 29 | | **music/join.py** | test_music_join.py | 1 | Join command success | | ||
| 30 | | **music/stop.py** | test_music_join.py | 1 | Stop command success | | ||
| 31 | | **music/play_commands.py** | test_music_play.py | 4 | Search embed, validation errors | | ||
| 32 | | **music/loop.py** | test_music_play.py | 2 | Is looping, toggle | | ||
| 33 | | **music/audio_playback.py** | test_music_audio.py | 8 | Validate voice, prepare source, cleanup | | ||
| 34 | | **assistant.py** | test_assistant.py | 4 | Keyword, on_message filtering, mentions | | ||
| 35 | |||
| 36 | ## Test Structure | ||
| 37 | |||
| 38 | ``` | ||
| 39 | tests/ | ||
| 40 | ├── conftest.py # Shared fixtures and mock classes | ||
| 41 | ├── cogs/ | ||
| 42 | │ ├── test_say.py | ||
| 43 | │ ├── test_web.py | ||
| 44 | │ ├── test_qr.py | ||
| 45 | │ ├── test_math_commands.py | ||
| 46 | │ ├── test_ai.py | ||
| 47 | │ ├── test_downloader.py | ||
| 48 | │ ├── test_search.py | ||
| 49 | │ ├── test_tts.py | ||
| 50 | │ ├── test_trigger.py | ||
| 51 | │ ├── test_compression_conversion.py | ||
| 52 | │ ├── test_plot.py | ||
| 53 | │ ├── test_vclogs.py | ||
| 54 | │ ├── test_voice.py | ||
| 55 | │ ├── test_music_join.py | ||
| 56 | │ ├── test_music_play.py | ||
| 57 | │ ├── test_music_audio.py | ||
| 58 | │ └── test_assistant.py | ||
| 59 | └── README.md # This file | ||
| 60 | ``` | ||
| 61 | |||
| 62 | ## Mock Classes | ||
| 63 | |||
| 64 | The test suite uses custom mock classes defined in `conftest.py`: | ||
| 65 | |||
| 66 | - `MockUser` - Simulates Discord user | ||
| 67 | - `MockChannel` / `MockTextChannel` / `MockVoiceChannel` - Simulates Discord channels | ||
| 68 | - `MockGuild` - Simulates Discord guild | ||
| 69 | - `MockMessage` - Simulates Discord message | ||
| 70 | - `MockInteraction` - Simulates Discord interaction | ||
| 71 | - `MockApplicationContext` - Simulates slash command context | ||
| 72 | |||
| 73 | ## Test Categories | ||
| 74 | |||
| 75 | 1. **Unit tests** - Test individual functions and methods | ||
| 76 | 2. **Integration tests** - Test command execution with mocked dependencies | ||
| 77 | 3. **Error handling tests** - Test error conditions and edge cases | ||
| 78 | |||
| 79 | ## Notes | ||
| 80 | |||
| 81 | - External services (OpenAI, yt-dlp, ffmpeg) are mocked | ||
| 82 | - Database operations in trigger.py are mocked | ||
| 83 | - File system operations are mocked where needed | ||
| 84 | - Some tests may require specific environment variables to be set (mocked) \ No newline at end of file | ||
diff --git a/tests/cogs/test_ai.py b/tests/cogs/test_ai.py new file mode 100644 index 0000000..bd8f813 --- /dev/null +++ b/tests/cogs/test_ai.py | |||
| @@ -0,0 +1,69 @@ | |||
| 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 ai_cog(bot): | ||
| 9 | from cogs.ai import AI | ||
| 10 | cog = AI(bot) | ||
| 11 | return cog | ||
| 12 | |||
| 13 | |||
| 14 | @pytest.fixture | ||
| 15 | def mock_ai_response(): | ||
| 16 | return { | ||
| 17 | "choices": [ | ||
| 18 | { | ||
| 19 | "message": { | ||
| 20 | "content": "Test response from AI" | ||
| 21 | } | ||
| 22 | } | ||
| 23 | ] | ||
| 24 | } | ||
| 25 | |||
| 26 | |||
| 27 | @pytest.mark.asyncio | ||
| 28 | async def test_senator_command_success(ai_cog, mock_channel, mock_guild, bot, mock_ai_response): | ||
| 29 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 30 | |||
| 31 | mock_response = MagicMock() | ||
| 32 | mock_response.json = MagicMock(return_value=mock_ai_response) | ||
| 33 | mock_response.status_code = 200 | ||
| 34 | |||
| 35 | with patch('cogs.ai.requests.post', return_value=mock_response): | ||
| 36 | with patch.object(ctx, 'send_followup', new_callable=AsyncMock) as mock_followup: | ||
| 37 | await ai_cog.senator.callback(ai_cog, ctx, text="Hello", tts=False) | ||
| 38 | mock_followup.assert_called_once_with("Test response from AI") | ||
| 39 | |||
| 40 | |||
| 41 | @pytest.mark.asyncio | ||
| 42 | async def test_senator_command_empty_choices(ai_cog, mock_channel, mock_guild, bot): | ||
| 43 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 44 | |||
| 45 | mock_response = MagicMock() | ||
| 46 | mock_response.json = MagicMock(return_value={"choices": []}) | ||
| 47 | mock_response.status_code = 200 | ||
| 48 | |||
| 49 | with patch('cogs.ai.requests.post', return_value=mock_response): | ||
| 50 | with patch.object(ctx, 'send_followup', new_callable=AsyncMock) as mock_followup: | ||
| 51 | await ai_cog.senator.callback(ai_cog, ctx, text="Hello", tts=False) | ||
| 52 | mock_followup.assert_called_once_with("bot siem zesroł") | ||
| 53 | |||
| 54 | |||
| 55 | @pytest.mark.asyncio | ||
| 56 | async def test_senator_command_no_choices_key(ai_cog, mock_channel, mock_guild, bot): | ||
| 57 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 58 | |||
| 59 | mock_response = MagicMock() | ||
| 60 | mock_response.json = MagicMock(return_value={}) | ||
| 61 | mock_response.status_code = 200 | ||
| 62 | |||
| 63 | with patch('cogs.ai.requests.post', return_value=mock_response): | ||
| 64 | with patch('cogs.ai.logger') as mock_logger: | ||
| 65 | try: | ||
| 66 | await ai_cog.senator.callback(ai_cog, ctx, text="Hello", tts=False) | ||
| 67 | except KeyError: | ||
| 68 | pass | ||
| 69 | assert mock_logger.info.called \ No newline at end of file | ||
diff --git a/tests/cogs/test_assistant.py b/tests/cogs/test_assistant.py new file mode 100644 index 0000000..09c3934 --- /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) \ No newline at end of file | ||
diff --git a/tests/cogs/test_compression_conversion.py b/tests/cogs/test_compression_conversion.py new file mode 100644 index 0000000..f9f74ad --- /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 \ No newline at end of file | ||
diff --git a/tests/cogs/test_downloader.py b/tests/cogs/test_downloader.py new file mode 100644 index 0000000..91eef93 --- /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) \ No newline at end of file | ||
diff --git a/tests/cogs/test_math_commands.py b/tests/cogs/test_math_commands.py new file mode 100644 index 0000000..0acac4a --- /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) \ No newline at end of file | ||
diff --git a/tests/cogs/test_music_audio.py b/tests/cogs/test_music_audio.py new file mode 100644 index 0000000..b53cf95 --- /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, display_name, 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") \ No newline at end of file | ||
diff --git a/tests/cogs/test_music_join.py b/tests/cogs/test_music_join.py new file mode 100644 index 0000000..5fc66f6 --- /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() \ No newline at end of file | ||
diff --git a/tests/cogs/test_music_play.py b/tests/cogs/test_music_play.py new file mode 100644 index 0000000..abdd81c --- /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) \ No newline at end of file | ||
diff --git a/tests/cogs/test_plot.py b/tests/cogs/test_plot.py new file mode 100644 index 0000000..e38a2c7 --- /dev/null +++ b/tests/cogs/test_plot.py | |||
| @@ -0,0 +1,85 @@ | |||
| 1 | import pytest | ||
| 2 | from unittest.mock import AsyncMock, patch, MagicMock | ||
| 3 | import os | ||
| 4 | from tests.conftest import MockApplicationContext | ||
| 5 | |||
| 6 | |||
| 7 | def test_parse_expression(): | ||
| 8 | from cogs.plot import parse_expression | ||
| 9 | |||
| 10 | assert parse_expression("x^2") == "x**2" | ||
| 11 | assert parse_expression("x!") == "factorial(x)" | ||
| 12 | assert parse_expression("2x") == "2*x" | ||
| 13 | assert parse_expression("x2") == "x*2" | ||
| 14 | assert parse_expression("sin(x)") == "sin(x)" | ||
| 15 | |||
| 16 | |||
| 17 | def test_is_implicit(): | ||
| 18 | from cogs.plot import is_implicit | ||
| 19 | |||
| 20 | assert is_implicit("x^2 + y^2 = 1") == True | ||
| 21 | assert is_implicit("x^2 = y") == True | ||
| 22 | assert is_implicit("x^2 + 1") == False | ||
| 23 | |||
| 24 | |||
| 25 | def test_parse_implicit(): | ||
| 26 | from cogs.plot import parse_implicit | ||
| 27 | |||
| 28 | lhs, rhs = parse_implicit("x^2 + y = 2") | ||
| 29 | assert "x**2" in lhs | ||
| 30 | assert "+ y" in lhs or "+y" in lhs | ||
| 31 | assert rhs == "2" | ||
| 32 | |||
| 33 | |||
| 34 | @pytest.fixture | ||
| 35 | def plot_cog(bot): | ||
| 36 | from cogs.plot import Plot | ||
| 37 | cog = Plot(bot) | ||
| 38 | return cog | ||
| 39 | |||
| 40 | |||
| 41 | @pytest.mark.asyncio | ||
| 42 | async def test_plot_command_not_available(plot_cog, mock_channel, mock_guild, bot): | ||
| 43 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 44 | |||
| 45 | with patch('cogs.plot._ensure_plot', return_value=False): | ||
| 46 | with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond: | ||
| 47 | await plot_cog.plot.callback(plot_cog, ctx, funkcja="x^2") | ||
| 48 | mock_respond.assert_called_once() | ||
| 49 | args = mock_respond.call_args | ||
| 50 | assert "matplotlib" in str(args).lower() or "nie jest dostępne" in str(args) | ||
| 51 | |||
| 52 | |||
| 53 | @pytest.mark.asyncio | ||
| 54 | async def test_plot_command_success(plot_cog, mock_channel, mock_guild, bot): | ||
| 55 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 56 | |||
| 57 | with patch('cogs.plot._ensure_plot', return_value=True): | ||
| 58 | with patch('cogs.plot._plt_module') as mock_plt: | ||
| 59 | with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"): | ||
| 60 | with patch('os.path.exists', return_value=True): | ||
| 61 | with patch('os.unlink'): | ||
| 62 | with patch.object(ctx, 'followup', send=AsyncMock()) as mock_followup: | ||
| 63 | mock_fig = MagicMock() | ||
| 64 | mock_plt.subplots.return_value = (mock_fig, MagicMock()) | ||
| 65 | |||
| 66 | await plot_cog.plot.callback(plot_cog, ctx, funkcja="x^2") | ||
| 67 | mock_followup.send.assert_called_once() | ||
| 68 | |||
| 69 | |||
| 70 | @pytest.mark.asyncio | ||
| 71 | async def test_plot_command_error_handling(plot_cog, mock_channel, mock_guild, bot): | ||
| 72 | ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild) | ||
| 73 | |||
| 74 | with patch('cogs.plot._ensure_plot', return_value=True): | ||
| 75 | with patch('cogs.plot._plt_module') as mock_plt: | ||
| 76 | with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"): | ||
| 77 | with patch('os.path.exists', return_value=True): | ||
| 78 | with patch('os.unlink'): | ||
| 79 | with patch.object(ctx, 'followup', send=AsyncMock()) as mock_followup: | ||
| 80 | mock_plt.subplots.side_effect = ValueError("Test error") | ||
| 81 | |||
| 82 | await plot_cog.plot.callback(plot_cog, ctx, funkcja="invalid!!") | ||
| 83 | mock_followup.send.assert_called_once() | ||
| 84 | args = mock_followup.send.call_args | ||
| 85 | assert "Nie można" in str(args) or "Błąd" in str(args) \ No newline at end of file | ||
diff --git a/tests/cogs/test_qr.py b/tests/cogs/test_qr.py new file mode 100644 index 0000000..f3688e3 --- /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() \ No newline at end of file | ||
diff --git a/tests/cogs/test_say.py b/tests/cogs/test_say.py new file mode 100644 index 0000000..12f2d2a --- /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) \ No newline at end of file | ||
diff --git a/tests/cogs/test_search.py b/tests/cogs/test_search.py new file mode 100644 index 0000000..4fde494 --- /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 \ No newline at end of file | ||
diff --git a/tests/cogs/test_trigger.py b/tests/cogs/test_trigger.py new file mode 100644 index 0000000..befc360 --- /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) \ No newline at end of file | ||
diff --git a/tests/cogs/test_tts.py b/tests/cogs/test_tts.py new file mode 100644 index 0000000..8c95670 --- /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 | mock_temp_path = "/tmp/test_tts.mp3" | ||
| 21 | |||
| 22 | with patch('cogs.tts.gTTS') as mock_gtts: | ||
| 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) \ No newline at end of file | ||
diff --git a/tests/cogs/test_vclogs.py b/tests/cogs/test_vclogs.py new file mode 100644 index 0000000..14bc6d5 --- /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 \ No newline at end of file | ||
diff --git a/tests/cogs/test_voice.py b/tests/cogs/test_voice.py new file mode 100644 index 0000000..f5784f6 --- /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() \ No newline at end of file | ||
diff --git a/tests/cogs/test_web.py b/tests/cogs/test_web.py new file mode 100644 index 0000000..53a44e5 --- /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) \ No newline at end of file | ||
diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c471b6b --- /dev/null +++ b/tests/conftest.py | |||
| @@ -0,0 +1,198 @@ | |||
| 1 | import asyncio | ||
| 2 | from unittest.mock import AsyncMock, MagicMock | ||
| 3 | import pytest | ||
| 4 | |||
| 5 | import discord | ||
| 6 | from discord.ext import commands | ||
| 7 | |||
| 8 | |||
| 9 | class MockUser: | ||
| 10 | def __init__(self, id=12345, name="testuser", discriminator="0001"): | ||
| 11 | self.id = id | ||
| 12 | self.name = name | ||
| 13 | self.discriminator = discriminator | ||
| 14 | self.mention = f"@{name}" | ||
| 15 | self.display_name = name | ||
| 16 | self.avatar = None | ||
| 17 | self.bot = False | ||
| 18 | self.guild = None | ||
| 19 | self._state = MagicMock() | ||
| 20 | |||
| 21 | def __str__(self): | ||
| 22 | return f"{self.name}#{self.discriminator}" | ||
| 23 | |||
| 24 | |||
| 25 | class MockChannel: | ||
| 26 | def __init__(self, id=11111, name="test-channel", type=discord.ChannelType.text): | ||
| 27 | self.id = id | ||
| 28 | self.name = name | ||
| 29 | self.type = type | ||
| 30 | self.mention = f"#{name}" | ||
| 31 | self.guild = None | ||
| 32 | self.category = None | ||
| 33 | self._state = MagicMock() | ||
| 34 | |||
| 35 | async def send(self, *args, **kwargs): | ||
| 36 | return MagicMock() | ||
| 37 | |||
| 38 | |||
| 39 | class MockTextChannel(MockChannel): | ||
| 40 | def __init__(self, id=11111, name="test-channel"): | ||
| 41 | super().__init__(id, name, discord.ChannelType.text) | ||
| 42 | |||
| 43 | |||
| 44 | class MockVoiceChannel(MockChannel): | ||
| 45 | def __init__(self, id=11112, name="test-voice"): | ||
| 46 | super().__init__(id, name, discord.ChannelType.voice) | ||
| 47 | self.voice_states = {} | ||
| 48 | |||
| 49 | |||
| 50 | class MockGuild: | ||
| 51 | def __init__(self, id=99999, name="Test Server"): | ||
| 52 | self.id = id | ||
| 53 | self.name = name | ||
| 54 | self.channels = [] | ||
| 55 | self.roles = [] | ||
| 56 | self.emojis = [] | ||
| 57 | self.me = MockUser(id=111111, name="Sen Bot") | ||
| 58 | self._state = MagicMock() | ||
| 59 | self._default_role = None | ||
| 60 | |||
| 61 | def get_channel(self, channel_id): | ||
| 62 | for ch in self.channels: | ||
| 63 | if ch.id == channel_id: | ||
| 64 | return ch | ||
| 65 | return None | ||
| 66 | |||
| 67 | def get_role(self, role_id): | ||
| 68 | for role in self.roles: | ||
| 69 | if role.id == role_id: | ||
| 70 | return role | ||
| 71 | return None | ||
| 72 | |||
| 73 | def get_member(self, member_id): | ||
| 74 | return None | ||
| 75 | |||
| 76 | |||
| 77 | class MockMessage: | ||
| 78 | def __init__(self, id=22222, content="test message", author=None, channel=None, guild=None): | ||
| 79 | self.id = id | ||
| 80 | self.content = content | ||
| 81 | self.author = author or MockUser() | ||
| 82 | self.channel = channel or MockChannel() | ||
| 83 | self.guild = guild | ||
| 84 | self.attachments = [] | ||
| 85 | self.embeds = [] | ||
| 86 | self.jump_url = "https://discord.com/channels/..." | ||
| 87 | self.created_at = None | ||
| 88 | |||
| 89 | async def reply(self, *args, **kwargs): | ||
| 90 | return MagicMock() | ||
| 91 | |||
| 92 | |||
| 93 | class MockInteraction: | ||
| 94 | def __init__(self, user=None, channel=None, guild=None): | ||
| 95 | self.user = user or MockUser() | ||
| 96 | self.channel = channel or MockChannel() | ||
| 97 | self.guild = guild or MockGuild() | ||
| 98 | self.message = None | ||
| 99 | self.response = AsyncMock() | ||
| 100 | self.followup = AsyncMock() | ||
| 101 | self.client = None | ||
| 102 | self.id = 33333 | ||
| 103 | self.token = "test_token" | ||
| 104 | self._state = MagicMock() | ||
| 105 | |||
| 106 | async def defer(self, *args, **kwargs): | ||
| 107 | pass | ||
| 108 | |||
| 109 | async def respond(self, *args, **kwargs): | ||
| 110 | pass | ||
| 111 | |||
| 112 | async def delete_original_response(self, *args, **kwargs): | ||
| 113 | pass | ||
| 114 | |||
| 115 | async def send(self, *args, **kwargs): | ||
| 116 | return MagicMock() | ||
| 117 | |||
| 118 | |||
| 119 | class MockApplicationContext: | ||
| 120 | def __init__(self, bot=None, interaction=None, user=None, channel=None, guild=None): | ||
| 121 | self.bot = bot | ||
| 122 | self.interaction = interaction or MockInteraction(user=user, channel=channel, guild=guild) | ||
| 123 | self.user = user or MockUser() | ||
| 124 | self.channel = channel or MockChannel() | ||
| 125 | self.guild = guild or MockGuild() | ||
| 126 | self.message = None | ||
| 127 | self.response = AsyncMock() | ||
| 128 | self.followup = AsyncMock() | ||
| 129 | self.client = self.bot | ||
| 130 | self.id = 33333 | ||
| 131 | self.token = "test_token" | ||
| 132 | self._state = self.interaction._state | ||
| 133 | self.author = self.user | ||
| 134 | |||
| 135 | async def defer(self, *args, **kwargs): | ||
| 136 | pass | ||
| 137 | |||
| 138 | async def respond(self, *args, **kwargs): | ||
| 139 | pass | ||
| 140 | |||
| 141 | async def delete_original_response(self, *args, **kwargs): | ||
| 142 | pass | ||
| 143 | |||
| 144 | async def send(self, *args, **kwargs): | ||
| 145 | return MagicMock() | ||
| 146 | |||
| 147 | async def reply(self, *args, **kwargs): | ||
| 148 | return MagicMock() | ||
| 149 | |||
| 150 | async def send_followup(self, *args, **kwargs): | ||
| 151 | return MagicMock() | ||
| 152 | |||
| 153 | |||
| 154 | @pytest.fixture | ||
| 155 | def mock_guild(): | ||
| 156 | return MockGuild() | ||
| 157 | |||
| 158 | |||
| 159 | @pytest.fixture | ||
| 160 | def mock_channel(mock_guild): | ||
| 161 | channel = MockTextChannel() | ||
| 162 | channel.guild = mock_guild | ||
| 163 | return channel | ||
| 164 | |||
| 165 | |||
| 166 | @pytest.fixture | ||
| 167 | def mock_voice_channel(mock_guild): | ||
| 168 | channel = MockVoiceChannel() | ||
| 169 | channel.guild = mock_guild | ||
| 170 | return channel | ||
| 171 | |||
| 172 | |||
| 173 | @pytest.fixture | ||
| 174 | def mock_user(): | ||
| 175 | return MockUser() | ||
| 176 | |||
| 177 | |||
| 178 | @pytest.fixture | ||
| 179 | def mock_member(mock_guild): | ||
| 180 | member = MockUser() | ||
| 181 | member.guild = mock_guild | ||
| 182 | return member | ||
| 183 | |||
| 184 | |||
| 185 | @pytest.fixture | ||
| 186 | def mock_message(mock_channel, mock_user, mock_guild): | ||
| 187 | return MockMessage(channel=mock_channel, author=mock_user, guild=mock_guild) | ||
| 188 | |||
| 189 | |||
| 190 | @pytest.fixture | ||
| 191 | def mock_interaction(mock_channel, mock_user, mock_guild): | ||
| 192 | return MockInteraction(user=mock_user, channel=mock_channel, guild=mock_guild) | ||
| 193 | |||
| 194 | |||
| 195 | @pytest.fixture | ||
| 196 | def bot(): | ||
| 197 | intents = discord.Intents.all() | ||
| 198 | return commands.Bot(intents=intents, command_prefix="!", loop=asyncio.new_event_loop()) \ No newline at end of file | ||
