import { describe, it, expect } from 'vitest'; import { executeDeterministicDrawV1, verifyDrawResult, ALGORITHM_VERSION_V1 } from '../src/core/randomizer/deterministic'; import { generateCryptoSecureSeed } from '../src/core/randomizer/hasher'; import { computeParticipantsSnapshotHash, computeConditionsHash } from '../src/core/randomizer/canonical'; import { FilteredParticipant } from '../src/core/types/participant'; import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway'; import { ParticipantSnapshotData } from '../src/core/types/audit'; function createMockSnapshot(count: number): ParticipantSnapshotData { const eligible: FilteredParticipant[] = 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: false, subscribed: true, eligible: true, exclusionReason: null, })); return { id: 'snap-test-1', giveawayId: 'gw-test-1', version: 1, createdAt: new Date().toISOString(), eligibleParticipants: eligible, filterRulesSnapshot: { ...DEFAULT_FILTER_RULES }, participantCount: count, participantsSnapshotHash: computeParticipantsSnapshotHash(eligible), conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES), }; } describe('Deterministic Randomizer V1 (HMAC_SHA256_FY_V1)', () => { it('should use CSPRNG crypto.randomBytes for seed generation (no Math.random)', () => { const seed1 = generateCryptoSecureSeed(); const seed2 = generateCryptoSecureSeed(); expect(seed1).toHaveLength(32); // 16 bytes in hex = 32 chars expect(seed2).toHaveLength(32); expect(seed1).not.toBe(seed2); expect(/^[0-9a-f]{32}$/.test(seed1)).toBe(true); }); it('should guarantee deterministic replay given the same snapshot and seed', () => { const snapshot = createMockSnapshot(50); const seed = 'test-secret-seed-2026'; const draw1 = executeDeterministicDrawV1({ giveawayId: 'gw-1', snapshot, totalLoadedCount: 50, winnersCount: 3, reserveWinnersCount: 2, seed, filterRules: DEFAULT_FILTER_RULES, }); const draw2 = executeDeterministicDrawV1({ giveawayId: 'gw-1', snapshot, totalLoadedCount: 50, winnersCount: 3, reserveWinnersCount: 2, seed, filterRules: DEFAULT_FILTER_RULES, }); expect(draw1.algorithmVersion).toBe(ALGORITHM_VERSION_V1); expect(draw1.participantsSnapshotHash).toBe(draw2.participantsSnapshotHash); expect(draw1.deterministicProofHash).toBe(draw2.deterministicProofHash); expect(draw1.winnerIds).toEqual(draw2.winnerIds); expect(draw1.reserveWinnerIds).toEqual(draw2.reserveWinnerIds); 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) ); }); it('should produce different winners when seed changes', () => { const snapshot = createMockSnapshot(100); const seedA = 'seed-alpha-123'; const seedB = 'seed-beta-456'; const drawA = executeDeterministicDrawV1({ giveawayId: 'gw-a', snapshot, totalLoadedCount: 100, winnersCount: 5, reserveWinnersCount: 2, seed: seedA, filterRules: DEFAULT_FILTER_RULES, }); const drawB = executeDeterministicDrawV1({ giveawayId: 'gw-b', snapshot, totalLoadedCount: 100, winnersCount: 5, reserveWinnersCount: 2, seed: seedB, filterRules: DEFAULT_FILTER_RULES, }); expect(drawA.winnerIds).not.toEqual(drawB.winnerIds); }); it('should guarantee no duplicates between winners and reserve winners', () => { const snapshot = createMockSnapshot(30); const seed = generateCryptoSecureSeed(); const draw = executeDeterministicDrawV1({ giveawayId: 'gw-uniq', snapshot, totalLoadedCount: 30, winnersCount: 5, reserveWinnersCount: 5, seed, filterRules: DEFAULT_FILTER_RULES, }); const allChosenIds = [...draw.winnerIds, ...draw.reserveWinnerIds]; const uniqueIds = new Set(allChosenIds); expect(uniqueIds.size).toBe(10); }); it('should strictly enforce winner count contract (never silently reduce winners count)', () => { const snapshot = createMockSnapshot(2); const seed = 'small-pool-seed'; // Requesting 5 winners on 2 eligible participants must throw expect(() => executeDeterministicDrawV1({ giveawayId: 'gw-small', snapshot, totalLoadedCount: 2, winnersCount: 5, reserveWinnersCount: 3, seed, filterRules: DEFAULT_FILTER_RULES, }) ).toThrow(/exceeds eligible participants count/i); // Requesting exactly 2 winners on 2 eligible participants succeeds const validDraw = executeDeterministicDrawV1({ giveawayId: 'gw-small-valid', snapshot, totalLoadedCount: 2, winnersCount: 2, reserveWinnersCount: 0, seed, filterRules: DEFAULT_FILTER_RULES, }); expect(validDraw.winners.length).toBe(2); expect(validDraw.reserveWinners.length).toBe(0); }); it('should allow third-party audit replay verification via verifyDrawResult', () => { const snapshot = createMockSnapshot(25); const seed = 'audit-verification-seed'; const originalDraw = executeDeterministicDrawV1({ giveawayId: 'gw-audit', snapshot, totalLoadedCount: 25, winnersCount: 2, reserveWinnersCount: 2, seed, filterRules: DEFAULT_FILTER_RULES, }); const verification = verifyDrawResult({ giveawayId: 'gw-audit', drawId: originalDraw.drawId, drawnAt: originalDraw.drawnAt, snapshot, seed, claimedWinnersCount: 2, claimedReserveCount: 2, claimedWinnerIds: originalDraw.winnerIds, claimedReserveWinnerIds: originalDraw.reserveWinnerIds, claimedDeterministicProofHash: originalDraw.deterministicProofHash, claimedAuditEventHash: originalDraw.auditEventHash, algorithmVersion: ALGORITHM_VERSION_V1, }); expect(verification.verified).toBe(true); expect(verification.expectedWinnerIds).toEqual(originalDraw.winnerIds); expect(verification.expectedReserveWinnerIds).toEqual(originalDraw.reserveWinnerIds); expect(verification.expectedWinners.map(w => w.participant.platformUserId)).toEqual( originalDraw.winners.map(w => w.participant.platformUserId) ); }); });