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")