Manage Atom feeds in a persistent git repository
1"""Tests for the Thicket Zulip bot."""
2
3import json
4import tempfile
5from pathlib import Path
6from unittest.mock import patch
7
8import pytest
9
10from thicket.bots.test_bot import BotTester, MockBotHandler, create_test_message, create_test_entry
11from thicket.bots.thicket_bot import ThicketBotHandler
12
13
14class TestThicketBot:
15 """Test suite for ThicketBotHandler."""
16
17 def setup_method(self) -> None:
18 """Set up test environment."""
19 self.bot = ThicketBotHandler()
20 self.handler = MockBotHandler()
21
22 def test_usage(self) -> None:
23 """Test bot usage message."""
24 usage = self.bot.usage()
25 assert "Thicket Feed Bot" in usage
26 assert "@thicket status" in usage
27 assert "@thicket config" in usage
28
29 def test_help_command(self) -> None:
30 """Test help command response."""
31 message = create_test_message("@thicket help")
32 self.bot.handle_message(message, self.handler)
33
34 assert len(self.handler.sent_messages) == 1
35 response = self.handler.sent_messages[0]["content"]
36 assert "Thicket Feed Bot" in response
37
38 def test_status_command_unconfigured(self) -> None:
39 """Test status command when bot is not configured."""
40 message = create_test_message("@thicket status")
41 self.bot.handle_message(message, self.handler)
42
43 assert len(self.handler.sent_messages) == 1
44 response = self.handler.sent_messages[0]["content"]
45 assert "Not configured" in response
46 assert "Stream:" in response
47 assert "Topic:" in response
48
49 def test_config_stream_command(self) -> None:
50 """Test setting stream configuration."""
51 message = create_test_message("@thicket config stream general")
52 self.bot.handle_message(message, self.handler)
53
54 assert len(self.handler.sent_messages) == 1
55 response = self.handler.sent_messages[0]["content"]
56 assert "Stream set to: **general**" in response
57 assert self.bot.stream_name == "general"
58
59 def test_config_topic_command(self) -> None:
60 """Test setting topic configuration."""
61 message = create_test_message("@thicket config topic 'Feed Updates'")
62 self.bot.handle_message(message, self.handler)
63
64 assert len(self.handler.sent_messages) == 1
65 response = self.handler.sent_messages[0]["content"]
66 assert "Topic set to:" in response and "Feed Updates" in response
67 assert self.bot.topic_name == "'Feed Updates'"
68
69 def test_config_interval_command(self) -> None:
70 """Test setting sync interval."""
71 message = create_test_message("@thicket config interval 600")
72 self.bot.handle_message(message, self.handler)
73
74 assert len(self.handler.sent_messages) == 1
75 response = self.handler.sent_messages[0]["content"]
76 assert "Sync interval set to: **600s**" in response
77 assert self.bot.sync_interval == 600
78
79 def test_config_interval_too_small(self) -> None:
80 """Test setting sync interval that's too small."""
81 message = create_test_message("@thicket config interval 30")
82 self.bot.handle_message(message, self.handler)
83
84 assert len(self.handler.sent_messages) == 1
85 response = self.handler.sent_messages[0]["content"]
86 assert "must be at least 60 seconds" in response
87 assert self.bot.sync_interval != 30
88
89 def test_config_path_nonexistent(self) -> None:
90 """Test setting config path that doesn't exist."""
91 message = create_test_message("@thicket config path /nonexistent/config.yaml")
92 self.bot.handle_message(message, self.handler)
93
94 assert len(self.handler.sent_messages) == 1
95 response = self.handler.sent_messages[0]["content"]
96 assert "Config file not found" in response
97
98 def test_unknown_command(self) -> None:
99 """Test unknown command handling."""
100 message = create_test_message("@thicket unknown")
101 self.bot.handle_message(message, self.handler)
102
103 assert len(self.handler.sent_messages) == 1
104 response = self.handler.sent_messages[0]["content"]
105 assert "Unknown command: unknown" in response
106
107 def test_config_persistence(self) -> None:
108 """Test that configuration is persisted."""
109 # Set some config
110 self.bot.stream_name = "test-stream"
111 self.bot.topic_name = "test-topic"
112 self.bot.sync_interval = 600
113
114 # Save config
115 self.bot._save_bot_config(self.handler)
116
117 # Create new bot instance
118 new_bot = ThicketBotHandler()
119 new_bot._load_bot_config(self.handler)
120
121 # Check config was loaded
122 assert new_bot.stream_name == "test-stream"
123 assert new_bot.topic_name == "test-topic"
124 assert new_bot.sync_interval == 600
125
126 def test_posted_entries_persistence(self) -> None:
127 """Test that posted entries are persisted."""
128 # Add some entries
129 self.bot.posted_entries = {"user1:entry1", "user2:entry2"}
130
131 # Save entries
132 self.bot._save_posted_entries(self.handler)
133
134 # Create new bot instance
135 new_bot = ThicketBotHandler()
136 new_bot._load_posted_entries(self.handler)
137
138 # Check entries were loaded
139 assert new_bot.posted_entries == {"user1:entry1", "user2:entry2"}
140
141 def test_mention_detection(self) -> None:
142 """Test bot mention detection."""
143 assert self.bot._is_mentioned("@Thicket Bot help", self.handler)
144 assert self.bot._is_mentioned("@thicket status", self.handler)
145 assert not self.bot._is_mentioned("regular message", self.handler)
146
147 def test_mention_cleaning(self) -> None:
148 """Test cleaning mentions from messages."""
149 cleaned = self.bot._clean_mention("@Thicket Bot status", self.handler)
150 assert cleaned == "status"
151
152 cleaned = self.bot._clean_mention("@thicket help", self.handler)
153 assert cleaned == "help"
154
155 def test_sync_now_uninitialized(self) -> None:
156 """Test sync now command when not initialized."""
157 message = create_test_message("@thicket sync now")
158 self.bot.handle_message(message, self.handler)
159
160 assert len(self.handler.sent_messages) == 1
161 response = self.handler.sent_messages[0]["content"]
162 assert "not initialized" in response.lower()
163
164 def test_debug_mode_initialization(self) -> None:
165 """Test debug mode initialization."""
166 import os
167
168 # Mock environment variable
169 os.environ["THICKET_DEBUG_USER"] = "testuser"
170
171 try:
172 bot = ThicketBotHandler()
173 # Simulate initialize call
174 bot.debug_user = os.getenv("THICKET_DEBUG_USER")
175
176 assert bot.debug_user == "testuser"
177 assert bot.debug_zulip_user_id is None # Not validated yet
178 finally:
179 # Clean up
180 if "THICKET_DEBUG_USER" in os.environ:
181 del os.environ["THICKET_DEBUG_USER"]
182
183 def test_debug_mode_status(self) -> None:
184 """Test status command in debug mode."""
185 self.bot.debug_user = "testuser"
186 self.bot.debug_zulip_user_id = "test.user"
187
188 message = create_test_message("@thicket status")
189 self.bot.handle_message(message, self.handler)
190
191 assert len(self.handler.sent_messages) == 1
192 response = self.handler.sent_messages[0]["content"]
193 assert "**Debug Mode:** ENABLED" in response
194 assert "**Debug User:** testuser" in response
195 assert "**Debug Zulip ID:** test.user" in response
196
197 def test_debug_mode_check_initialization(self) -> None:
198 """Test initialization check in debug mode."""
199 from unittest.mock import Mock
200
201 # Setup mock git store and config
202 self.bot.git_store = Mock()
203 self.bot.config = Mock()
204 self.bot.debug_user = "testuser"
205 self.bot.debug_zulip_user_id = "test.user"
206
207 message = create_test_message("@thicket sync now")
208
209 # Should pass with debug mode properly set up
210 result = self.bot._check_initialization(message, self.handler)
211 assert result is True
212
213 # Should fail if debug_zulip_user_id is missing
214 self.bot.debug_zulip_user_id = None
215 result = self.bot._check_initialization(message, self.handler)
216 assert result is False
217 assert len(self.handler.sent_messages) == 1
218 assert "Debug mode validation failed" in self.handler.sent_messages[0]["content"]
219
220 def test_debug_mode_dm_posting(self) -> None:
221 """Test that debug mode posts DMs instead of stream messages."""
222 from unittest.mock import Mock
223 from datetime import datetime
224 from pydantic import HttpUrl
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")