randomayzer/tests/winner-count-contract.test.ts

169 lines
5.4 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { NextRequest } from 'next/server';
import { GiveawayStore } from '../src/lib/giveaway-store';
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
import { ProviderRegistry } from '../src/providers/registry';
import { POST as drawPost } from '../src/app/api/giveaways/[id]/draw/route';
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
import { FilteredParticipant } from '../src/core/types/participant';
import { executeDeterministicDrawV1 } from '../src/core/randomizer/deterministic';
describe('Winner Count Contract & Draw Retry Invariants', () => {
beforeEach(() => {
GiveawayStore.setRepository(new MemoryGiveawayRepository());
ProviderRegistry.useMockVk();
});
const threeParticipants: FilteredParticipant[] = Array.from({ length: 3 }, (_, i) => ({
platformUserId: `user_${i + 1}`,
firstName: 'User',
lastName: `${i + 1}`,
source: 'LIKES',
liked: true,
commented: false,
commentsCount: 0,
reposted: false,
subscribed: true,
eligible: true,
exclusionReason: null,
}));
it('eligible=3, winners=3, reserve=0 -> success', async () => {
const gw = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-100_1',
post: {
platform: 'VK',
ownerId: '-100',
postId: '1',
sourceUrl: 'https://vk.com/wall-100_1',
title: 'Title',
likesCount: 3,
commentsCount: 0,
repostsCount: 0,
},
filterRules: DEFAULT_FILTER_RULES,
winnersCount: 3,
reserveWinnersCount: 0,
});
await GiveawayStore.updateParticipants(gw.id, threeParticipants);
const snapshot = await GiveawayStore.createAndLockSnapshot(gw.id, threeParticipants, DEFAULT_FILTER_RULES);
const result = executeDeterministicDrawV1({
giveawayId: gw.id,
snapshot,
totalLoadedCount: 3,
winnersCount: 3,
reserveWinnersCount: 0,
seed: 'test-seed-3-3-0',
});
expect(result.winners.length).toBe(3);
expect(result.reserveWinners.length).toBe(0);
});
it('eligible=3, winners=4, reserve=0 -> error (never silently reduces winners count)', async () => {
const gw = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-100_2',
post: {
platform: 'VK',
ownerId: '-100',
postId: '2',
sourceUrl: 'https://vk.com/wall-100_2',
title: 'Title',
likesCount: 3,
commentsCount: 0,
repostsCount: 0,
},
filterRules: DEFAULT_FILTER_RULES,
winnersCount: 4,
reserveWinnersCount: 0,
});
await GiveawayStore.updateParticipants(gw.id, threeParticipants);
const snapshot = await GiveawayStore.createAndLockSnapshot(gw.id, threeParticipants, DEFAULT_FILTER_RULES);
expect(() =>
executeDeterministicDrawV1({
giveawayId: gw.id,
snapshot,
totalLoadedCount: 3,
winnersCount: 4,
reserveWinnersCount: 0,
seed: 'test-seed-3-4-0',
})
).toThrow(/exceeds eligible participants count/i);
});
it('eligible=3, winners=3, reserve=3 -> error (total 6 exceeds pool of 3)', async () => {
const gw = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-100_3',
post: {
platform: 'VK',
ownerId: '-100',
postId: '3',
sourceUrl: 'https://vk.com/wall-100_3',
title: 'Title',
likesCount: 3,
commentsCount: 0,
repostsCount: 0,
},
filterRules: DEFAULT_FILTER_RULES,
winnersCount: 3,
reserveWinnersCount: 3,
});
await GiveawayStore.updateParticipants(gw.id, threeParticipants);
const snapshot = await GiveawayStore.createAndLockSnapshot(gw.id, threeParticipants, DEFAULT_FILTER_RULES);
expect(() =>
executeDeterministicDrawV1({
giveawayId: gw.id,
snapshot,
totalLoadedCount: 3,
winnersCount: 3,
reserveWinnersCount: 3,
seed: 'test-seed-3-3-3',
})
).toThrow(/exceeds eligible participants count/i);
});
it('repeat draw on already DRAWN giveaway returns 409 DRAW_ALREADY_COMPLETED', async () => {
const gw = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-100_4',
post: {
platform: 'VK',
ownerId: '-100',
postId: '4',
sourceUrl: 'https://vk.com/wall-100_4',
title: 'Title',
likesCount: 3,
commentsCount: 0,
repostsCount: 0,
},
filterRules: DEFAULT_FILTER_RULES,
});
await GiveawayStore.updateParticipants(gw.id, threeParticipants);
await GiveawayStore.createAndLockSnapshot(gw.id, threeParticipants, DEFAULT_FILTER_RULES);
// First draw
const req1 = new NextRequest(`http://localhost/api/giveaways/${gw.id}/draw`, {
method: 'POST',
body: JSON.stringify({ winnersCount: 1, reserveWinnersCount: 0 }),
});
const res1 = await drawPost(req1, { params: { id: gw.id } });
expect(res1.status).toBe(200);
// Second draw -> MUST return 409 DRAW_ALREADY_COMPLETED
const req2 = new NextRequest(`http://localhost/api/giveaways/${gw.id}/draw`, {
method: 'POST',
body: JSON.stringify({ winnersCount: 1, reserveWinnersCount: 0 }),
});
const res2 = await drawPost(req2, { params: { id: gw.id } });
expect(res2.status).toBe(409);
const body = await res2.json();
expect(body.error?.code).toBe('DRAW_ALREADY_COMPLETED');
});
});