aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-04-18 16:24:42 +0200
committersalmonik <raisersoft@proton.me>2026-04-18 16:24:42 +0200
commit857a7364ddb9b10529593c5e9c3578eb95c1da9b (patch)
treecc160ed8685039fcb08663226395fb97d78fcea6 /tests
parentadd support for x^2+y^2=0,4 and alike (diff)
downloadsenator-sperma-857a7364ddb9b10529593c5e9c3578eb95c1da9b.tar.gz
senator-sperma-857a7364ddb9b10529593c5e9c3578eb95c1da9b.zip
add a test suite
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to '')
-rw-r--r--tests/README.md84
-rw-r--r--tests/cogs/test_ai.py69
-rw-r--r--tests/cogs/test_assistant.py66
-rw-r--r--tests/cogs/test_compression_conversion.py139
-rw-r--r--tests/cogs/test_downloader.py93
-rw-r--r--tests/cogs/test_math_commands.py55
-rw-r--r--tests/cogs/test_music_audio.py93
-rw-r--r--tests/cogs/test_music_join.py53
-rw-r--r--tests/cogs/test_music_play.py104
-rw-r--r--tests/cogs/test_plot.py85
-rw-r--r--tests/cogs/test_qr.py75
-rw-r--r--tests/cogs/test_say.py38
-rw-r--r--tests/cogs/test_search.py83
-rw-r--r--tests/cogs/test_trigger.py106
-rw-r--r--tests/cogs/test_tts.py58
-rw-r--r--tests/cogs/test_vclogs.py83
-rw-r--r--tests/cogs/test_voice.py79
-rw-r--r--tests/cogs/test_web.py57
-rw-r--r--tests/conftest.py198
19 files changed, 1618 insertions, 0 deletions
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
3This document tracks the test coverage for Senator Sperma bot.
4
5## Running Tests
6
7```bash
8pytest 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```
39tests/
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
64The 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
751. **Unit tests** - Test individual functions and methods
762. **Integration tests** - Test command execution with mocked dependencies
773. **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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import discord
4from tests.conftest import MockApplicationContext
5
6
7@pytest.fixture
8def ai_cog(bot):
9 from cogs.ai import AI
10 cog = AI(bot)
11 return cog
12
13
14@pytest.fixture
15def 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
28async 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
42async 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
56async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3from tests.conftest import MockApplicationContext
4
5
6@pytest.fixture
7def 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
19def test_assistant_keyword_default(assistant_cog):
20 assert assistant_cog.keyword == "test_keyword"
21
22
23@pytest.mark.asyncio
24async 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
38async 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
54async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import os
4import discord
5from tests.conftest import MockApplicationContext
6
7
8def test_compression_filetype_video():
9 from cogs.compression import filetype
10 assert filetype("test.mp4") == "Video"
11 assert filetype("test.avi") == "Video"
12
13
14def test_compression_filetype_audio():
15 from cogs.compression import filetype
16 assert filetype("test.mp3") == "Audio"
17 assert filetype("test.wav") == "Audio"
18
19
20def test_compression_filetype_image():
21 from cogs.compression import filetype
22 assert filetype("test.jpg") == "Image"
23 assert filetype("test.png") == "Image"
24
25
26def 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
35async 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
54async 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
73async 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
91async 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
109async 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
126async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import discord
4from tests.conftest import MockApplicationContext, MockMessage
5
6
7@pytest.fixture
8def downloader_cog(bot):
9 from cogs.downloader import Downloader
10 cog = Downloader(bot)
11 return cog
12
13
14@pytest.mark.asyncio
15async 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
35async 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
51async 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
66async 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
83async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3from tests.conftest import MockApplicationContext
4
5
6@pytest.fixture
7def math_cog(bot):
8 from cogs.math_commands import Math
9 cog = Math(bot)
10 return cog
11
12
13@pytest.mark.asyncio
14async 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
30async 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
42async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import os
4from tests.conftest import MockApplicationContext
5
6
7@pytest.mark.asyncio
8async 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
22async 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
37async 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
47async 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
59async 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
69async 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
80async 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
89async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3from tests.conftest import MockApplicationContext
4
5
6@pytest.fixture
7def join_cog(bot):
8 from cogs.music.join import Join
9 cog = Join(bot)
10 return cog
11
12
13@pytest.fixture
14def stop_cog(bot):
15 from cogs.music.stop import Stop
16 cog = Stop(bot)
17 return cog
18
19
20@pytest.mark.asyncio
21async 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
40async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import os
4from tests.conftest import MockApplicationContext
5
6
7@pytest.fixture
8def 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
16def loop_cog(bot):
17 from cogs.music.loop import Loop
18 cog = Loop(bot)
19 return cog
20
21
22@pytest.mark.asyncio
23async 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
31async 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
44async 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
55async 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
68def test_loop_cog_is_looping_initial(loop_cog):
69 assert loop_cog.is_looping(123) == False
70
71
72def 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
78async 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
92async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import os
4from tests.conftest import MockApplicationContext
5
6
7def 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
17def 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
25def 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
35def plot_cog(bot):
36 from cogs.plot import Plot
37 cog = Plot(bot)
38 return cog
39
40
41@pytest.mark.asyncio
42async 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
54async 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
71async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock, mock_open
3import os
4from cogs.qr import QR
5from tests.conftest import MockApplicationContext
6
7
8@pytest.fixture
9def qr_cog(bot):
10 cog = QR(bot)
11 return cog
12
13
14@pytest.fixture
15def 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
24async 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
37async 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
48async 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
63async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch
3from cogs.say import Say
4from tests.conftest import MockApplicationContext
5
6
7@pytest.fixture
8def say_cog(bot):
9 cog = Say(bot)
10 return cog
11
12
13@pytest.mark.asyncio
14async 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
23async 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
32async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import discord
4from tests.conftest import MockApplicationContext
5
6
7@pytest.fixture
8def search_cog(bot):
9 from cogs.search import Search
10 cog = Search(bot)
11 return cog
12
13
14@pytest.mark.asyncio
15async 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
31async 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
42async 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
57async 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
68async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import sqlite3
4from tests.conftest import MockApplicationContext
5
6
7@pytest.fixture
8def 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
18def 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
29async 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
48async 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
65async 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
82async 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
99async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import discord
4from tests.conftest import MockApplicationContext
5
6
7@pytest.fixture
8def tts_cog(bot):
9 from cogs.tts import TTS
10 cog = TTS(bot)
11 return cog
12
13
14@pytest.mark.asyncio
15async 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
32async 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
47async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import json
4from tests.conftest import MockApplicationContext
5
6
7def 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
15def 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
24def 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
33def 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
43async 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
54async 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
65async 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
76async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import asyncio
4from tests.conftest import MockApplicationContext
5
6
7@pytest.mark.asyncio
8async 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
23async 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
36async 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
50async 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
66async 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 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3from cogs.web import Web
4from tests.conftest import MockApplicationContext
5
6
7@pytest.fixture
8def web_cog(bot):
9 cog = Web(bot)
10 return cog
11
12
13@pytest.mark.asyncio
14async 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
34async 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
48async 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 @@
1import asyncio
2from unittest.mock import AsyncMock, MagicMock
3import pytest
4
5import discord
6from discord.ext import commands
7
8
9class 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
25class 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
39class MockTextChannel(MockChannel):
40 def __init__(self, id=11111, name="test-channel"):
41 super().__init__(id, name, discord.ChannelType.text)
42
43
44class 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
50class 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
77class 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
93class 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
119class 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
155def mock_guild():
156 return MockGuild()
157
158
159@pytest.fixture
160def mock_channel(mock_guild):
161 channel = MockTextChannel()
162 channel.guild = mock_guild
163 return channel
164
165
166@pytest.fixture
167def mock_voice_channel(mock_guild):
168 channel = MockVoiceChannel()
169 channel.guild = mock_guild
170 return channel
171
172
173@pytest.fixture
174def mock_user():
175 return MockUser()
176
177
178@pytest.fixture
179def mock_member(mock_guild):
180 member = MockUser()
181 member.guild = mock_guild
182 return member
183
184
185@pytest.fixture
186def 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
191def 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
196def 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