randomayzer/tests/auth-guard.test.ts

266 lines
9.8 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 { POST as giveawaysPost } from '../src/app/api/giveaways/route';
import { GET as giveawayDetailGet } from '../src/app/api/giveaways/[id]/route';
import { POST as participantsPost } from '../src/app/api/giveaways/[id]/participants/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 { GET as verifyGet } from '../src/app/api/giveaways/[id]/verify/route';
import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
describe('Phase 2.2.1 Giveaway Ownership & AuthZ Guard Security Suite', () => {
let memoryRepo: MemoryGiveawayRepository;
const ownerUser = { id: 'usr_organizer_1', vkUserId: '111111', firstName: 'Alice' };
const intruderUser = { id: 'usr_intruder_2', vkUserId: '222222', firstName: 'Eve' };
let ownerSessionId: string;
let intruderSessionId: string;
beforeEach(async () => {
memoryRepo = new MemoryGiveawayRepository();
GiveawayStore.setRepository(memoryRepo);
defaultSessionStore.clear();
ownerSessionId = await defaultSessionStore.createSession(ownerUser);
intruderSessionId = await defaultSessionStore.createSession(intruderUser);
});
const validPostData = {
sourceUrl: 'https://vk.com/wall-1_100',
post: {
platform: 'VK' as const,
ownerId: '-1',
postId: '100',
sourceUrl: 'https://vk.com/wall-1_100',
title: 'Mega Giveaway',
text: 'Mega Giveaway Description',
likesCount: 10,
commentsCount: 5,
repostsCount: 0,
},
filterRules: {
requireLike: true,
requireComment: false,
requireRepost: false,
requireSubscription: false,
excludeAdmins: false,
excludeBlacklistedIds: [],
excludeDuplicateComments: true,
minEligibleParticipants: 1,
},
winnersCount: 1,
reserveWinnersCount: 0,
};
it('anonymous POST /api/giveaways returns 401 Unauthorized', async () => {
const req = new NextRequest('http://localhost:3000/api/giveaways', {
method: 'POST',
body: JSON.stringify(validPostData),
headers: { 'Content-Type': 'application/json' },
});
const res = await giveawaysPost(req);
expect(res.status).toBe(401);
const body = await res.json();
expect(body.error?.message).toMatch(/authentication required/i);
});
it('authenticated POST /api/giveaways binds organizerId strictly from server session', async () => {
const req = new NextRequest('http://localhost:3000/api/giveaways', {
method: 'POST',
body: JSON.stringify({
...validPostData,
organizerId: 'usr_fake_spoofed_id', // Client attempt to spoof organizer
}),
headers: {
'Content-Type': 'application/json',
cookie: `${SESSION_COOKIE_NAME}=${ownerSessionId}`,
},
});
const res = await giveawaysPost(req);
expect(res.status).toBe(201);
const body = await res.json();
expect(body.giveaway.organizerId).toBe(ownerUser.id);
});
it('owner can access private giveaway details; intruder gets 403 Forbidden', async () => {
const created = await GiveawayStore.create({
sourceUrl: validPostData.sourceUrl,
post: validPostData.post,
filterRules: validPostData.filterRules,
organizerId: ownerUser.id,
});
// 1. Owner access
const ownerReq = new NextRequest(`http://localhost:3000/api/giveaways/${created.id}`, {
headers: { cookie: `${SESSION_COOKIE_NAME}=${ownerSessionId}` },
});
const ownerRes = await giveawayDetailGet(ownerReq, { params: { id: created.id } });
expect(ownerRes.status).toBe(200);
// 2. Intruder access
const intruderReq = new NextRequest(`http://localhost:3000/api/giveaways/${created.id}`, {
headers: { cookie: `${SESSION_COOKIE_NAME}=${intruderSessionId}` },
});
const intruderRes = await giveawayDetailGet(intruderReq, { params: { id: created.id } });
expect(intruderRes.status).toBe(403);
const intruderBody = await intruderRes.json();
expect(intruderBody.error?.message).toMatch(/not the organizer/i);
});
it('owner can import participants; intruder gets 403 Forbidden', async () => {
const created = await GiveawayStore.create({
sourceUrl: validPostData.sourceUrl,
post: validPostData.post,
filterRules: validPostData.filterRules,
organizerId: ownerUser.id,
});
const body = JSON.stringify({ filterRules: validPostData.filterRules });
// 1. Intruder attempt
const intruderReq = new NextRequest(`http://localhost:3000/api/giveaways/${created.id}/participants`, {
method: 'POST',
body,
headers: {
'Content-Type': 'application/json',
cookie: `${SESSION_COOKIE_NAME}=${intruderSessionId}`,
},
});
const intruderRes = await participantsPost(intruderReq, { params: { id: created.id } });
expect(intruderRes.status).toBe(403);
// 2. Anonymous attempt
const anonReq = new NextRequest(`http://localhost:3000/api/giveaways/${created.id}/participants`, {
method: 'POST',
body,
headers: { 'Content-Type': 'application/json' },
});
const anonRes = await participantsPost(anonReq, { params: { id: created.id } });
expect(anonRes.status).toBe(401);
});
it('owner can lock snapshot and execute draw; non-owner gets 403 Forbidden', async () => {
const created = await GiveawayStore.create({
sourceUrl: validPostData.sourceUrl,
post: validPostData.post,
filterRules: validPostData.filterRules,
organizerId: ownerUser.id,
});
// Populate participants
await GiveawayStore.updateParticipants(created.id, [
{
platformUserId: 'u1',
firstName: 'Bob',
lastName: 'Test',
source: 'LIKES',
liked: true,
commented: false,
commentsCount: 0,
reposted: false,
subscribed: false,
eligible: true,
},
]);
// 1. Snapshot by intruder -> 403
const intruderSnapshotReq = new NextRequest(`http://localhost:3000/api/giveaways/${created.id}/snapshot`, {
method: 'POST',
body: JSON.stringify({ filterRules: validPostData.filterRules }),
headers: {
'Content-Type': 'application/json',
cookie: `${SESSION_COOKIE_NAME}=${intruderSessionId}`,
},
});
const intruderSnapRes = await snapshotPost(intruderSnapshotReq, { params: { id: created.id } });
expect(intruderSnapRes.status).toBe(403);
// 2. Snapshot by owner -> 200
const ownerSnapshotReq = new NextRequest(`http://localhost:3000/api/giveaways/${created.id}/snapshot`, {
method: 'POST',
body: JSON.stringify({ filterRules: validPostData.filterRules }),
headers: {
'Content-Type': 'application/json',
cookie: `${SESSION_COOKIE_NAME}=${ownerSessionId}`,
},
});
const ownerSnapRes = await snapshotPost(ownerSnapshotReq, { params: { id: created.id } });
expect(ownerSnapRes.status).toBe(200);
// 3. Draw by intruder -> 403
const intruderDrawReq = new NextRequest(`http://localhost:3000/api/giveaways/${created.id}/draw`, {
method: 'POST',
body: JSON.stringify({ winnersCount: 1, reserveWinnersCount: 0 }),
headers: {
'Content-Type': 'application/json',
cookie: `${SESSION_COOKIE_NAME}=${intruderSessionId}`,
},
});
const intruderDrawRes = await drawPost(intruderDrawReq, { params: { id: created.id } });
expect(intruderDrawRes.status).toBe(403);
// 4. Draw by owner -> 200
const ownerDrawReq = new NextRequest(`http://localhost:3000/api/giveaways/${created.id}/draw`, {
method: 'POST',
body: JSON.stringify({ winnersCount: 1, reserveWinnersCount: 0 }),
headers: {
'Content-Type': 'application/json',
cookie: `${SESSION_COOKIE_NAME}=${ownerSessionId}`,
},
});
const ownerDrawRes = await drawPost(ownerDrawReq, { params: { id: created.id } });
expect(ownerDrawRes.status).toBe(200);
});
it('GET /api/giveaways/[id]/verify remains public without requiring authentication', async () => {
const created = await GiveawayStore.create({
sourceUrl: validPostData.sourceUrl,
post: validPostData.post,
filterRules: validPostData.filterRules,
organizerId: ownerUser.id,
});
await GiveawayStore.updateParticipants(created.id, [
{
platformUserId: 'u1',
firstName: 'Bob',
lastName: 'Test',
source: 'LIKES',
liked: true,
commented: false,
commentsCount: 0,
reposted: false,
subscribed: false,
eligible: true,
},
]);
await GiveawayStore.createAndLockSnapshot(created.id, created.participants, validPostData.filterRules);
// Draw
const drawReq = new NextRequest(`http://localhost:3000/api/giveaways/${created.id}/draw`, {
method: 'POST',
body: JSON.stringify({ winnersCount: 1, reserveWinnersCount: 0 }),
headers: {
'Content-Type': 'application/json',
cookie: `${SESSION_COOKIE_NAME}=${ownerSessionId}`,
},
});
await drawPost(drawReq, { params: { id: created.id } });
// Public verify request without any cookie/session
const publicVerifyReq = new NextRequest(`http://localhost:3000/api/giveaways/${created.id}/verify`);
const verifyRes = await verifyGet(publicVerifyReq, { params: { id: created.id } });
expect(verifyRes.status).toBe(200);
const verifyBody = await verifyRes.json();
expect(verifyBody.verified).toBe(true);
expect(verifyBody.deterministicProofHash).toBeDefined();
// Verify no private credentials or PII lists are leaked
expect(verifyBody.access_token).toBeUndefined();
expect(verifyBody.participants).toBeUndefined();
});
});