Manage Atom feeds in a persistent git repository
1"""Tests for the Thicket Zulip bot.""" 2 3import pytest 4 5from thicket.bots.test_bot import ( 6 BotTester, 7 MockBotHandler, 8 create_test_entry, 9 create_test_message, 10) 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 ( 219 "Debug mode validation failed" in self.handler.sent_messages[0]["content"] 220 ) 221 222 def test_debug_mode_dm_posting(self) -> None: 223 """Test that debug mode posts DMs instead of stream messages.""" 224 from unittest.mock import Mock 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 tester.send_command("config stream general") 279 tester.assert_response_contains("Stream set to") 280 281 # Configure topic 282 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")