Files
new_lottery_bot/tests/test_chat_delivery.py
Trevor1985 d5a9c13b68
All checks were successful
continuous-integration/drone/push Build is passing
Recover stalled database connections and skip unreachable chat recipients
2026-09-13 20:21:20 +09:00

123 lines
6.4 KiB
Python

import asyncio
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from aiogram import Bot
from aiogram.exceptions import TelegramRetryAfter
from aiogram.methods import CopyMessage
from aiogram.types import Message, Chat, User as TelegramUser
from sqlalchemy import select
from src.core.broadcast_services import BroadcastService
from src.core.chat_services import ChatMessageService
from src.core.database import async_session_maker
from src.core.models import ChatMessage, BlockedUser
from src.core.services import UserService
from src.handlers.chat_handlers import _is_message_processed, _processed_messages, _copy_with_sender
from test_dispatcher import TelegramStub
def test_equal_message_numbers_from_different_users_are_independent():
_processed_messages.clear()
assert not _is_message_processed(101, 7)
assert not _is_message_processed(102, 7)
assert _is_message_processed(101, 7)
async def test_moderation_lookup_includes_receiving_chat():
async with async_session_maker() as session:
first = await UserService.get_or_create_user(session, 101)
second = await UserService.get_or_create_user(session, 102)
one = ChatMessage(user_id=first.id, telegram_message_id=7, message_type="text",
forwarded_message_ids={"900001": 41})
two = ChatMessage(user_id=second.id, telegram_message_id=7, message_type="text",
forwarded_message_ids={"900002": 41})
session.add_all([one, two])
await session.commit()
assert (await ChatMessageService.get_message_by_telegram_id(session, 7, chat_id=101)).id == one.id
assert (await ChatMessageService.get_message_by_telegram_id(session, 41, chat_id=900002)).id == two.id
assert await ChatMessageService.get_message_by_telegram_id(session, 41, chat_id=123) is None
assert await ChatMessageService.get_message_by_telegram_id(session, 7) is None
async def test_chat_preserves_literal_sender_and_text_and_keeps_max_length_messages():
stub = TelegramStub()
bot = Bot("123456:TEST_TOKEN_FOR_ISOLATED_TESTS", session=stub)
msg = Message(message_id=2, date=datetime.now(timezone.utc), chat=Chat(id=101, type="private"),
from_user=TelegramUser(id=101, first_name="<name>", is_bot=False), text="<b>literal</b> & value").as_(bot)
assert await _copy_with_sender(msg, 102, "<name>") == 100
assert "<name>" in stub.calls[0].text
assert "<b>literal</b> & value" in stub.calls[0].text
assert stub.calls[0].parse_mode is None
assert len(stub.calls[0].entities) == 1 # Only the explicit sender header is formatted.
stub.calls.clear()
long_message = msg.model_copy(update={"text": "x" * 4096}).as_(bot)
await _copy_with_sender(long_message, 102, "Sender")
assert [call.__api_method__ for call in stub.calls] == ["sendMessage", "copyMessage"]
async def test_parallel_broadcast_retries_do_not_deadlock_delivery_slots():
service = BroadcastService()
async def send(index):
message = SimpleNamespace(copy_to=AsyncMock(side_effect=[
TelegramRetryAfter(method=CopyMessage(chat_id=index, from_chat_id=1, message_id=1),
message="Retry", retry_after=0), SimpleNamespace(message_id=1)]))
result = await service.send_message_to_user(None, SimpleNamespace(telegram_id=index), message)
assert message.copy_to.await_count == 2
return result
results = await asyncio.wait_for(asyncio.gather(*(send(index) for index in range(8))), timeout=8)
assert all(success for success, _ in results)
async def test_parallel_delivery_failures_create_one_blocked_user_record():
service = BroadcastService()
async def mark():
async with async_session_maker() as session:
await service.mark_user_blocked(session, 200, "blocked_bot", "test")
await asyncio.gather(*(mark() for _ in range(8)))
async with async_session_maker() as session:
record = (await session.scalars(select(BlockedUser))).one()
assert record.telegram_id == 200
assert record.attempt_count == 8
async def test_inactivity_job_preserves_existing_delivery_block_and_continues():
from datetime import timedelta
from src.core.activity_service import ActivityService
async with async_session_maker() as session:
for number in (201, 202):
user = await UserService.get_or_create_user(session, number)
user.is_registered = True
user.last_activity = datetime.now(timezone.utc) - timedelta(days=31)
await session.commit()
await BroadcastService().mark_user_blocked(session, 201, "blocked_bot", "test")
assert await ActivityService.mark_inactive_users(session) == 1
assert await ActivityService.mark_inactive_users(session) == 0
assert await session.scalar(select(BlockedUser.error_type).where(BlockedUser.telegram_id == 201)) == "blocked_bot"
@pytest.mark.parametrize("error_kind", ["forbidden", "chat_not_found", "invalid_content"])
async def test_chat_skips_unreachable_recipients_and_restores_them_on_incoming_activity(monkeypatch, error_kind):
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from aiogram.methods import SendMessage
from src.core.activity_service import ActivityService
from src.handlers import chat_handlers
async with async_session_maker() as session:
await UserService.get_or_create_user(session, 301)
await UserService.get_or_create_user(session, 302)
error = (TelegramForbiddenError if error_kind == "forbidden" else TelegramBadRequest)(
method=SendMessage(chat_id=301, text="test"),
message={"forbidden": "bot was blocked by the user", "chat_not_found": "chat not found",
"invalid_content": "CUSTOM_EMOJI_INVALID"}[error_kind],
)
monkeypatch.setattr(chat_handlers, "copy_preserving_entities", AsyncMock(side_effect=error))
assert await chat_handlers._copy_with_sender(SimpleNamespace(), 301, "Sender") is None
async with async_session_maker() as session:
recipients = {user.telegram_id for user in await chat_handlers.get_all_active_users(session)}
assert recipients == ({301, 302} if error_kind == "invalid_content" else {302})
await ActivityService.reactivate_user(session, 301)
assert {user.telegram_id for user in await chat_handlers.get_all_active_users(session)} == {301, 302}