randomayzer/tests/randomizer.test.ts

202 lines
6.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { executeDeterministicDraw, verifyDrawResult } from '../src/core/randomizer/deterministic';
import { computeParticipantsSnapshotHash, generateRandomSeed } from '../src/core/randomizer/hasher';
import { FilteredParticipant } from '../src/core/types/participant';
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
function createMockEligibleParticipants(count: number): FilteredParticipant[] {
return Array.from({ length: count }, (_, i) => ({
platformUserId: `${1000 + i}`,
firstName: `User${i + 1}`,
lastName: `Surname${i + 1}`,
username: `user_${i + 1}`,
avatarUrl: `https://example.com/avatar/${i + 1}.jpg`,
source: 'LIKES',
liked: true,
commented: true,
commentsCount: 1,
reposted: true,
subscribed: true,
eligible: true,
exclusionReason: null,
}));
}
describe('Deterministic Randomizer & Provably Fair Engine', () => {
it('should generate identical winners given the same participants snapshot and seed', () => {
const participants = createMockEligibleParticipants(50);
const seed = 'test-secret-seed-2026';
const draw1 = executeDeterministicDraw({
giveawayId: 'gw-1',
eligibleParticipants: participants,
totalLoadedCount: 50,
winnersCount: 3,
reserveWinnersCount: 2,
seed,
filterRules: DEFAULT_FILTER_RULES,
});
const draw2 = executeDeterministicDraw({
giveawayId: 'gw-1',
eligibleParticipants: participants,
totalLoadedCount: 50,
winnersCount: 3,
reserveWinnersCount: 2,
seed,
filterRules: DEFAULT_FILTER_RULES,
});
expect(draw1.participantsSnapshotHash).toBe(draw2.participantsSnapshotHash);
expect(draw1.winners.map(w => w.participant.platformUserId)).toEqual(
draw2.winners.map(w => w.participant.platformUserId)
);
expect(draw1.reserveWinners.map(w => w.participant.platformUserId)).toEqual(
draw2.reserveWinners.map(w => w.participant.platformUserId)
);
expect(draw1.verificationSignature).toBe(draw2.verificationSignature);
});
it('should produce different winners when seed changes', () => {
const participants = createMockEligibleParticipants(100);
const seedA = 'seed-alpha-123';
const seedB = 'seed-beta-456';
const drawA = executeDeterministicDraw({
giveawayId: 'gw-a',
eligibleParticipants: participants,
totalLoadedCount: 100,
winnersCount: 5,
reserveWinnersCount: 2,
seed: seedA,
filterRules: DEFAULT_FILTER_RULES,
});
const drawB = executeDeterministicDraw({
giveawayId: 'gw-b',
eligibleParticipants: participants,
totalLoadedCount: 100,
winnersCount: 5,
reserveWinnersCount: 2,
seed: seedB,
filterRules: DEFAULT_FILTER_RULES,
});
const winnersA = drawA.winners.map(w => w.participant.platformUserId);
const winnersB = drawB.winners.map(w => w.participant.platformUserId);
expect(winnersA).not.toEqual(winnersB);
});
it('should guarantee no duplicates between winners and reserve winners', () => {
const participants = createMockEligibleParticipants(30);
const seed = generateRandomSeed();
const draw = executeDeterministicDraw({
giveawayId: 'gw-uniq',
eligibleParticipants: participants,
totalLoadedCount: 30,
winnersCount: 5,
reserveWinnersCount: 5,
seed,
filterRules: DEFAULT_FILTER_RULES,
});
const allChosenIds = [
...draw.winners.map(w => w.participant.platformUserId),
...draw.reserveWinners.map(w => w.participant.platformUserId),
];
const uniqueIds = new Set(allChosenIds);
expect(uniqueIds.size).toBe(10);
});
it('should handle cases where pool size is smaller than requested winners', () => {
const participants = createMockEligibleParticipants(2);
const seed = 'small-pool-seed';
const draw = executeDeterministicDraw({
giveawayId: 'gw-small',
eligibleParticipants: participants,
totalLoadedCount: 2,
winnersCount: 5,
reserveWinnersCount: 3,
seed,
filterRules: DEFAULT_FILTER_RULES,
});
expect(draw.winners.length).toBe(2);
expect(draw.reserveWinners.length).toBe(0);
});
it('should throw when attempting draw with 0 eligible participants', () => {
expect(() => {
executeDeterministicDraw({
giveawayId: 'gw-empty',
eligibleParticipants: [],
totalLoadedCount: 0,
winnersCount: 1,
reserveWinnersCount: 0,
seed: 'empty-seed',
filterRules: DEFAULT_FILTER_RULES,
});
}).toThrow(/Cannot conduct draw with 0 eligible participants/);
});
it('should be independent of input participant ordering (canonical sorting)', () => {
const p1 = createMockEligibleParticipants(20);
const p2 = [...p1].reverse(); // reversed order
const seed = 'sort-order-invariant-seed';
const draw1 = executeDeterministicDraw({
giveawayId: 'gw-sort',
eligibleParticipants: p1,
totalLoadedCount: 20,
winnersCount: 3,
reserveWinnersCount: 1,
seed,
filterRules: DEFAULT_FILTER_RULES,
});
const draw2 = executeDeterministicDraw({
giveawayId: 'gw-sort',
eligibleParticipants: p2,
totalLoadedCount: 20,
winnersCount: 3,
reserveWinnersCount: 1,
seed,
filterRules: DEFAULT_FILTER_RULES,
});
expect(draw1.participantsSnapshotHash).toBe(draw2.participantsSnapshotHash);
expect(draw1.winners.map(w => w.participant.platformUserId)).toEqual(
draw2.winners.map(w => w.participant.platformUserId)
);
});
it('should allow third-party verification through verifyDrawResult', () => {
const participants = createMockEligibleParticipants(25);
const seed = 'audit-verification-seed';
const originalDraw = executeDeterministicDraw({
giveawayId: 'gw-audit',
eligibleParticipants: participants,
totalLoadedCount: 25,
winnersCount: 2,
reserveWinnersCount: 2,
seed,
filterRules: DEFAULT_FILTER_RULES,
});
const verification = verifyDrawResult(participants, seed, 2, 2);
expect(verification.snapshotHash).toBe(originalDraw.participantsSnapshotHash);
expect(verification.winners.map(w => w.participant.platformUserId)).toEqual(
originalDraw.winners.map(w => w.participant.platformUserId)
);
expect(verification.reserveWinners.map(w => w.participant.platformUserId)).toEqual(
originalDraw.reserveWinners.map(w => w.participant.platformUserId)
);
});
});