"""Тесты работы с просмотрами рекламных постов.""" import datetime import pytest from src import domain, dto, usecase from tests.conftest import MockDatabase @pytest.mark.asyncio async def test_fetch_views_no_ad_post_url(usecases: usecase.Usecase, mock_database: MockDatabase) -> None: """Попытка получить просмотры для закупа без ad_post_url возвращает unavailable.""" user = domain.User(telegram_id=123456, username='testuser') await mock_database.create_user(user) target_channel = domain.TargetChannel( telegram_id=-1001234567890, title='Target Channel', username='targetchannel', user_id=user.id, status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) external_channel = domain.ExternalChannel( telegram_id=-1009876543210, title='External Channel', username='externalchannel', description=None, subscribers_count=50000, user_id=user.id, ) await mock_database.create_external_channel(external_channel) creative = domain.Creative( name='Test Creative', text='Test text', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative) placement = domain.Placement( target_channel_id=target_channel.id, external_channel_id=external_channel.id, creative_id=creative.id, user_id=user.id, placement_date=datetime.datetime.now(datetime.UTC), invite_link='https://t.me/+link1', invite_link_type=domain.InviteLinkType.PUBLIC, status=domain.PlacementStatus.ACTIVE, ad_post_url=None, # Нет ссылки на пост ) await mock_database.create_placement(placement) input_data = dto.FetchViewsInputManually( placement_id=placement.id, user_id=user.id, ) result = await usecases.fetch_views(input=input_data) assert result.views_count is None assert result.views_availability == domain.PostViewsAvailability.UNAVAILABLE assert result.error_message == 'No ad_post_url specified' @pytest.mark.asyncio async def test_fetch_views_api_unavailable(usecases: usecase.Usecase, mock_database: MockDatabase) -> None: """Если Telegram API недоступен (возвращает None), статус = unavailable.""" user = domain.User(telegram_id=123456, username='testuser') await mock_database.create_user(user) target_channel = domain.TargetChannel( telegram_id=-1001234567890, title='Target Channel', username='targetchannel', user_id=user.id, status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) external_channel = domain.ExternalChannel( telegram_id=-1009876543210, title='External Channel', username='externalchannel', description=None, subscribers_count=30000, user_id=user.id, ) await mock_database.create_external_channel(external_channel) creative = domain.Creative( name='Test Creative', text='Test text', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative) placement = domain.Placement( target_channel_id=target_channel.id, external_channel_id=external_channel.id, creative_id=creative.id, user_id=user.id, placement_date=datetime.datetime.now(datetime.UTC), invite_link='https://t.me/+link2', invite_link_type=domain.InviteLinkType.PUBLIC, status=domain.PlacementStatus.ACTIVE, ad_post_url='https://t.me/externalchannel/123', ) await mock_database.create_placement(placement) input_data = dto.FetchViewsInputManually( placement_id=placement.id, user_id=user.id, ) # Mock telegram writer returns None (TDLib not integrated) result = await usecases.fetch_views(input=input_data) assert result.views_count is None assert result.views_availability == domain.PostViewsAvailability.UNAVAILABLE assert 'Failed to fetch views' in result.error_message # Проверяем, что placement обновлён updated_placement = await mock_database.get_placement(user.id, placement.id) assert updated_placement is not None assert updated_placement.views_availability == domain.PostViewsAvailability.UNAVAILABLE assert updated_placement.last_views_fetch_at is not None @pytest.mark.asyncio async def test_update_views_manually(usecases: usecase.Usecase, mock_database: MockDatabase) -> None: """Ручное обновление просмотров создаёт снимок и обновляет закуп.""" user = domain.User(telegram_id=123456, username='testuser') await mock_database.create_user(user) target_channel = domain.TargetChannel( telegram_id=-1001234567890, title='Target Channel', username='targetchannel', user_id=user.id, status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) external_channel = domain.ExternalChannel( telegram_id=-1009876543210, title='External Channel', username='externalchannel', description=None, subscribers_count=40000, user_id=user.id, ) await mock_database.create_external_channel(external_channel) creative = domain.Creative( name='Test Creative', text='Test text', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative) placement = domain.Placement( target_channel_id=target_channel.id, external_channel_id=external_channel.id, creative_id=creative.id, user_id=user.id, placement_date=datetime.datetime.now(datetime.UTC), invite_link='https://t.me/+link3', invite_link_type=domain.InviteLinkType.PUBLIC, status=domain.PlacementStatus.ACTIVE, ad_post_url='https://t.me/externalchannel/456', ) await mock_database.create_placement(placement) input_data = dto.UpdateViewsManuallyInput( placement_id=placement.id, user_id=user.id, views_count=15000, ) result = await usecases.update_views_manually(input=input_data) # Проверяем возвращённый результат assert result.views_count == 15000 assert result.views_availability == domain.PostViewsAvailability.MANUAL assert result.last_views_fetch_at is not None # Проверяем, что создана история просмотров histories = [h for h in mock_database.placement_views_histories.values() if h.placement_id == placement.id] assert len(histories) == 1 assert histories[0].views_count == 15000 # Проверяем, что закуп обновлён updated_placement = await mock_database.get_placement(user.id, placement.id) assert updated_placement is not None assert updated_placement.views_count == 15000 assert updated_placement.views_availability == domain.PostViewsAvailability.MANUAL @pytest.mark.asyncio async def test_get_views_history_empty(usecases: usecase.Usecase, mock_database: MockDatabase) -> None: """Получение истории просмотров для закупа без снимков возвращает пустой список.""" user = domain.User(telegram_id=123456, username='testuser') await mock_database.create_user(user) target_channel = domain.TargetChannel( telegram_id=-1001234567890, title='Target Channel', username='targetchannel', user_id=user.id, status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) external_channel = domain.ExternalChannel( telegram_id=-1009876543210, title='External Channel', username='externalchannel', description=None, subscribers_count=25000, user_id=user.id, ) await mock_database.create_external_channel(external_channel) creative = domain.Creative( name='Test Creative', text='Test text', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative) placement = domain.Placement( target_channel_id=target_channel.id, external_channel_id=external_channel.id, creative_id=creative.id, user_id=user.id, placement_date=datetime.datetime.now(datetime.UTC), invite_link='https://t.me/+link4', invite_link_type=domain.InviteLinkType.PUBLIC, status=domain.PlacementStatus.ACTIVE, ) await mock_database.create_placement(placement) input_data = dto.GetViewsHistoryInput( placement_id=placement.id, user_id=user.id, ) result = await usecases.get_views_history(input=input_data) assert len(result.histories) == 0 @pytest.mark.asyncio async def test_get_views_history_with_snapshots(usecases: usecase.Usecase, mock_database: MockDatabase) -> None: """Получение истории просмотров возвращает записи в хронологическом порядке.""" user = domain.User(telegram_id=123456, username='testuser') await mock_database.create_user(user) target_channel = domain.TargetChannel( telegram_id=-1001234567890, title='Target Channel', username='targetchannel', user_id=user.id, status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) external_channel = domain.ExternalChannel( telegram_id=-1009876543210, title='External Channel', username='externalchannel', description=None, subscribers_count=35000, user_id=user.id, ) await mock_database.create_external_channel(external_channel) creative = domain.Creative( name='Test Creative', text='Test text', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative) placement = domain.Placement( target_channel_id=target_channel.id, external_channel_id=external_channel.id, creative_id=creative.id, user_id=user.id, placement_date=datetime.datetime.now(datetime.UTC), invite_link='https://t.me/+link5', invite_link_type=domain.InviteLinkType.PUBLIC, status=domain.PlacementStatus.ACTIVE, ) await mock_database.create_placement(placement) # Создаём несколько записей истории now = datetime.datetime.now(datetime.UTC) history1 = domain.PlacementViewsHistory( placement_id=placement.id, views_count=1000, fetched_at=now - datetime.timedelta(hours=2), ) history2 = domain.PlacementViewsHistory( placement_id=placement.id, views_count=1500, fetched_at=now - datetime.timedelta(hours=1), ) history3 = domain.PlacementViewsHistory( placement_id=placement.id, views_count=2000, fetched_at=now, ) await mock_database.create_placement_views_history(history1) await mock_database.create_placement_views_history(history2) await mock_database.create_placement_views_history(history3) input_data = dto.GetViewsHistoryInput( placement_id=placement.id, user_id=user.id, ) result = await usecases.get_views_history(input=input_data) assert len(result.histories) == 3 # Проверяем сортировку по времени (старые первые) assert result.histories[0].views_count == 1000 assert result.histories[1].views_count == 1500 assert result.histories[2].views_count == 2000 @pytest.mark.asyncio async def test_get_views_history_with_date_filter(usecases: usecase.Usecase, mock_database: MockDatabase) -> None: """Фильтрация истории просмотров по датам работает корректно.""" user = domain.User(telegram_id=123456, username='testuser') await mock_database.create_user(user) target_channel = domain.TargetChannel( telegram_id=-1001234567890, title='Target Channel', username='targetchannel', user_id=user.id, status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) external_channel = domain.ExternalChannel( telegram_id=-1009876543210, title='External Channel', username='externalchannel', description=None, subscribers_count=28000, user_id=user.id, ) await mock_database.create_external_channel(external_channel) creative = domain.Creative( name='Test Creative', text='Test text', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative) placement = domain.Placement( target_channel_id=target_channel.id, external_channel_id=external_channel.id, creative_id=creative.id, user_id=user.id, placement_date=datetime.datetime.now(datetime.UTC), invite_link='https://t.me/+link6', invite_link_type=domain.InviteLinkType.PUBLIC, status=domain.PlacementStatus.ACTIVE, ) await mock_database.create_placement(placement) # Создаём записи с разными датами now = datetime.datetime.now(datetime.UTC) history1 = domain.PlacementViewsHistory( placement_id=placement.id, views_count=1000, fetched_at=now - datetime.timedelta(days=3), ) history2 = domain.PlacementViewsHistory( placement_id=placement.id, views_count=1500, fetched_at=now - datetime.timedelta(days=1), ) history3 = domain.PlacementViewsHistory( placement_id=placement.id, views_count=2000, fetched_at=now, ) await mock_database.create_placement_views_history(history1) await mock_database.create_placement_views_history(history2) await mock_database.create_placement_views_history(history3) # Запрашиваем историю за последние 2 дня input_data = dto.GetViewsHistoryInput( placement_id=placement.id, user_id=user.id, from_date=now - datetime.timedelta(days=2), ) result = await usecases.get_views_history(input=input_data) # Должны вернуться только history2 и history3 assert len(result.histories) == 2 assert result.histories[0].views_count == 1500 assert result.histories[1].views_count == 2000