142 lines
5.2 KiB
TypeScript
142 lines
5.2 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 { GET as giveawaysGet } from '../src/app/api/giveaways/route';
|
|
import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
|
|
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
|
|
|
|
describe('Phase 2.2.3 Claude PoC: GET /api/giveaways IDOR & Scoped Query Gate', () => {
|
|
let memoryRepo: MemoryGiveawayRepository;
|
|
|
|
const userA = { id: 'usr_organizer_alpha', vkUserId: '11111', firstName: 'Alice' };
|
|
const userB = { id: 'usr_organizer_beta', vkUserId: '22222', firstName: 'Bob' };
|
|
const userEmpty = { id: 'usr_organizer_empty', vkUserId: '33333', firstName: 'Charlie' };
|
|
|
|
let sessionA: string;
|
|
let sessionB: string;
|
|
let sessionEmpty: string;
|
|
|
|
beforeEach(async () => {
|
|
memoryRepo = new MemoryGiveawayRepository();
|
|
GiveawayStore.setRepository(memoryRepo);
|
|
|
|
defaultSessionStore.clear();
|
|
sessionA = await defaultSessionStore.createSession(userA);
|
|
sessionB = await defaultSessionStore.createSession(userB);
|
|
sessionEmpty = await defaultSessionStore.createSession(userEmpty);
|
|
|
|
// Populate giveaways for User A
|
|
await GiveawayStore.create({
|
|
sourceUrl: 'https://vk.com/wall-10_100',
|
|
post: {
|
|
platform: 'VK',
|
|
ownerId: '-10',
|
|
postId: '100',
|
|
sourceUrl: 'https://vk.com/wall-10_100',
|
|
title: 'Secret Giveaway of Alice',
|
|
likesCount: 10,
|
|
commentsCount: 2,
|
|
repostsCount: 0,
|
|
},
|
|
filterRules: DEFAULT_FILTER_RULES,
|
|
organizerId: userA.id,
|
|
});
|
|
|
|
// Populate giveaways for User B
|
|
await GiveawayStore.create({
|
|
sourceUrl: 'https://vk.com/wall-20_200',
|
|
post: {
|
|
platform: 'VK',
|
|
ownerId: '-20',
|
|
postId: '200',
|
|
sourceUrl: 'https://vk.com/wall-20_200',
|
|
title: 'Confidential Giveaway of Bob',
|
|
likesCount: 50,
|
|
commentsCount: 15,
|
|
repostsCount: 5,
|
|
},
|
|
filterRules: DEFAULT_FILTER_RULES,
|
|
organizerId: userB.id,
|
|
});
|
|
});
|
|
|
|
it('Claude PoC Reproduction: anonymous GET /api/giveaways is rejected with 401 Unauthorized', async () => {
|
|
const req = new NextRequest('http://localhost:3000/api/giveaways');
|
|
const res = await giveawaysGet(req);
|
|
|
|
expect(res.status).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.error?.message).toMatch(/authentication required/i);
|
|
// Crucial: No giveaways leaked to anonymous attacker
|
|
expect(body.giveaways).toBeUndefined();
|
|
});
|
|
|
|
it('User A list returns strictly User A giveaways (no User B records leaked)', async () => {
|
|
const req = new NextRequest('http://localhost:3000/api/giveaways', {
|
|
headers: { cookie: `${SESSION_COOKIE_NAME}=${sessionA}` },
|
|
});
|
|
const res = await giveawaysGet(req);
|
|
|
|
expect(res.status).toBe(200);
|
|
const body = await res.json();
|
|
expect(body.success).toBe(true);
|
|
expect(body.giveaways).toHaveLength(1);
|
|
|
|
const gw = body.giveaways[0];
|
|
expect(gw.organizerId).toBe(userA.id);
|
|
expect(gw.title).toBe('Secret Giveaway of Alice');
|
|
|
|
// Ensure User B information is completely absent
|
|
expect(JSON.stringify(body)).not.toContain('Bob');
|
|
expect(JSON.stringify(body)).not.toContain('Confidential Giveaway of Bob');
|
|
expect(JSON.stringify(body)).not.toContain('wall-20_200');
|
|
});
|
|
|
|
it('User B list returns strictly User B giveaways (no User A records leaked)', async () => {
|
|
const req = new NextRequest('http://localhost:3000/api/giveaways', {
|
|
headers: { cookie: `${SESSION_COOKIE_NAME}=${sessionB}` },
|
|
});
|
|
const res = await giveawaysGet(req);
|
|
|
|
expect(res.status).toBe(200);
|
|
const body = await res.json();
|
|
expect(body.success).toBe(true);
|
|
expect(body.giveaways).toHaveLength(1);
|
|
|
|
const gw = body.giveaways[0];
|
|
expect(gw.organizerId).toBe(userB.id);
|
|
expect(gw.title).toBe('Confidential Giveaway of Bob');
|
|
|
|
// Ensure User A information is completely absent
|
|
expect(JSON.stringify(body)).not.toContain('Alice');
|
|
expect(JSON.stringify(body)).not.toContain('Secret Giveaway of Alice');
|
|
expect(JSON.stringify(body)).not.toContain('wall-10_100');
|
|
});
|
|
|
|
it('empty account receives empty array [] without errors', async () => {
|
|
const req = new NextRequest('http://localhost:3000/api/giveaways', {
|
|
headers: { cookie: `${SESSION_COOKIE_NAME}=${sessionEmpty}` },
|
|
});
|
|
const res = await giveawaysGet(req);
|
|
|
|
expect(res.status).toBe(200);
|
|
const body = await res.json();
|
|
expect(body.success).toBe(true);
|
|
expect(body.giveaways).toEqual([]);
|
|
expect(body.totalCount).toBe(0);
|
|
});
|
|
|
|
it('Repository level test: listGiveawaysSummary filters directly by organizerId', async () => {
|
|
const summariesA = await memoryRepo.listGiveawaysSummary(userA.id);
|
|
expect(summariesA).toHaveLength(1);
|
|
expect(summariesA[0].organizerId).toBe(userA.id);
|
|
|
|
const summariesB = await memoryRepo.listGiveawaysSummary(userB.id);
|
|
expect(summariesB).toHaveLength(1);
|
|
expect(summariesB[0].organizerId).toBe(userB.id);
|
|
|
|
const summariesEmpty = await memoryRepo.listGiveawaysSummary(userEmpty.id);
|
|
expect(summariesEmpty).toHaveLength(0);
|
|
});
|
|
});
|