randomayzer/tests/public-verification.test.ts

182 lines
7.8 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { NextRequest } from 'next/server';
import { createHash } from 'crypto';
import { GET as publicGiveawayGet } from '../src/app/api/giveaways/[id]/public/route';
import { GET as privateGiveawayGet } from '../src/app/api/giveaways/[id]/route';
import { POST as snapshotPost } from '../src/app/api/giveaways/[id]/snapshot/route';
import { POST as drawPost } from '../src/app/api/giveaways/[id]/draw/route';
import { GiveawayStore } from '../src/lib/giveaway-store';
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
import { FilteredParticipant } from '../src/core/types/participant';
import { expensiveApiRateLimiter } from '../src/lib/rate-limiter';
describe('Task 06: Public Giveaway Verification Endpoint (GET /api/giveaways/[id]/public)', () => {
const organizerUser = { id: 'usr_pub_verify_org', vkUserId: '555444' };
let sessionCookie: string;
const testParticipants: FilteredParticipant[] = Array.from({ length: 25 }, (_, i) => ({
platformUserId: `${4000 + i}`,
firstName: `User${i}`,
lastName: `Public${i}`,
source: 'LIKES',
liked: true,
commented: false,
commentsCount: 0,
reposted: false,
subscribed: true,
eligible: true,
exclusionReason: null,
}));
beforeEach(async () => {
GiveawayStore.setRepository(new MemoryGiveawayRepository());
defaultSessionStore.clear();
expensiveApiRateLimiter.reset();
const sessionId = await defaultSessionStore.createSession(organizerUser);
sessionCookie = `${SESSION_COOKIE_NAME}=${sessionId}`;
});
async function createLockedGiveaway() {
const gw = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-55667788_999',
post: {
platform: 'VK',
ownerId: '-55667788',
postId: '999',
sourceUrl: 'https://vk.com/wall-55667788_999',
title: 'Public Verification Test Post',
text: 'Provably fair giveaway description',
likesCount: 25,
commentsCount: 0,
repostsCount: 0,
},
filterRules: DEFAULT_FILTER_RULES,
organizerId: organizerUser.id,
});
await GiveawayStore.updateParticipants(gw.id, testParticipants);
const snapReq = new NextRequest(`http://localhost:3000/api/giveaways/${gw.id}/snapshot`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Cookie: sessionCookie },
body: JSON.stringify({ filterRules: DEFAULT_FILTER_RULES }),
});
const snapRes = await snapshotPost(snapReq, { params: { id: gw.id } });
const snapData = await snapRes.json();
return { giveawayId: gw.id, seedCommitment: snapData.seedCommitment, snapshot: snapData.snapshot };
}
// ─── Test 1: Anonymous Access Before Draw (Masked Seed, Exposed Commitment) ───
it('anonymous visitor before draw sees seedCommitment and snapshot hash, but seed is strictly null', async () => {
const { giveawayId, seedCommitment } = await createLockedGiveaway();
const req = new NextRequest(`http://localhost:3000/api/giveaways/${giveawayId}/public`);
const res = await publicGiveawayGet(req, { params: { id: giveawayId } });
expect(res.status).toBe(200);
const data = await res.json();
expect(data.success).toBe(true);
const gw = data.giveaway;
expect(gw.id).toBe(giveawayId);
expect(gw.status).toBe('SNAPSHOT_LOCKED');
expect(gw.title).toBe('Public Verification Test Post');
expect(gw.seedCommitment).toBe(seedCommitment);
expect(gw.seed).toBeNull(); // Strictly masked before draw!
expect(gw.latestSnapshot.participantsSnapshotHash).toBeDefined();
expect(gw.latestSnapshot.conditionsHash).toBeDefined();
expect(gw.drawResult).toBeNull();
// Verify raw PII protection
expect((gw as any).participants).toBeUndefined();
expect((gw as any).eligibleParticipants).toBeUndefined();
expect((gw as any).organizerId).toBeUndefined();
});
// ─── Test 2: Anonymous Access After Draw (Revealed Seed, Proof & Winners) ─────
it('anonymous visitor after draw sees revealed seed, deterministic proof, and winner public profiles', async () => {
const { giveawayId, seedCommitment } = await createLockedGiveaway();
// Execute draw as organizer
const drawReq = new NextRequest(`http://localhost:3000/api/giveaways/${giveawayId}/draw`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Cookie: sessionCookie },
body: JSON.stringify({ winnersCount: 2, reserveWinnersCount: 1 }),
});
const drawRes = await drawPost(drawReq, { params: { id: giveawayId } });
expect(drawRes.status).toBe(200);
// Fetch public giveaway
const req = new NextRequest(`http://localhost:3000/api/giveaways/${giveawayId}/public`);
const res = await publicGiveawayGet(req, { params: { id: giveawayId } });
expect(res.status).toBe(200);
const data = await res.json();
const gw = data.giveaway;
expect(gw.status).toBe('DRAWN');
expect(gw.seedCommitment).toBe(seedCommitment);
expect(gw.seed).toBeDefined();
expect(typeof gw.seed).toBe('string');
// SHA-256 of revealed seed MUST equal the pre-committed hash
const computedHash = createHash('sha256').update(gw.seed).digest('hex');
expect(computedHash).toBe(seedCommitment);
// Draw result details
expect(gw.drawResult).toBeDefined();
expect(gw.drawResult.algorithmVersion).toBe('HMAC_SHA256_FY_V1');
expect(gw.drawResult.deterministicProofHash).toMatch(/^[a-f0-9]{64}$/);
expect(gw.drawResult.auditEventHash).toMatch(/^[a-f0-9]{64}$/);
expect(gw.drawResult.winners).toHaveLength(2);
expect(gw.drawResult.reserveWinners).toHaveLength(1);
// Winner profiles contain only safe public fields
const winner1 = gw.drawResult.winners[0];
expect(winner1.position).toBe(1);
expect(winner1.participant.platformUserId).toBeDefined();
expect(winner1.participant.firstName).toBeDefined();
expect(winner1.participant.lastName).toBeDefined();
expect((winner1.participant as any).encryptedToken).toBeUndefined();
// Raw participant list is still omitted
expect((gw as any).participants).toBeUndefined();
});
// ─── Test 3: Private Giveaway Detail Route Remains Protected (401) ───────────
it('anonymous request to private GET /api/giveaways/[id] returns 401 Unauthorized', async () => {
const { giveawayId } = await createLockedGiveaway();
const req = new NextRequest(`http://localhost:3000/api/giveaways/${giveawayId}`);
const res = await privateGiveawayGet(req, { params: { id: giveawayId } });
expect(res.status).toBe(401);
});
// ─── Test 4: Non-Existent Giveaway Returns 404 ────────────────────────────────
it('request for non-existent giveaway returns 404 Not Found', async () => {
const req = new NextRequest('http://localhost:3000/api/giveaways/non_existent_123/public');
const res = await publicGiveawayGet(req, { params: { id: 'non_existent_123' } });
expect(res.status).toBe(404);
});
// ─── Test 5: Rate Limiting on Public Endpoint ─────────────────────────────────
it('public endpoint enforces rate limiting per client IP', async () => {
const { giveawayId } = await createLockedGiveaway();
// Exhaust expensive rate limiter (15 requests)
for (let i = 0; i < 15; i++) {
expensiveApiRateLimiter.check(`giveaway-public-get:direct-client:${giveawayId}`);
}
const req = new NextRequest(`http://localhost:3000/api/giveaways/${giveawayId}/public`);
const res = await publicGiveawayGet(req, { params: { id: giveawayId } });
expect(res.status).toBe(429);
});
});