aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorsalmonik <salmonik@noreply.codeberg.org>2026-04-19 18:10:09 +0200
committersalmonik <salmonik@noreply.codeberg.org>2026-04-19 18:10:09 +0200
commitf8e1cf36e1efa0602bf2e450835f2a67654762a6 (patch)
treefe6b12226d7eebe2a0dc6525de3414d0c3718cc8 /tests
parentadd support for x^2+y^2=0,4 and alike (diff)
parentfix: proper aiohttp async context manager mocking in test (diff)
downloadsenator-sperma-f8e1cf36e1efa0602bf2e450835f2a67654762a6.tar.gz
senator-sperma-f8e1cf36e1efa0602bf2e450835f2a67654762a6.zip
Merge pull request 'massive changes' (#3) from dev into main
Reviewed-on: https://codeberg.org/salmonik/senator-sperma/pulls/3 stable enough
Diffstat (limited to '')
-rw-r--r--tests/cogs/test_ai.py83
-rw-r--r--tests/cogs/test_assistant.py66
-rw-r--r--tests/cogs/test_compression_conversion.py139
-rw-r--r--tests/cogs/test_cron.py166
-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_external_play.py116
-rw-r--r--tests/cogs/test_music_join.py53
-rw-r--r--tests/cogs/test_music_play.py104
-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.py206
19 files changed, 1753 insertions, 0 deletions
diff --git a/tests/cogs/test_ai.py b/tests/cogs/test_ai.py
new file mode 100644
index 0000000..a1c3fd4
--- /dev/null
+++ b/tests/cogs/test_ai.py
@@ -0,0 +1,83 @@
1import pytest
2from unittest.mock import AsyncMock, patch, MagicMock
3import discord
4import httpx
5from tests.conftest import MockApplicationContext
6
7
8@pytest.fixture
9def ai_cog(bot):
10 from cogs.ai import AI
11 cog = AI(bot)
12 return cog
13
14
15@pytest.fixture
16def mock_ai_response():
17 return {
18 "choices": [
19 {
20 "message": {
21 "content": "Test response from AI"
22 }
23 }
24 ]
25 }
26
27
28@pytest.mark.asyncio
29async def test_senator_command_success(ai_cog, mock_channel, mock_guild, bot, mock_ai_response):
30 ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild)
31
32 mock_response = MagicMock()
33 mock_response.json = MagicMock(return_value=mock_ai_response)
34
35 mock_client = MagicMock()
36 mock_client.post = AsyncMock(return_value=mock_response)
37 mock_client.__aenter__ = AsyncMock(return_value=mock_response)
38 mock_client.__aexit__ = AsyncMock(return_value=None)
39
40 with patch('cogs.ai.httpx.AsyncClient') as mock_httpx:
41 instance = mock_httpx.return_value.__aenter__.return_value
42 instance.post = AsyncMock(return_value=mock_response)
43 with patch.object(ctx, 'send_followup', new_callable=AsyncMock) as mock_followup:
44 await ai_cog.senator.callback(ai_cog, ctx, text="Hello", tts=False)
45 mock_followup.assert_called_once_with("Test response from AI")
46
47
48@pytest.mark.asyncio
49async def test_senator_command_empty_choices(ai_cog, mock_channel, mock_guild, bot):
50 ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild)
51
52 mock_response = MagicMock()
53 mock_response.json = MagicMock(return_value={"choices": []})
54
55 mock_client = MagicMock()
56 mock_client.post = AsyncMock(return_value=mock_response)
57 mock_client.__aenter__ = AsyncMock(return_value=mock_response)
58 mock_client.__aexit__ = AsyncMock(return_value=None)
59
60 with patch('cogs.ai.httpx.AsyncClient') as mock_httpx:
61 instance = mock_httpx.return_value.__aenter__.return_value
62 instance.post = AsyncMock(return_value=mock_response)
63 with patch.object(ctx, 'send_followup', new_callable=AsyncMock) as mock_followup:
64 await ai_cog.senator.callback(ai_cog, ctx, text="Hello", tts=False)
65 mock_followup.assert_called_once_with("bot siem zesroĊ‚")
66
67
68@pytest.mark.asyncio
69async def test_senator_command_no_choices_key(ai_cog, mock_channel, mock_guild, bot):
70 ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild)
71
72 mock_response = MagicMock()
73 mock_response.json = MagicMock(return_value={})
74
75 with patch('cogs.ai.httpx.AsyncClient') as mock_httpx:
76 instance = mock_httpx.return_value.__aenter__.return_value
77 instance.post = AsyncMock(return_value=mock_response)
78 with patch('cogs.ai.logger') as mock_logger:
79 try:
80 await ai_cog.senator.callback(ai_cog, ctx, text="Hello", tts=False)
81 except KeyError:
82 pass
83 assert mock_logger.info.called
diff --git a/tests/cogs/test_assistant.py b/tests/cogs/test_assistant.py
new file mode 100644
index 0000000..9a33232
--- /dev/null
+++ b/tests/cogs/test_assistant.py
@@ -0,0 +1,66 @@
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)
diff --git a/tests/cogs/test_compression_conversion.py b/tests/cogs/test_compression_conversion.py
new file mode 100644
index 0000000..f9d3427
--- /dev/null
+++ b/tests/cogs/test_compression_conversion.py
@@ -0,0 +1,139 @@
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
diff --git a/tests/cogs/test_cron.py b/tests/cogs/test_cron.py
new file mode 100644
index 0000000..93031f8
--- /dev/null
+++ b/tests/cogs/test_cron.py
@@ -0,0 +1,166 @@
1import json
2import os
3import pytest
4from datetime import datetime
5from unittest.mock import MagicMock, patch
6
7from cogs.cron import Cron
8
9
10@pytest.fixture
11def mock_bot():
12 bot = MagicMock()
13 bot.loop = MagicMock()
14 bot.is_closed = MagicMock(return_value=False)
15 bot.wait_until_ready = MagicMock()
16 bot.loop.create_task = MagicMock(return_value=MagicMock())
17 return bot
18
19
20def create_cog(mock_bot, tmp_path):
21 with patch.object(Cron, "__init__", lambda self, bot: None):
22 cog = Cron.__new__(Cron)
23 cog.bot = mock_bot
24 cog.scheduled_tasks = []
25 cog.cron_file = str(tmp_path / "cron_jobs.json")
26 cog.task = MagicMock()
27 return cog
28
29
30class TestLoadTasks:
31 def test_load_tasks_file_not_exists(self, mock_bot, tmp_path):
32 cog = create_cog(mock_bot, tmp_path)
33 cog.cron_file = str(tmp_path / "nonexistent.json")
34 cog.load_tasks()
35 assert cog.scheduled_tasks == []
36
37 def test_load_tasks_empty_file(self, mock_bot, tmp_path):
38 cron_file = tmp_path / "empty.json"
39 cron_file.write_text("")
40 cog = create_cog(mock_bot, tmp_path)
41 cog.cron_file = str(cron_file)
42 cog.load_tasks()
43 assert cog.scheduled_tasks == []
44
45 def test_load_tasks_valid_file(self, mock_bot, tmp_path):
46 cron_file = tmp_path / "valid.json"
47 tasks = [{"id": 1, "cron_expression": "* * * * *", "type": "audio", "channel_id": 123, "active": True}]
48 cron_file.write_text(json.dumps(tasks))
49 cog = create_cog(mock_bot, tmp_path)
50 cog.cron_file = str(cron_file)
51 cog.load_tasks()
52 assert len(cog.scheduled_tasks) == 1
53 assert cog.scheduled_tasks[0]["id"] == 1
54
55 def test_load_tasks_io_error(self, mock_bot, tmp_path):
56 cog = create_cog(mock_bot, tmp_path)
57 with patch("builtins.open", side_effect=IOError("test error")):
58 cog.load_tasks()
59 assert cog.scheduled_tasks == []
60
61
62class TestSaveTasks:
63 def test_save_tasks_with_real_file(self, mock_bot, tmp_path):
64 cron_file = tmp_path / "cron_jobs.json"
65 cog = create_cog(mock_bot, tmp_path)
66 cog.cron_file = str(cron_file)
67 cog.scheduled_tasks = [{"id": 1, "cron_expression": "* * * * *"}]
68 cog.save_tasks()
69 assert os.path.exists(cron_file)
70 with open(cron_file) as f:
71 loaded = json.load(f)
72 assert len(loaded) == 1
73
74 def test_save_tasks_creates_directory_structure(self, mock_bot, tmp_path):
75 cog = create_cog(mock_bot, tmp_path)
76 new_dir = tmp_path / "new_dir"
77 cron_file = new_dir / "cron_jobs.json"
78 cog.cron_file = str(cron_file)
79 cog.scheduled_tasks = [{"id": 1}]
80
81 cog.save_tasks()
82
83 assert os.path.exists(new_dir)
84
85
86class TestShouldRunTask:
87 def test_should_run_task_invalid_cron(self, mock_bot, tmp_path):
88 cog = create_cog(mock_bot, tmp_path)
89 task = {"cron_expression": "invalid"}
90 now = datetime.now()
91 result = cog.should_run_task(task, now)
92 assert result is False
93
94 def test_should_run_task_missing_cron_expression(self, mock_bot, tmp_path):
95 cog = create_cog(mock_bot, tmp_path)
96 task = {}
97 now = datetime.now()
98 result = cog.should_run_task(task, now)
99 assert result is False
100
101
102class TestCogUnload:
103 def test_cog_unload_cancels_task(self, mock_bot, tmp_path):
104 with patch.object(Cron, "__init__", lambda self, bot: None):
105 cog = Cron.__new__(Cron)
106 cog.bot = mock_bot
107 cog.task = MagicMock()
108
109 cog.cog_unload()
110 cog.task.cancel.assert_called_once()
111
112
113class TestTaskManagement:
114 def test_add_task_increments_id(self, mock_bot, tmp_path):
115 cog = create_cog(mock_bot, tmp_path)
116 cog.scheduled_tasks = [
117 {"id": 1, "cron_expression": "* * * * *"},
118 {"id": 2, "cron_expression": "0 * * * *"},
119 ]
120
121 new_id = max([t["id"] for t in cog.scheduled_tasks], default=0) + 1
122 assert new_id == 3
123
124 def test_find_task_by_id(self, mock_bot, tmp_path):
125 cog = create_cog(mock_bot, tmp_path)
126 cog.scheduled_tasks = [
127 {"id": 1, "cron_expression": "* * * * *"},
128 {"id": 2, "cron_expression": "0 * * * *"},
129 ]
130
131 found_task = next((t for t in cog.scheduled_tasks if t["id"] == 2), None)
132 assert found_task is not None
133 assert found_task["cron_expression"] == "0 * * * *"
134
135 def test_remove_task_by_index(self, mock_bot, tmp_path):
136 cog = create_cog(mock_bot, tmp_path)
137 cog.scheduled_tasks = [
138 {"id": 1, "cron_expression": "* * * * *"},
139 {"id": 2, "cron_expression": "0 * * * *"},
140 ]
141
142 cog.scheduled_tasks.pop(0)
143 assert len(cog.scheduled_tasks) == 1
144 assert cog.scheduled_tasks[0]["id"] == 2
145
146 def test_toggle_task_active(self, mock_bot, tmp_path):
147 cog = create_cog(mock_bot, tmp_path)
148 cog.scheduled_tasks = [{"id": 1, "active": True}]
149
150 cog.scheduled_tasks[0]["active"] = False
151 assert cog.scheduled_tasks[0]["active"] is False
152
153 cog.scheduled_tasks[0]["active"] = True
154 assert cog.scheduled_tasks[0]["active"] is True
155
156
157class TestTaskTypes:
158 def test_task_type_audio_aliases(self, mock_bot, tmp_path):
159 audio_types = ("zagraj_audio", "audio")
160 for task_type in audio_types:
161 assert task_type in ("zagraj_audio", "audio")
162
163 def test_task_type_message_aliases(self, mock_bot, tmp_path):
164 message_types = ("wyĊ›lij_wiadomoĊ›Ä‡", "send_message")
165 for task_type in message_types:
166 assert task_type in ("wyĊ›lij_wiadomoĊ›Ä‡", "send_message")
diff --git a/tests/cogs/test_downloader.py b/tests/cogs/test_downloader.py
new file mode 100644
index 0000000..6ac8ae7
--- /dev/null
+++ b/tests/cogs/test_downloader.py
@@ -0,0 +1,93 @@
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)
diff --git a/tests/cogs/test_math_commands.py b/tests/cogs/test_math_commands.py
new file mode 100644
index 0000000..5c78027
--- /dev/null
+++ b/tests/cogs/test_math_commands.py
@@ -0,0 +1,55 @@
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)
diff --git a/tests/cogs/test_music_audio.py b/tests/cogs/test_music_audio.py
new file mode 100644
index 0000000..7ea902e
--- /dev/null
+++ b/tests/cogs/test_music_audio.py
@@ -0,0 +1,93 @@
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, _, 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")
diff --git a/tests/cogs/test_music_external_play.py b/tests/cogs/test_music_external_play.py
new file mode 100644
index 0000000..fd4b625
--- /dev/null
+++ b/tests/cogs/test_music_external_play.py
@@ -0,0 +1,116 @@
1import pytest
2import os
3import asyncio
4import tempfile
5from unittest.mock import AsyncMock, patch, MagicMock
6from tests.conftest import MockApplicationContext
7
8
9@pytest.fixture
10def play_commands_cog(bot):
11 with patch('cogs.utils.temp.get_temp_dir', return_value="/tmp"):
12 from cogs.music.play_commands import PlayCommands
13 cog = PlayCommands(bot)
14 return cog
15
16
17@pytest.fixture
18def temp_mp3_file():
19 with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f:
20 f.write(b'fake mp3 content')
21 temp_path = f.name
22 yield temp_path
23 if os.path.exists(temp_path):
24 os.remove(temp_path)
25
26
27@pytest.mark.asyncio
28async def test_external_url_pattern_matches_youtube(play_commands_cog):
29 from cogs.music.play_commands import EXTERNAL_URL_PATTERN
30
31 assert EXTERNAL_URL_PATTERN.match("https://youtube.com/watch?v=test")
32 assert EXTERNAL_URL_PATTERN.match("https://youtu.be/test")
33 assert EXTERNAL_URL_PATTERN.match("https://www.youtube.com/watch?v=test")
34
35
36@pytest.mark.asyncio
37async def test_external_url_pattern_matches_soundcloud(play_commands_cog):
38 from cogs.music.play_commands import EXTERNAL_URL_PATTERN
39
40 assert EXTERNAL_URL_PATTERN.match("https://soundcloud.com/artist/track")
41 assert EXTERNAL_URL_PATTERN.match("https://api.soundcloud.com/tracks")
42
43
44@pytest.mark.asyncio
45async def test_external_url_pattern_matches_bandcamp(play_commands_cog):
46 from cogs.music.play_commands import EXTERNAL_URL_PATTERN
47
48 assert EXTERNAL_URL_PATTERN.match("https://artist.bandcamp.com/track")
49
50
51@pytest.mark.asyncio
52async def test_external_url_pattern_rejects_copyparty(play_commands_cog):
53 from cogs.music.play_commands import EXTERNAL_URL_PATTERN
54
55 assert not EXTERNAL_URL_PATTERN.match("https://catbox.2137697.xyz/sperma/file.mp3")
56 assert not EXTERNAL_URL_PATTERN.match("some search query")
57
58
59@pytest.mark.asyncio
60async def test_play_command_calls_external_handler(play_commands_cog, mock_channel, mock_guild, bot):
61 ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild)
62
63 with patch.object(play_commands_cog, '_handle_external_url_play', new_callable=AsyncMock) as mock_handler:
64 mock_handler.return_value = None
65 mock_defer = AsyncMock()
66 ctx.defer = mock_defer
67 ctx.respond = AsyncMock()
68
69 await play_commands_cog.play.callback(
70 play_commands_cog, ctx,
71 melodia="https://youtube.com/watch?v=test",
72 file=None, compressed=False
73 )
74
75 mock_handler.assert_called_once_with(ctx, "https://youtube.com/watch?v=test")
76
77
78@pytest.mark.asyncio
79async def test_play_command_calls_local_handler(play_commands_cog, mock_channel, mock_guild, bot):
80 ctx = MockApplicationContext(bot=bot, channel=mock_channel, guild=mock_guild)
81
82 with patch.object(play_commands_cog, '_handle_local_file_play', new_callable=AsyncMock) as mock_handler:
83 mock_handler.return_value = None
84 mock_defer = AsyncMock()
85 ctx.defer = mock_defer
86 ctx.respond = AsyncMock()
87
88 await play_commands_cog.play.callback(
89 play_commands_cog, ctx,
90 melodia="some search query",
91 file=None, compressed=False
92 )
93
94 mock_handler.assert_called_once_with(ctx, "some search query", False)
95
96
97@pytest.mark.asyncio
98async def test_upload_to_copyparty_async(play_commands_cog, temp_mp3_file, monkeypatch):
99 monkeypatch.setenv("COPYPARTY_URL_UPLOAD", "https://catbox.2137697.xyz/sperma/")
100 monkeypatch.setenv("COPYPARTY_PASSWORD", "testpassword")
101
102 mock_response = AsyncMock()
103 mock_response.status = 200
104 mock_response.text = AsyncMock(return_value="")
105 mock_response.json = AsyncMock(return_value={"fileurl": "https://example.com/file.mp3"})
106 mock_response.__aenter__ = AsyncMock(return_value=mock_response)
107 mock_response.__aexit__ = AsyncMock()
108
109 mock_session_context = AsyncMock()
110 mock_session_context.__aenter__ = AsyncMock(return_value=mock_session_context)
111 mock_session_context.__aexit__ = AsyncMock()
112 mock_session_context.post = MagicMock(return_value=mock_response)
113
114 with patch('aiohttp.ClientSession', return_value=mock_session_context):
115 await play_commands_cog._upload_to_copyparty_async(temp_mp3_file)
116 mock_session_context.post.assert_called_once()
diff --git a/tests/cogs/test_music_join.py b/tests/cogs/test_music_join.py
new file mode 100644
index 0000000..3c7ca6e
--- /dev/null
+++ b/tests/cogs/test_music_join.py
@@ -0,0 +1,53 @@
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()
diff --git a/tests/cogs/test_music_play.py b/tests/cogs/test_music_play.py
new file mode 100644
index 0000000..e720c9a
--- /dev/null
+++ b/tests/cogs/test_music_play.py
@@ -0,0 +1,104 @@
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)
diff --git a/tests/cogs/test_qr.py b/tests/cogs/test_qr.py
new file mode 100644
index 0000000..2a132ca
--- /dev/null
+++ b/tests/cogs/test_qr.py
@@ -0,0 +1,75 @@
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()
diff --git a/tests/cogs/test_say.py b/tests/cogs/test_say.py
new file mode 100644
index 0000000..13cad40
--- /dev/null
+++ b/tests/cogs/test_say.py
@@ -0,0 +1,38 @@
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)
diff --git a/tests/cogs/test_search.py b/tests/cogs/test_search.py
new file mode 100644
index 0000000..8c04dfc
--- /dev/null
+++ b/tests/cogs/test_search.py
@@ -0,0 +1,83 @@
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
diff --git a/tests/cogs/test_trigger.py b/tests/cogs/test_trigger.py
new file mode 100644
index 0000000..6aaaa11
--- /dev/null
+++ b/tests/cogs/test_trigger.py
@@ -0,0 +1,106 @@
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)
diff --git a/tests/cogs/test_tts.py b/tests/cogs/test_tts.py
new file mode 100644
index 0000000..de68580
--- /dev/null
+++ b/tests/cogs/test_tts.py
@@ -0,0 +1,58 @@
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 "/tmp/test_tts.mp3"
21
22 with patch('cogs.tts.gTTS') as _:
23 with patch('os.path.exists', return_value=True):
24 with patch('os.unlink'):
25 with patch.object(ctx, 'respond', new_callable=AsyncMock) as mock_respond:
26 with patch('cogs.tts.get_volume', return_value=1.0):
27 await tts_cog.tts.callback(tts_cog, ctx, text="Hello", output="file")
28 mock_respond.assert_called_once()
29
30
31@pytest.mark.asyncio
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)
diff --git a/tests/cogs/test_vclogs.py b/tests/cogs/test_vclogs.py
new file mode 100644
index 0000000..2003478
--- /dev/null
+++ b/tests/cogs/test_vclogs.py
@@ -0,0 +1,83 @@
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
diff --git a/tests/cogs/test_voice.py b/tests/cogs/test_voice.py
new file mode 100644
index 0000000..ade29cb
--- /dev/null
+++ b/tests/cogs/test_voice.py
@@ -0,0 +1,79 @@
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()
diff --git a/tests/cogs/test_web.py b/tests/cogs/test_web.py
new file mode 100644
index 0000000..e0d2208
--- /dev/null
+++ b/tests/cogs/test_web.py
@@ -0,0 +1,57 @@
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)
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..e9f0f6f
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,206 @@
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 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 # Stub for reply - kept async to match Discord interface signature
92
93
94class MockInteraction:
95 def __init__(self, user=None, channel=None, guild=None):
96 self.user = user or MockUser()
97 self.channel = channel or MockChannel()
98 self.guild = guild or MockGuild()
99 self.message = None
100 self.response = AsyncMock()
101 self.followup = AsyncMock()
102 self.client = None
103 self.id = 33333
104 self.token = "test_token"
105 self._state = MagicMock()
106
107 async def defer(self, *args, **kwargs):
108 pass
109
110 async def respond(self, *args, **kwargs):
111 pass
112
113 async def delete_original_response(self, *args, **kwargs):
114 pass
115
116 async def send(self, *args, **kwargs):
117 return MagicMock()
118
119
120# Stub for Discord interaction - test implementation only
121class MockApplicationContext:
122 def __init__(self, bot=None, interaction=None, user=None, channel=None, guild=None):
123 self.bot = bot
124 self.interaction = interaction or MockInteraction(user=user, channel=channel, guild=guild)
125 self.user = user or MockUser()
126 self.channel = channel or MockChannel()
127 self.guild = guild or MockGuild()
128 self.message = None
129 self.response = AsyncMock()
130 self.followup = AsyncMock()
131 self.client = self.bot
132 self.id = 33333
133 self.token = "test_token"
134 self._state = self.interaction._state
135 self.author = self.user
136
137 async def defer(self, *args, **kwargs):
138 pass
139 # Stub for defer - tests don't verify interaction state
140
141 async def respond(self, *args, **kwargs):
142 pass
143 # Stub for respond - tests mock the response
144
145 async def delete_original_response(self, *args, **kwargs):
146 pass
147 # Stub for delete_original_response - not used in tests
148
149 async def send(self, *args, **kwargs):
150 return MagicMock()
151 # Stub for send - not used in tests
152
153 async def reply(self, *args, **kwargs):
154 return MagicMock()
155 # Stub for reply - not used in tests
156
157 async def send_followup(self, *args, **kwargs):
158 return MagicMock()
159 # Stub for send_followup - not used in tests
160
161
162@pytest.fixture
163def mock_guild():
164 return MockGuild()
165
166
167@pytest.fixture
168def mock_channel(mock_guild):
169 channel = MockTextChannel()
170 channel.guild = mock_guild
171 return channel
172
173
174@pytest.fixture
175def mock_voice_channel(mock_guild):
176 channel = MockVoiceChannel()
177 channel.guild = mock_guild
178 return channel
179
180
181@pytest.fixture
182def mock_user():
183 return MockUser()
184
185
186@pytest.fixture
187def mock_member(mock_guild):
188 member = MockUser()
189 member.guild = mock_guild
190 return member
191
192
193@pytest.fixture
194def mock_message(mock_channel, mock_user, mock_guild):
195 return MockMessage(channel=mock_channel, author=mock_user, guild=mock_guild)
196
197
198@pytest.fixture
199def mock_interaction(mock_channel, mock_user, mock_guild):
200 return MockInteraction(user=mock_user, channel=mock_channel, guild=mock_guild)
201
202
203@pytest.fixture
204def bot():
205 intents = discord.Intents.all()
206 return commands.Bot(intents=intents, command_prefix="!", loop=asyncio.new_event_loop())