import asyncio import pytest from sqlalchemy import select, func from sqlalchemy.exc import IntegrityError from src.core.database import async_session_maker from src.core.models import User, Lottery, Participation, Winner from src.core.services import UserService, LotteryService, ParticipationService from src.core.registration_services import AccountService, WinnerNotificationService from src.handlers.account_services import AccountParticipationService async def seed(prizes=2): async with async_session_maker() as session: user = await UserService.get_or_create_user(session, 101, first_name="Client") user.club_card_number = "1234" user.verification_code = "AABBCCDD" user.is_registered = True await session.commit() lottery = await LotteryService.create_lottery(session, "Prizes", "Draw", ["Prize"] * prizes, user.id) return user.id, lottery.id async def independent(call, *args): async with async_session_maker() as session: return await call(session, *args) async def test_concurrent_user_upsert_preserves_one_user_and_metadata(): users = await asyncio.gather(*(independent(UserService.get_or_create_user, 102, "client", "Name") for _ in range(12))) assert len({user.id for user in users}) == 1 user = await independent(UserService.get_or_create_user, 102) assert user.first_name == "Name" async def test_two_cashiers_add_same_ticket_once(): _, lottery_id = await seed() results = await asyncio.gather(*(independent(AccountParticipationService.add_account_to_lottery, lottery_id, "11-22-33-44-55-66-77") for _ in range(12))) assert sum(result["success"] for result in results) == 1 assert await independent(ParticipationService.get_participants_count, lottery_id) == 1 async def test_two_admins_add_same_user_once(): user_id, lottery_id = await seed() results = await asyncio.gather(*(independent(ParticipationService.add_participant, lottery_id, user_id) for _ in range(8))) assert sum(results) == 1 async def test_two_admins_conduct_one_draw_and_keep_ticket_numbers(): _, lottery_id = await seed() for account in ["11-22-33-44-55-66-77", "22-33-44-55-66-77-88"]: await independent(AccountService.create_account, "1234", account) await independent(AccountParticipationService.add_account_to_lottery, lottery_id, account) results = await asyncio.gather(*(independent(LotteryService.conduct_draw, lottery_id) for _ in range(6))) assert sum(bool(result) for result in results) == 1 winners = await independent(LotteryService.get_winners, lottery_id) assert {w.account_number for w in winners} == {"11-22-33-44-55-66-77", "22-33-44-55-66-77-88"} async with async_session_maker() as session: lottery = await session.get(Lottery, lottery_id) assert lottery.is_completed and not lottery.is_active and len(lottery.draw_results) == 2 async def test_two_cashiers_claim_one_prize_once(): user_id, lottery_id = await seed(prizes=1) await independent(ParticipationService.add_participant, lottery_id, user_id) await independent(LotteryService.conduct_draw, lottery_id) results = await asyncio.gather(*(independent(WinnerNotificationService.verify_winner, "AABBCCDD", lottery_id) for _ in range(8))) assert sum(result is not None for result in results) == 1 assert next(result for result in results if result is not None).claimed_at is not None async def test_multiple_winning_accounts_can_be_claimed_separately(): _, lottery_id = await seed() for account in ["11-22-33-44-55-66-77", "22-33-44-55-66-77-88"]: await independent(AccountService.create_account, "1234", account) await independent(AccountParticipationService.add_account_to_lottery, lottery_id, account) await independent(LotteryService.conduct_draw, lottery_id) one = await independent(WinnerNotificationService.verify_winner, "AABBCCDD", lottery_id) two = await independent(WinnerNotificationService.verify_winner, "AABBCCDD", lottery_id) assert one.id != two.id assert await independent(WinnerNotificationService.verify_winner, "AABBCCDD", lottery_id) is None async def test_closed_draw_rejects_all_participant_mutations(): user_id, lottery_id = await seed(1) await independent(ParticipationService.add_participant, lottery_id, user_id) await independent(LotteryService.conduct_draw, lottery_id) result = await independent(AccountParticipationService.add_account_to_lottery, lottery_id, "11-22-33-44-55-66-77") assert not result["success"] assert not await independent(ParticipationService.remove_participant, lottery_id, user_id) assert not await independent(ParticipationService.add_participant, lottery_id, user_id) async def test_duplicate_account_error_does_not_poison_bulk_session(): await seed() async with async_session_maker() as session: await AccountService.create_account(session, "1234", "11223344556677") with pytest.raises(ValueError): await AccountService.create_account(session, "1234", "11223344556677") account = await AccountService.create_account(session, "1234", "22334455667788") assert account.account_number == "22-33-44-55-66-77-88" async def test_database_unique_constraints_protect_external_writers(): user_id, lottery_id = await seed() async with async_session_maker() as session: session.add_all([Winner(lottery_id=lottery_id, user_id=user_id, place=1), Winner(lottery_id=lottery_id, user_id=user_id, place=1)]) with pytest.raises(IntegrityError): await session.commit() await session.rollback() assert await session.scalar(select(func.count()).select_from(Winner)) == 0 async def test_manual_winner_json_is_persisted_and_participant_is_required(): user_id, lottery_id = await seed() assert not await independent(LotteryService.set_manual_winner, lottery_id, 1, 101) await independent(ParticipationService.add_participant, lottery_id, user_id) assert await independent(LotteryService.set_manual_winner, lottery_id, 1, 101) result = await independent(LotteryService.conduct_draw, lottery_id) assert result[1]["is_manual"] and result[1]["user"].id == user_id