Manage Atom feeds in a persistent git repository
1"""Tests for the Thicket Zulip bot."""
2
3
4import pytest
5
6from thicket.bots.test_bot import (
7 BotTester,
8 MockBotHandler,
9 create_test_entry,
10 create_test_message,
11)
12from thicket.bots.thicket_bot import ThicketBotHandler
13
14
15class TestThicketBot:
16 """Test suite for ThicketBotHandler."""
17
18 def setup_method(self) -> None:
19 """Set up test environment."""
20 self.bot = ThicketBotHandler()
21 self.handler = MockBotHandler()
22
23 def test_usage(self) -> None:
24 """Test bot usage message."""
25 usage = self.bot.usage()
26 assert "Thicket Feed Bot" in usage
27 assert "@thicket status" in usage
28 assert "@thicket config" in usage
29
30 def test_help_command(self) -> None:
31 """Test help command response."""
32 message = create_test_message("@thicket help")
33 self.bot.handle_message(message, self.handler)
34
35 assert len(self.handler.sent_messages) == 1
36 response = self.handler.sent_messages[0]["content"]
37 assert "Thicket Feed Bot" in response
38
39 def test_status_command_unconfigured(self) -> None:
40 """Test status command when bot is not configured."""
41 message = create_test_message("@thicket status")
42 self.bot.handle_message(message, self.handler)
43
44 assert len(self.handler.sent_messages) == 1
45 response = self.handler.sent_messages[0]["content"]
46 assert "Not configured" in response
47 assert "Stream:" in response
48 assert "Topic:" in response
49
50 def test_config_stream_command(self) -> None:
51 """Test setting stream configuration."""
52 message = create_test_message("@thicket config stream general")
53 self.bot.handle_message(message, self.handler)
54
55 assert len(self.handler.sent_messages) == 1
56 response = self.handler.sent_messages[0]["content"]
57 assert "Stream set to: **general**" in response
58 assert self.bot.stream_name == "general"
59
60 def test_config_topic_command(self) -> None:
61 """Test setting topic configuration."""
62 message = create_test_message("@thicket config topic 'Feed Updates'")
63 self.bot.handle_message(message, self.handler)
64
65 assert len(self.handler.sent_messages) == 1
66 response = self.handler.sent_messages[0]["content"]
67 assert "Topic set to:" in response and "Feed Updates" in response
68 assert self.bot.topic_name == "'Feed Updates'"
69
70 def test_config_interval_command(self) -> None:
71 """Test setting sync interval."""
72 message = create_test_message("@thicket config interval 600")
73 self.bot.handle_message(message, self.handler)
74
75 assert len(self.handler.sent_messages) == 1
76 response = self.handler.sent_messages[0]["content"]
77 assert "Sync interval set to: **600s**" in response
78 assert self.bot.sync_interval == 600
79
80 def test_config_interval_too_small(self) -> None:
81 """Test setting sync interval that's too small."""
82 message = create_test_message("@thicket config interval 30")
83 self.bot.handle_message(message, self.handler)
84
85 assert len(self.handler.sent_messages) == 1
86 response = self.handler.sent_messages[0]["content"]
87 assert "must be at least 60 seconds" in response
88 assert self.bot.sync_interval != 30
89
90 def test_config_path_nonexistent(self) -> None:
91 """Test setting config path that doesn't exist."""
92 message = create_test_message("@thicket config path /nonexistent/config.yaml")
93 self.bot.handle_message(message, self.handler)
94
95 assert len(self.handler.sent_messages) == 1
96 response = self.handler.sent_messages[0]["content"]
97 assert "Config file not found" in response
98
99 def test_unknown_command(self) -> None:
100 """Test unknown command handling."""
101 message = create_test_message("@thicket unknown")
102 self.bot.handle_message(message, self.handler)
103
104 assert len(self.handler.sent_messages) == 1
105 response = self.handler.sent_messages[0]["content"]
106 assert "Unknown command: unknown" in response
107
108 def test_config_persistence(self) -> None:
109 """Test that configuration is persisted."""
110 # Set some config
111 self.bot.stream_name = "test-stream"
112 self.bot.topic_name = "test-topic"
113 self.bot.sync_interval = 600
114
115 # Save config
116 self.bot._save_bot_config(self.handler)
117
118 # Create new bot instance
119 new_bot = ThicketBotHandler()
120 new_bot._load_bot_config(self.handler)
121
122 # Check config was loaded
123 assert new_bot.stream_name == "test-stream"
124 assert new_bot.topic_name == "test-topic"
125 assert new_bot.sync_interval == 600
126
127 def test_posted_entries_persistence(self) -> None:
128 """Test that posted entries are persisted."""
129 # Add some entries
130 self.bot.posted_entries = {"user1:entry1", "user2:entry2"}
131
132 # Save entries
133 self.bot._save_posted_entries(self.handler)
134
135 # Create new bot instance
136 new_bot = ThicketBotHandler()
137 new_bot._load_posted_entries(self.handler)
138
139 # Check entries were loaded
140 assert new_bot.posted_entries == {"user1:entry1", "user2:entry2"}
141
142 def test_mention_detection(self) -> None:
143 """Test bot mention detection."""
144 assert self.bot._is_mentioned("@Thicket Bot help", self.handler)
145 assert self.bot._is_mentioned("@thicket status", self.handler)
146 assert not self.bot._is_mentioned("regular message", self.handler)
147
148 def test_mention_cleaning(self) -> None:
149 """Test cleaning mentions from messages."""
150 cleaned = self.bot._clean_mention("@Thicket Bot status", self.handler)
151 assert cleaned == "status"
152
153 cleaned = self.bot._clean_mention("@thicket help", self.handler)
154 assert cleaned == "help"
155
156 def test_sync_now_uninitialized(self) -> None:
157 """Test sync now command when not initialized."""
158 message = create_test_message("@thicket sync now")
159 self.bot.handle_message(message, self.handler)
160
161 assert len(self.handler.sent_messages) == 1
162 response = self.handler.sent_messages[0]["content"]
163 assert "not initialized" in response.lower()
164
165 def test_debug_mode_initialization(self) -> None:
166 """Test debug mode initialization."""
167 import os
168
169 # Mock environment variable
170 os.environ["THICKET_DEBUG_USER"] = "testuser"
171
172 try:
173 bot = ThicketBotHandler()
174 # Simulate initialize call
175 bot.debug_user = os.getenv("THICKET_DEBUG_USER")
176
177 assert bot.debug_user == "testuser"
178 assert bot.debug_zulip_user_id is None # Not validated yet
179 finally:
180 # Clean up
181 if "THICKET_DEBUG_USER" in os.environ:
182 del os.environ["THICKET_DEBUG_USER"]
183
184 def test_debug_mode_status(self) -> None:
185 """Test status command in debug mode."""
186 self.bot.debug_user = "testuser"
187 self.bot.debug_zulip_user_id = "test.user"
188
189 message = create_test_message("@thicket status")
190 self.bot.handle_message(message, self.handler)
191
192 assert len(self.handler.sent_messages) == 1
193 response = self.handler.sent_messages[0]["content"]
194 assert "**Debug Mode:** ENABLED" in response
195 assert "**Debug User:** testuser" in response
196 assert "**Debug Zulip ID:** test.user" in response
197
198 def test_debug_mode_check_initialization(self) -> None:
199 """Test initialization check in debug mode."""
200 from unittest.mock import Mock
201
202 # Setup mock git store and config
203 self.bot.git_store = Mock()
204 self.bot.config = Mock()
205 self.bot.debug_user = "testuser"
206 self.bot.debug_zulip_user_id = "test.user"
207
208 message = create_test_message("@thicket sync now")
209
210 # Should pass with debug mode properly set up
211 result = self.bot._check_initialization(message, self.handler)
212 assert result is True
213
214 # Should fail if debug_zulip_user_id is missing
215 self.bot.debug_zulip_user_id = None
216 result = self.bot._check_initialization(message, self.handler)
217 assert result is False
218 assert len(self.handler.sent_messages) == 1
219 assert "Debug mode validation failed" in self.handler.sent_messages[0]["content"]
220
221 def test_debug_mode_dm_posting(self) -> None:
222 """Test that debug mode posts DMs instead of stream messages."""
223 from unittest.mock import Mock
224
225
226 # Setup bot in debug mode
227 self.bot.debug_user = "testuser"
228 self.bot.debug_zulip_user_id = "test.user@example.com"
229 self.bot.git_store = Mock()
230
231 # Create a test entry
232 entry = create_test_entry()
233
234 # Mock the handler config
235 self.handler.config_info = {
236 "full_name": "Thicket Bot",
237 "email": "thicket-bot@example.com",
238 "site": "https://example.zulipchat.com"
239 }
240
241 # Mock git store user
242 mock_user = Mock()
243 mock_user.get_zulip_mention.return_value = "author.user"
244 self.bot.git_store.get_user.return_value = mock_user
245
246 # Post entry
247 self.bot._post_entry_to_zulip(entry, self.handler, "testauthor")
248
249 # Check that a DM was sent
250 assert len(self.handler.sent_messages) == 1
251 message = self.handler.sent_messages[0]
252
253 # Verify it's a DM
254 assert message["type"] == "private"
255 assert message["to"] == ["test.user@example.com"]
256 assert "DEBUG:" in message["content"]
257 assert entry.title in message["content"]
258 assert "@**author.user** posted:" in message["content"]
259
260
261class TestBotTester:
262 """Test the bot testing utilities."""
263
264 def test_bot_tester_basic(self) -> None:
265 """Test basic bot tester functionality."""
266 tester = BotTester()
267
268 # Test help command
269 responses = tester.send_command("help")
270 assert len(responses) == 1
271 assert "Thicket Feed Bot" in tester.get_last_response_content()
272
273 def test_bot_tester_config(self) -> None:
274 """Test bot tester configuration."""
275 tester = BotTester()
276
277 # Configure stream
278 responses = tester.send_command("config stream general")
279 tester.assert_response_contains("Stream set to")
280
281 # Configure topic
282 responses = tester.send_command("config topic test")
283 tester.assert_response_contains("Topic set to")
284
285 def test_assert_response_contains(self) -> None:
286 """Test response assertion helper."""
287 tester = BotTester()
288
289 # Send command
290 tester.send_command("help")
291
292 # This should pass
293 tester.assert_response_contains("Thicket Feed Bot")
294
295 # This should fail
296 with pytest.raises(AssertionError):
297 tester.assert_response_contains("nonexistent text")