|
3 | 3 | import pytest |
4 | 4 | from decimal import Decimal |
5 | 5 |
|
6 | | -from bot.main import detect_changes, format_changes |
| 6 | +from bot.main import detect_changes, format_changes, get_user_id, TELEGRAM_MAX_LENGTH |
7 | 7 |
|
8 | 8 |
|
9 | 9 | def parse_input(text: str) -> tuple[str, Decimal]: |
@@ -172,3 +172,131 @@ def test_handles_missing_fields(self): |
172 | 172 | curr = {} |
173 | 173 | result = format_changes(changes, curr) |
174 | 174 | assert "Test change" in result |
| 175 | + |
| 176 | + def test_truncates_long_messages(self): |
| 177 | + """Test that messages exceeding Telegram limit are truncated.""" |
| 178 | + # Create many changes that would exceed the limit |
| 179 | + changes = [f"Change {i}: Order modified at price ${90000 + i:,}" for i in range(200)] |
| 180 | + curr = {"direction": "long", "size": Decimal("1"), "entry_price": Decimal("95000")} |
| 181 | + result = format_changes(changes, curr) |
| 182 | + assert len(result) <= TELEGRAM_MAX_LENGTH |
| 183 | + assert "more changes" in result |
| 184 | + |
| 185 | + |
| 186 | +class TestDetectChangesEdgeCases: |
| 187 | + """Additional edge case tests for detect_changes.""" |
| 188 | + |
| 189 | + def test_string_vs_number_sz_no_spurious_change(self): |
| 190 | + """String "0.5" and number 0.5 should not trigger a change.""" |
| 191 | + prev = { |
| 192 | + "direction": "long", |
| 193 | + "size": "1", |
| 194 | + "entry_price": "95000", |
| 195 | + "orders": [{"oid": 123, "side": "B", "sz": "0.5", "limitPx": "90000"}] |
| 196 | + } |
| 197 | + curr = { |
| 198 | + "direction": "long", |
| 199 | + "size": Decimal("1"), |
| 200 | + "entry_price": Decimal("95000"), |
| 201 | + "orders": [{"oid": 123, "side": "B", "sz": 0.5, "limitPx": 90000}] # numbers instead of strings |
| 202 | + } |
| 203 | + changes = detect_changes(prev, curr) |
| 204 | + assert len(changes) == 0 |
| 205 | + |
| 206 | + def test_int_vs_string_oid_matched(self): |
| 207 | + """Integer oid 123 and string "123" should match the same order.""" |
| 208 | + prev = { |
| 209 | + "direction": "long", |
| 210 | + "size": "1", |
| 211 | + "entry_price": "95000", |
| 212 | + "orders": [{"oid": 123, "side": "B", "sz": "0.5", "limitPx": "90000"}] # int oid |
| 213 | + } |
| 214 | + curr = { |
| 215 | + "direction": "long", |
| 216 | + "size": Decimal("1"), |
| 217 | + "entry_price": Decimal("95000"), |
| 218 | + "orders": [{"oid": "123", "side": "B", "sz": "0.5", "limitPx": "90000"}] # string oid |
| 219 | + } |
| 220 | + changes = detect_changes(prev, curr) |
| 221 | + assert len(changes) == 0 |
| 222 | + |
| 223 | + def test_orders_with_none_oid_filtered(self): |
| 224 | + """Orders with None oid should be filtered out.""" |
| 225 | + prev = { |
| 226 | + "direction": "long", |
| 227 | + "size": "1", |
| 228 | + "entry_price": "95000", |
| 229 | + "orders": [{"oid": None, "side": "B", "sz": "0.5", "limitPx": "90000"}] |
| 230 | + } |
| 231 | + curr = { |
| 232 | + "direction": "long", |
| 233 | + "size": Decimal("1"), |
| 234 | + "entry_price": Decimal("95000"), |
| 235 | + "orders": [{"oid": None, "side": "B", "sz": "0.6", "limitPx": "91000"}] |
| 236 | + } |
| 237 | + changes = detect_changes(prev, curr) |
| 238 | + # Both orders have None oid, so they're filtered out - no changes detected |
| 239 | + assert len(changes) == 0 |
| 240 | + |
| 241 | + def test_malformed_price_handled(self): |
| 242 | + """Malformed limitPx should not crash the function.""" |
| 243 | + prev = { |
| 244 | + "direction": "long", |
| 245 | + "size": "1", |
| 246 | + "entry_price": "95000", |
| 247 | + "orders": [{"oid": 123, "side": "B", "sz": "0.5", "limitPx": "invalid"}] |
| 248 | + } |
| 249 | + curr = { |
| 250 | + "direction": "long", |
| 251 | + "size": Decimal("1"), |
| 252 | + "entry_price": Decimal("95000"), |
| 253 | + "orders": [] |
| 254 | + } |
| 255 | + # Should not raise, should produce "Order removed" with fallback price display |
| 256 | + changes = detect_changes(prev, curr) |
| 257 | + assert len(changes) == 1 |
| 258 | + assert "Order removed" in changes[0] |
| 259 | + |
| 260 | + def test_multiple_changes_detected(self): |
| 261 | + """Multiple simultaneous changes are all detected.""" |
| 262 | + prev = { |
| 263 | + "direction": "long", |
| 264 | + "size": "1", |
| 265 | + "entry_price": "95000", |
| 266 | + "orders": [{"oid": 123, "side": "B", "sz": "0.5", "limitPx": "90000"}] |
| 267 | + } |
| 268 | + curr = { |
| 269 | + "direction": "short", # changed |
| 270 | + "size": Decimal("2"), # changed |
| 271 | + "entry_price": Decimal("96000"), # changed |
| 272 | + "orders": [ |
| 273 | + {"oid": 123, "side": "B", "sz": "0.6", "limitPx": "91000"}, # modified |
| 274 | + {"oid": 456, "side": "A", "sz": "0.3", "limitPx": "100000"} # added |
| 275 | + ] |
| 276 | + } |
| 277 | + changes = detect_changes(prev, curr) |
| 278 | + # direction + size + entry + order modified + order added = 5 changes |
| 279 | + assert len(changes) == 5 |
| 280 | + |
| 281 | + |
| 282 | +class TestGetUserId: |
| 283 | + """Tests for the get_user_id helper function.""" |
| 284 | + |
| 285 | + def test_returns_none_for_none_effective_user(self): |
| 286 | + """Should return None when effective_user is None.""" |
| 287 | + class MockUpdate: |
| 288 | + effective_user = None |
| 289 | + |
| 290 | + result = get_user_id(MockUpdate()) |
| 291 | + assert result is None |
| 292 | + |
| 293 | + def test_returns_id_when_effective_user_exists(self): |
| 294 | + """Should return user ID when effective_user exists.""" |
| 295 | + class MockUser: |
| 296 | + id = 12345 |
| 297 | + |
| 298 | + class MockUpdate: |
| 299 | + effective_user = MockUser() |
| 300 | + |
| 301 | + result = get_user_id(MockUpdate()) |
| 302 | + assert result == 12345 |
0 commit comments