138 lines
4.7 KiB
TypeScript
138 lines
4.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
|
|
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
|
|
import { FilteredParticipant } from '../src/core/types/participant';
|
|
import { executeDeterministicDrawV1 } from '../src/core/randomizer/deterministic';
|
|
import { ConflictError } from '../src/core/errors/http-errors';
|
|
|
|
describe('100-Draw Concurrency Regression & Mixed Race', () => {
|
|
const participants: FilteredParticipant[] = Array.from({ length: 50 }, (_, i) => ({
|
|
platformUserId: `user_${1000 + i}`,
|
|
firstName: 'User',
|
|
lastName: `${i + 1}`,
|
|
source: 'LIKES',
|
|
liked: true,
|
|
commented: false,
|
|
commentsCount: 0,
|
|
reposted: false,
|
|
subscribed: true,
|
|
eligible: true,
|
|
exclusionReason: null,
|
|
}));
|
|
|
|
it('100 concurrent draw attempts result in exactly 1 success and 99 conflicts with 1 DrawResult', async () => {
|
|
const repo = new MemoryGiveawayRepository();
|
|
|
|
const gw = await repo.createGiveaway({
|
|
sourceUrl: 'https://vk.com/wall-100_100',
|
|
post: {
|
|
platform: 'VK',
|
|
ownerId: '-100',
|
|
postId: '100',
|
|
sourceUrl: 'https://vk.com/wall-100_100',
|
|
title: '100 Concurrency Test',
|
|
likesCount: 50,
|
|
commentsCount: 0,
|
|
repostsCount: 0,
|
|
},
|
|
filterRules: DEFAULT_FILTER_RULES,
|
|
winnersCount: 3,
|
|
reserveWinnersCount: 1,
|
|
organizerId: 'usr_conc_100',
|
|
});
|
|
|
|
const snapshot = await repo.createAndLockSnapshot(gw.id, participants, DEFAULT_FILTER_RULES);
|
|
|
|
// Launch 100 concurrent draw requests
|
|
const drawPromises = Array.from({ length: 100 }, async (_, index) => {
|
|
try {
|
|
const seed = `seed-concurrent-100-${index}`;
|
|
const drawResult = executeDeterministicDrawV1({
|
|
giveawayId: gw.id,
|
|
snapshot,
|
|
totalLoadedCount: 50,
|
|
winnersCount: 3,
|
|
reserveWinnersCount: 1,
|
|
seed,
|
|
});
|
|
|
|
const saved = await repo.saveDrawResultAndAudit(gw.id, snapshot.id, drawResult);
|
|
return { status: 200, result: saved };
|
|
} catch (err: any) {
|
|
if (
|
|
err instanceof ConflictError ||
|
|
err?.message?.includes('already been drawn') ||
|
|
err?.message?.includes('SNAPSHOT_LOCKED')
|
|
) {
|
|
return { status: 409, error: err.message };
|
|
}
|
|
return { status: 500, error: err.message };
|
|
}
|
|
});
|
|
|
|
const results = await Promise.all(drawPromises);
|
|
|
|
const successCount = results.filter(r => r.status === 200).length;
|
|
const conflictCount = results.filter(r => r.status === 409).length;
|
|
const errorCount = results.filter(r => r.status === 500).length;
|
|
|
|
expect(successCount).toBe(1);
|
|
expect(conflictCount).toBe(99);
|
|
expect(errorCount).toBe(0);
|
|
|
|
const finalized = await repo.getGiveawayById(gw.id);
|
|
expect(finalized?.status).toBe('DRAWN');
|
|
expect(finalized?.drawResult).toBeDefined();
|
|
expect(finalized?.drawResult?.winners.length).toBe(3);
|
|
expect(finalized?.drawResult?.reserveWinners.length).toBe(1);
|
|
});
|
|
|
|
it('mixed race between participant mutation, snapshot locking, and draw preserves terminal integrity', async () => {
|
|
const repo = new MemoryGiveawayRepository();
|
|
|
|
const gw = await repo.createGiveaway({
|
|
sourceUrl: 'https://vk.com/wall-100_200',
|
|
post: {
|
|
platform: 'VK',
|
|
ownerId: '-100',
|
|
postId: '200',
|
|
sourceUrl: 'https://vk.com/wall-100_200',
|
|
title: 'Mixed Race Test',
|
|
likesCount: 50,
|
|
commentsCount: 0,
|
|
repostsCount: 0,
|
|
},
|
|
filterRules: DEFAULT_FILTER_RULES,
|
|
winnersCount: 1,
|
|
reserveWinnersCount: 0,
|
|
organizerId: 'usr_conc_100',
|
|
});
|
|
|
|
await repo.saveParticipants(gw.id, participants);
|
|
|
|
// Launch mixed simultaneous actions
|
|
const actions = [
|
|
repo.createAndLockSnapshot(gw.id, participants, DEFAULT_FILTER_RULES).catch(e => ({ error: e.message })),
|
|
repo.saveParticipants(gw.id, participants.slice(0, 10)).catch(e => ({ error: e.message })),
|
|
(async () => {
|
|
const snap = await repo.getLatestSnapshot(gw.id);
|
|
if (!snap) return { skipped: true };
|
|
const drawResult = executeDeterministicDrawV1({
|
|
giveawayId: gw.id,
|
|
snapshot: snap,
|
|
totalLoadedCount: 50,
|
|
winnersCount: 1,
|
|
reserveWinnersCount: 0,
|
|
seed: 'seed-mixed',
|
|
});
|
|
return repo.saveDrawResultAndAudit(gw.id, snap.id, drawResult).catch(e => ({ error: e.message }));
|
|
})(),
|
|
];
|
|
|
|
await Promise.allSettled(actions);
|
|
|
|
const finalState = await repo.getGiveawayById(gw.id);
|
|
expect(finalState).not.toBeNull();
|
|
expect(['READY', 'SNAPSHOT_LOCKED', 'DRAWN']).toContain(finalState?.status);
|
|
});
|
|
});
|