test(integration): add real-stack integration test suite with PGlite
- PGlite (in-memory WASM Postgres) hosted in a child process, exposed via TCP socket; real migrations applied with prisma migrate deploy - Vitest harness (vitest.integration.config.ts, global-setup spawns PGlite + built Nuxt server, per-test TRUNCATE, factories, cookie-jar API client); no mocks, zero production code changes - 50 tests across auth, hunt, team, submit, review, showcase, diashow incl. SuperJSON envelope check; *.itest.ts keeps bun test untouched - CI: parallel unit + integration jobs in .github/workflows/test.yml - tests/ covered by nuxt typecheck via tests/tsconfig.json reference
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import superjson from 'superjson';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { newClient } from '../helpers/api';
|
||||
import { resetDatabase } from '../helpers/database';
|
||||
import {
|
||||
createAnswer,
|
||||
createHunt,
|
||||
createHuntMember,
|
||||
createQuest,
|
||||
createTeam,
|
||||
createUser,
|
||||
TEST_PASSWORD
|
||||
} from '../helpers/factories';
|
||||
import { prisma } from './setup';
|
||||
|
||||
beforeEach(resetDatabase);
|
||||
|
||||
/** Hunt with one quest, one team and a submitted (non-final) answer. */
|
||||
async function seedAnswer() {
|
||||
const creator = await createUser();
|
||||
const hunt = await createHunt(creator.id);
|
||||
const quest = await createQuest(hunt.id);
|
||||
const owner = await createUser();
|
||||
const team = await createTeam(hunt.id, owner.id);
|
||||
const answer = await createAnswer({
|
||||
teamId: team.id,
|
||||
questId: quest.id,
|
||||
memberId: owner.id
|
||||
});
|
||||
return { creator, hunt, quest, owner, team, answer };
|
||||
}
|
||||
|
||||
const reviewBody = {
|
||||
score: 42,
|
||||
review: 'Nice shot!',
|
||||
showcase: true,
|
||||
internalNote: 'check exposure',
|
||||
rankingValue: 7
|
||||
};
|
||||
|
||||
describe('review answer (POST /api/admin/answer/[answerId]/review)', () => {
|
||||
it('requires a session', async () => {
|
||||
const res = await newClient().post('/api/admin/answer/1/review', {
|
||||
...reviewBody,
|
||||
showcase: false
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('returns 404 for unknown answers', async () => {
|
||||
const { hunt } = await seedAnswer();
|
||||
const organizer = await createUser({ email: 'organizer@test.dev' });
|
||||
await createHuntMember(hunt.id, organizer.id);
|
||||
|
||||
const client = newClient();
|
||||
await client.login('organizer@test.dev', TEST_PASSWORD);
|
||||
const res = await client.post('/api/admin/answer/999999/review', {
|
||||
...reviewBody,
|
||||
showcase: false
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('denies non-members of the hunt with 404', async () => {
|
||||
const { answer } = await seedAnswer();
|
||||
await createUser({ email: 'stranger@test.dev' });
|
||||
|
||||
const client = newClient();
|
||||
await client.login('stranger@test.dev', TEST_PASSWORD);
|
||||
const res = await client.post(
|
||||
`/api/admin/answer/${answer.id}/review`,
|
||||
reviewBody
|
||||
);
|
||||
expect(res.status).toBe(404);
|
||||
expect(await prisma.review.count()).toBe(0);
|
||||
});
|
||||
|
||||
it('creates a review as a hunt member', async () => {
|
||||
const { hunt, answer } = await seedAnswer();
|
||||
const organizer = await createUser({ email: 'organizer@test.dev' });
|
||||
await createHuntMember(hunt.id, organizer.id);
|
||||
|
||||
const client = newClient();
|
||||
await client.login('organizer@test.dev', TEST_PASSWORD);
|
||||
const res = await client.post(
|
||||
`/api/admin/answer/${answer.id}/review`,
|
||||
reviewBody
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ id: answer.id });
|
||||
|
||||
const review = await prisma.review.findUniqueOrThrow({
|
||||
where: { answerId: answer.id }
|
||||
});
|
||||
expect(review).toMatchObject({
|
||||
score: 42,
|
||||
review: 'Nice shot!',
|
||||
showcase: true,
|
||||
internalNote: 'check exposure',
|
||||
rankingValue: 7,
|
||||
reviewerId: organizer.id
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the existing review on a second pass', async () => {
|
||||
const { hunt, answer } = await seedAnswer();
|
||||
const first = await createUser({ email: 'first@test.dev' });
|
||||
const second = await createUser({ email: 'second@test.dev' });
|
||||
await createHuntMember(hunt.id, first.id);
|
||||
await createHuntMember(hunt.id, second.id);
|
||||
|
||||
const firstClient = newClient();
|
||||
await firstClient.login('first@test.dev', TEST_PASSWORD);
|
||||
await firstClient.post(`/api/admin/answer/${answer.id}/review`, reviewBody);
|
||||
|
||||
const secondClient = newClient();
|
||||
await secondClient.login('second@test.dev', TEST_PASSWORD);
|
||||
const res = await secondClient.post(
|
||||
`/api/admin/answer/${answer.id}/review`,
|
||||
{ ...reviewBody, score: 100, showcase: false }
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const reviews = await prisma.review.findMany({
|
||||
where: { answerId: answer.id }
|
||||
});
|
||||
expect(reviews).toHaveLength(1);
|
||||
expect(reviews[0]).toMatchObject({
|
||||
score: 100,
|
||||
showcase: false,
|
||||
reviewerId: second.id
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects invalid scores with 400', async () => {
|
||||
const { hunt, answer } = await seedAnswer();
|
||||
const organizer = await createUser({ email: 'organizer@test.dev' });
|
||||
await createHuntMember(hunt.id, organizer.id);
|
||||
|
||||
const client = newClient();
|
||||
await client.login('organizer@test.dev', TEST_PASSWORD);
|
||||
const res = await client.post(`/api/admin/answer/${answer.id}/review`, {
|
||||
...reviewBody,
|
||||
score: -5
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hunt admin detail (GET /api/admin/hunt/[huntId])', () => {
|
||||
it('denies non-members with 404', async () => {
|
||||
const { hunt } = await seedAnswer();
|
||||
await createUser({ email: 'stranger@test.dev' });
|
||||
|
||||
const client = newClient();
|
||||
await client.login('stranger@test.dev', TEST_PASSWORD);
|
||||
const res = await client.get(`/api/admin/hunt/${hunt.id}`);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('responds with a SuperJSON envelope containing real Dates', async () => {
|
||||
const { hunt } = await seedAnswer();
|
||||
const organizer = await createUser({ email: 'organizer@test.dev' });
|
||||
await createHuntMember(hunt.id, organizer.id);
|
||||
const start = new Date('2026-09-01T12:00:00Z');
|
||||
await prisma.hunt.update({
|
||||
where: { id: hunt.id },
|
||||
data: { start }
|
||||
});
|
||||
|
||||
const client = newClient();
|
||||
await client.login('organizer@test.dev', TEST_PASSWORD);
|
||||
const res = await client.get(`/api/admin/hunt/${hunt.id}`);
|
||||
expect(res.status).toBe(200);
|
||||
const raw = await res.text();
|
||||
const envelope = JSON.parse(raw);
|
||||
// SuperJSON wraps the payload in { json, meta }
|
||||
expect(envelope.json).toBeDefined();
|
||||
expect(envelope.meta).toBeDefined();
|
||||
|
||||
const huntDto = superjson.parse<{ start: Date; id: number }>(raw);
|
||||
expect(huntDto.id).toBe(hunt.id);
|
||||
expect(huntDto.start).toBeInstanceOf(Date);
|
||||
expect(huntDto.start.toISOString()).toBe(start.toISOString());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user