"""Тесты работы с закупами.""" import datetime import pytest from fastapi import HTTPException from src import domain, dto, usecase from tests.conftest import MockDatabase @pytest.mark.asyncio async def test_create_placement_with_public_invite_link(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='Ad channel', subscribers_count=50000, user_id=user.id, ) await mock_database.create_external_channel(external_channel) creative = domain.Creative( name='Summer Sale', text='Подписывайтесь на наш канал по ссылке: {link}', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative) placement_date = datetime.datetime.now(datetime.UTC) input_data = dto.CreatePlacementInput( target_channel_id=target_channel.id, external_channel_id=external_channel.id, creative_id=creative.id, placement_date=placement_date, cost=5000.0, comment='Утреннее размещение', ad_post_url='https://t.me/externalchannel/123', invite_link_type=domain.InviteLinkType.PUBLIC, ) result = await usecases.create_placement(input_data, user.id) assert result.target_channel_id == target_channel.id assert result.external_channel_id == external_channel.id assert result.creative_id == creative.id assert result.cost == 5000.0 assert result.comment == 'Утреннее размещение' assert result.invite_link_type == domain.InviteLinkType.PUBLIC assert result.invite_link.startswith('https://t.me/+public_link_') assert result.status == domain.PlacementStatus.PENDING # Ожидает публикации поста assert result.subscriptions_count == 0 # Проверяем, что счётчик закупов у креатива увеличился updated_creative = await mock_database.get_creative(user.id, creative.id) assert updated_creative is not None assert updated_creative.placements_count == 1 @pytest.mark.asyncio async def test_create_placement_with_approval_invite_link( 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=30000, user_id=user.id, ) await mock_database.create_external_channel(external_channel) creative = domain.Creative( name='Premium Creative', text='Эксклюзивный контент: {link}', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative) placement_date = datetime.datetime.now(datetime.UTC) input_data = dto.CreatePlacementInput( target_channel_id=target_channel.id, external_channel_id=external_channel.id, creative_id=creative.id, placement_date=placement_date, invite_link_type=domain.InviteLinkType.APPROVAL, ) result = await usecases.create_placement(input_data, user.id) assert result.invite_link_type == domain.InviteLinkType.APPROVAL assert 'approval_link_' in result.invite_link @pytest.mark.asyncio async def test_create_placement_invalid_target_channel(usecases: usecase.Usecase, mock_database: MockDatabase) -> None: """Создание закупа для несуществующего целевого канала вызывает ошибку.""" user = domain.User(telegram_id=123456, username='testuser') await mock_database.create_user(user) import uuid fake_target_id = uuid.uuid4() fake_external_id = uuid.uuid4() fake_creative_id = uuid.uuid4() input_data = dto.CreatePlacementInput( target_channel_id=fake_target_id, external_channel_id=fake_external_id, creative_id=fake_creative_id, placement_date=datetime.datetime.now(datetime.UTC), ) with pytest.raises(HTTPException) as exc_info: await usecases.create_placement(input_data, user.id) assert exc_info.value.status_code == 404 assert 'Target channel' in str(exc_info.value.detail) @pytest.mark.asyncio async def test_get_user_placements(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=20000, 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) # Создаём два закупа placement1 = 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), cost=3000.0, invite_link='https://t.me/+link1', invite_link_type=domain.InviteLinkType.PUBLIC, status=domain.PlacementStatus.ACTIVE, ) placement2 = 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) - datetime.timedelta(days=1), cost=4000.0, invite_link='https://t.me/+link2', invite_link_type=domain.InviteLinkType.PUBLIC, status=domain.PlacementStatus.ACTIVE, ) await mock_database.create_placement(placement1) await mock_database.create_placement(placement2) result = await usecases.get_placements(dto.GetPlacementsInput(user_id=user.id)) assert len(result.placements) == 2 # Проверяем сортировку по дате размещения (новые первые) assert result.placements[0].id == placement1.id @pytest.mark.asyncio async def test_get_placements_filtered_by_target_channel( usecases: usecase.Usecase, mock_database: MockDatabase ) -> None: """Получение закупов, отфильтрованных по целевому каналу.""" user = domain.User(telegram_id=123456, username='testuser') await mock_database.create_user(user) target1 = domain.TargetChannel( telegram_id=-1001234567890, title='Target 1', username='target1', user_id=user.id, status=domain.TargetChannelStatus.ACTIVE, ) target2 = domain.TargetChannel( telegram_id=-1001234567891, title='Target 2', username='target2', user_id=user.id, status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target1) await mock_database.upsert_target_channel(target2) external_channel = domain.ExternalChannel( telegram_id=-1009876543210, title='External Channel', username='externalchannel', description=None, subscribers_count=15000, user_id=user.id, ) await mock_database.create_external_channel(external_channel) creative1 = domain.Creative( name='Creative 1', text='Text 1', target_channel_id=target1.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) creative2 = domain.Creative( name='Creative 2', text='Text 2', target_channel_id=target2.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative1) await mock_database.create_creative(creative2) placement1 = domain.Placement( target_channel_id=target1.id, external_channel_id=external_channel.id, creative_id=creative1.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, ) placement2 = domain.Placement( target_channel_id=target2.id, external_channel_id=external_channel.id, creative_id=creative2.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, ) await mock_database.create_placement(placement1) await mock_database.create_placement(placement2) result = await usecases.get_placements(dto.GetPlacementsInput(user_id=user.id, target_channel_id=target1.id)) assert len(result.placements) == 1 assert result.placements[0].target_channel_id == target1.id @pytest.mark.asyncio async def test_get_placements_filtered_by_creative(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) creative1 = domain.Creative( name='Creative A', text='Вариант A', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) creative2 = domain.Creative( name='Creative B', text='Вариант B', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative1) await mock_database.create_creative(creative2) # Создаём закупы с разными креативами placement1 = domain.Placement( target_channel_id=target_channel.id, external_channel_id=external_channel.id, creative_id=creative1.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, ) placement2 = domain.Placement( target_channel_id=target_channel.id, external_channel_id=external_channel.id, creative_id=creative2.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, ) await mock_database.create_placement(placement1) await mock_database.create_placement(placement2) result = await usecases.get_placements(dto.GetPlacementsInput(user_id=user.id, creative_id=creative1.id)) assert len(result.placements) == 1 assert result.placements[0].creative_id == creative1.id # TODO: Implement archive_placement method # @pytest.mark.asyncio # async def test_archive_placement(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=18000, # 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, # ) # await mock_database.create_placement(placement) # # result = await usecases.archive_placement(placement.id, user.id) # # assert result.status == domain.PlacementStatus.ARCHIVED # # # Проверяем, что архивированный закуп не возвращается по умолчанию # placements_list = await usecases.get_placements(dto.GetPlacementsInput(user_id=user.id)) # assert len(placements_list.placements) == 0 # # # Проверяем, что архивированный закуп возвращается с флагом include_archived # placements_with_archived = await usecases.get_placements( # dto.GetPlacementsInput(user_id=user.id, include_archived=True) # ) # assert len(placements_with_archived.placements) == 1 @pytest.mark.asyncio async def test_update_placement(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=22000, 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) old_date = datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=1) 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=old_date, cost=3000.0, comment='Старый комментарий', invite_link='https://t.me/+link1', invite_link_type=domain.InviteLinkType.PUBLIC, status=domain.PlacementStatus.ACTIVE, ) await mock_database.create_placement(placement) new_date = datetime.datetime.now(datetime.UTC) update_input = dto.UpdatePlacementInput( placement_date=new_date, cost=3500.0, comment='Обновлённый комментарий', ad_post_url='https://t.me/externalchannel/456', ) result = await usecases.update_placement(placement.id, update_input, user.id) assert result.placement_date == new_date assert result.cost == 3500.0 assert result.comment == 'Обновлённый комментарий' assert result.ad_post_url == 'https://t.me/externalchannel/456' @pytest.mark.asyncio async def test_delete_placement(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, placements_count=1, ) 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, ) await mock_database.create_placement(placement) await usecases.delete_placement(dto.DeletePlacementInput(placement_id=placement.id, user_id=user.id)) # Проверяем, что закуп удалён placements_list = await usecases.get_placements(dto.GetPlacementsInput(user_id=user.id, include_archived=True)) assert len(placements_list.placements) == 0 # Проверяем, что счётчик закупов у креатива уменьшился updated_creative = await mock_database.get_creative(user.id, creative.id) assert updated_creative is not None assert updated_creative.placements_count == 0 @pytest.mark.asyncio async def test_multiple_placements_same_creative(usecases: usecase.Usecase, mock_database: MockDatabase) -> None: """Создание нескольких закупов с одним креативом - для A/B тестирования в разных каналах.""" 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) external1 = domain.ExternalChannel( telegram_id=-1009876543210, title='External 1', username='external1', description=None, subscribers_count=10000, user_id=user.id, ) external2 = domain.ExternalChannel( telegram_id=-1009876543211, title='External 2', username='external2', description=None, subscribers_count=20000, user_id=user.id, ) await mock_database.create_external_channel(external1) await mock_database.create_external_channel(external2) creative = domain.Creative( name='Universal Creative', text='Универсальный текст', target_channel_id=target_channel.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE, ) await mock_database.create_creative(creative) # Создаём два закупа с одним креативом в разных внешних каналах input1 = dto.CreatePlacementInput( target_channel_id=target_channel.id, external_channel_id=external1.id, creative_id=creative.id, placement_date=datetime.datetime.now(datetime.UTC), cost=2000.0, ) input2 = dto.CreatePlacementInput( target_channel_id=target_channel.id, external_channel_id=external2.id, creative_id=creative.id, placement_date=datetime.datetime.now(datetime.UTC), cost=4000.0, ) result1 = await usecases.create_placement(input1, user.id) result2 = await usecases.create_placement(input2, user.id) # Проверяем, что созданы два разных закупа assert result1.id != result2.id assert result1.invite_link != result2.invite_link # Проверяем, что счётчик креатива увеличился на 2 updated_creative = await mock_database.get_creative(user.id, creative.id) assert updated_creative is not None assert updated_creative.placements_count == 2 # Проверяем фильтрацию по креативу placements_for_creative = await usecases.get_placements( dto.GetPlacementsInput(user_id=user.id, creative_id=creative.id) ) assert len(placements_for_creative.placements) == 2