aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tests/README.md84
-rw-r--r--tests/cogs/test_cron.py166
-rw-r--r--tests/conftest.py20
3 files changed, 180 insertions, 90 deletions
diff --git a/tests/README.md b/tests/README.md
deleted file mode 100644
index 77b832c..0000000
--- a/tests/README.md
+++ /dev/null
@@ -1,84 +0,0 @@
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_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/conftest.py b/tests/conftest.py
index c471b6b..e9f0f6f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -32,7 +32,7 @@ class MockChannel:
32 self.category = None 32 self.category = None
33 self._state = MagicMock() 33 self._state = MagicMock()
34 34
35 async def send(self, *args, **kwargs): 35 def send(self, *args, **kwargs):
36 return MagicMock() 36 return MagicMock()
37 37
38 38
@@ -88,6 +88,7 @@ class MockMessage:
88 88
89 async def reply(self, *args, **kwargs): 89 async def reply(self, *args, **kwargs):
90 return MagicMock() 90 return MagicMock()
91 # Stub for reply - kept async to match Discord interface signature
91 92
92 93
93class MockInteraction: 94class MockInteraction:
@@ -102,20 +103,21 @@ class MockInteraction:
102 self.id = 33333 103 self.id = 33333
103 self.token = "test_token" 104 self.token = "test_token"
104 self._state = MagicMock() 105 self._state = MagicMock()
105 106
106 async def defer(self, *args, **kwargs): 107 async def defer(self, *args, **kwargs):
107 pass 108 pass
108 109
109 async def respond(self, *args, **kwargs): 110 async def respond(self, *args, **kwargs):
110 pass 111 pass
111 112
112 async def delete_original_response(self, *args, **kwargs): 113 async def delete_original_response(self, *args, **kwargs):
113 pass 114 pass
114 115
115 async def send(self, *args, **kwargs): 116 async def send(self, *args, **kwargs):
116 return MagicMock() 117 return MagicMock()
117 118
118 119
120# Stub for Discord interaction - test implementation only
119class MockApplicationContext: 121class MockApplicationContext:
120 def __init__(self, bot=None, interaction=None, user=None, channel=None, guild=None): 122 def __init__(self, bot=None, interaction=None, user=None, channel=None, guild=None):
121 self.bot = bot 123 self.bot = bot
@@ -134,21 +136,27 @@ class MockApplicationContext:
134 136
135 async def defer(self, *args, **kwargs): 137 async def defer(self, *args, **kwargs):
136 pass 138 pass
139 # Stub for defer - tests don't verify interaction state
137 140
138 async def respond(self, *args, **kwargs): 141 async def respond(self, *args, **kwargs):
139 pass 142 pass
143 # Stub for respond - tests mock the response
140 144
141 async def delete_original_response(self, *args, **kwargs): 145 async def delete_original_response(self, *args, **kwargs):
142 pass 146 pass
147 # Stub for delete_original_response - not used in tests
143 148
144 async def send(self, *args, **kwargs): 149 async def send(self, *args, **kwargs):
145 return MagicMock() 150 return MagicMock()
151 # Stub for send - not used in tests
146 152
147 async def reply(self, *args, **kwargs): 153 async def reply(self, *args, **kwargs):
148 return MagicMock() 154 return MagicMock()
155 # Stub for reply - not used in tests
149 156
150 async def send_followup(self, *args, **kwargs): 157 async def send_followup(self, *args, **kwargs):
151 return MagicMock() 158 return MagicMock()
159 # Stub for send_followup - not used in tests
152 160
153 161
154@pytest.fixture 162@pytest.fixture
@@ -195,4 +203,4 @@ def mock_interaction(mock_channel, mock_user, mock_guild):
195@pytest.fixture 203@pytest.fixture
196def bot(): 204def bot():
197 intents = discord.Intents.all() 205 intents = discord.Intents.all()
198 return commands.Bot(intents=intents, command_prefix="!", loop=asyncio.new_event_loop()) \ No newline at end of file 206 return commands.Bot(intents=intents, command_prefix="!", loop=asyncio.new_event_loop())