diff --git a/tests/test_telegram.py b/tests/test_telegram.py index 17ec801..264625b 100644 --- a/tests/test_telegram.py +++ b/tests/test_telegram.py @@ -261,6 +261,71 @@ async def test_aggregator_buffer_cleared_after_flush(): _cleanup(path) +# --------------------------------------------------------------------------- +# BATON-007: 400 "chat not found" handling +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_send_message_400_chat_not_found_does_not_raise(): + """400 'chat not found' must not raise an exception (service stays alive).""" + with respx.mock(assert_all_called=False) as mock: + mock.post(SEND_URL).mock( + return_value=httpx.Response( + 400, + json={"ok": False, "error_code": 400, "description": "Bad Request: chat not found"}, + ) + ) + # Must not raise — service must stay alive even with wrong CHAT_ID + await send_message("test") + + +@pytest.mark.asyncio +async def test_send_message_400_chat_not_found_logs_error(caplog): + """400 response from Telegram must be logged as ERROR with the status code.""" + import logging + + with respx.mock(assert_all_called=False) as mock: + mock.post(SEND_URL).mock( + return_value=httpx.Response( + 400, + json={"ok": False, "error_code": 400, "description": "Bad Request: chat not found"}, + ) + ) + with caplog.at_level(logging.ERROR, logger="backend.telegram"): + await send_message("test chat not found") + + assert any("400" in record.message for record in caplog.records), ( + "Expected ERROR log containing '400' but got: " + str([r.message for r in caplog.records]) + ) + + +@pytest.mark.asyncio +async def test_send_message_400_breaks_after_first_attempt(): + """On 400, send_message breaks immediately (no retry loop) — only one HTTP call made.""" + with respx.mock(assert_all_called=False) as mock: + route = mock.post(SEND_URL).mock( + return_value=httpx.Response( + 400, + json={"ok": False, "error_code": 400, "description": "Bad Request: chat not found"}, + ) + ) + await send_message("test no retry on 400") + + assert route.call_count == 1, f"Expected 1 call on 400, got {route.call_count}" + + +@pytest.mark.asyncio +async def test_send_message_all_5xx_retries_exhausted_does_not_raise(): + """When all 3 attempts fail with 5xx, send_message logs error but does NOT raise.""" + with respx.mock(assert_all_called=False) as mock: + mock.post(SEND_URL).mock( + return_value=httpx.Response(500, text="Internal Server Error") + ) + with patch("backend.telegram.asyncio.sleep", new_callable=AsyncMock): + # Must not raise — message is dropped, service stays alive + await send_message("test all retries exhausted") + + @pytest.mark.asyncio async def test_aggregator_unknown_user_shows_uuid_prefix(): """If user_name is None, the message shows first 8 chars of uuid."""