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,141 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { newClient } from '../helpers/api';
|
||||
import { resetDatabase } from '../helpers/database';
|
||||
import {
|
||||
createHunt,
|
||||
createTeam,
|
||||
createTeamMember,
|
||||
createUser,
|
||||
TEST_PASSWORD
|
||||
} from '../helpers/factories';
|
||||
import { prisma } from './setup';
|
||||
|
||||
beforeEach(resetDatabase);
|
||||
|
||||
describe('create team (POST /api/hunt/[huntId]/team)', () => {
|
||||
it('requires a session', async () => {
|
||||
const res = await newClient().post('/api/hunt/1/team', {
|
||||
name: 'The A-Team'
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('creates a team with a generated invite password', async () => {
|
||||
const creator = await createUser();
|
||||
const hunt = await createHunt(creator.id);
|
||||
const owner = await createUser({ email: 'owner@test.dev' });
|
||||
|
||||
const client = newClient();
|
||||
await client.login('owner@test.dev', TEST_PASSWORD);
|
||||
const res = await client.post(`/api/hunt/${hunt.id}/team`, {
|
||||
name: 'The A-Team',
|
||||
description: 'We love photos'
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.team).toMatchObject({
|
||||
name: 'The A-Team',
|
||||
description: 'We love photos',
|
||||
ownerId: owner.id,
|
||||
huntId: hunt.id
|
||||
});
|
||||
// short hex invite code
|
||||
expect(body.team.password).toMatch(/^[0-9a-f]{6}$/);
|
||||
|
||||
expect(await prisma.huntTeam.count({ where: { huntId: hunt.id } })).toBe(1);
|
||||
});
|
||||
|
||||
it('enforces one team per user per hunt', async () => {
|
||||
const creator = await createUser();
|
||||
const hunt = await createHunt(creator.id);
|
||||
await createUser({ email: 'owner@test.dev' });
|
||||
|
||||
const client = newClient();
|
||||
await client.login('owner@test.dev', TEST_PASSWORD);
|
||||
expect(
|
||||
(await client.post(`/api/hunt/${hunt.id}/team`, { name: 'First' })).status
|
||||
).toBe(200);
|
||||
// as owner of the first team
|
||||
expect(
|
||||
(await client.post(`/api/hunt/${hunt.id}/team`, { name: 'Second' }))
|
||||
.status
|
||||
).toBe(409);
|
||||
|
||||
// as member of an existing team
|
||||
const team = await createTeam(hunt.id, creator.id);
|
||||
const member = await createUser({ email: 'member@test.dev' });
|
||||
await createTeamMember(team.id, member.id);
|
||||
const memberClient = newClient();
|
||||
await memberClient.login('member@test.dev', TEST_PASSWORD);
|
||||
expect(
|
||||
(await memberClient.post(`/api/hunt/${hunt.id}/team`, { name: 'Third' }))
|
||||
.status
|
||||
).toBe(409);
|
||||
});
|
||||
|
||||
it('rejects invalid team names with 400', async () => {
|
||||
const creator = await createUser();
|
||||
const hunt = await createHunt(creator.id);
|
||||
|
||||
const client = newClient();
|
||||
await client.login(creator.email, TEST_PASSWORD);
|
||||
expect(
|
||||
(await client.post(`/api/hunt/${hunt.id}/team`, { name: 'ab' })).status
|
||||
).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('teams overview (GET /api/hunt/[huntId]/teams)', () => {
|
||||
it('returns teams with owner name and member count, sorted by name', async () => {
|
||||
const creator = await createUser();
|
||||
const hunt = await createHunt(creator.id);
|
||||
const ownerA = await createUser({ name: 'Alice' });
|
||||
const ownerB = await createUser({ name: 'Bob' });
|
||||
const teamA = await createTeam(hunt.id, ownerA.id, { name: 'Alpha' });
|
||||
await createTeam(hunt.id, ownerB.id, { name: 'Zebra' });
|
||||
const joiner = await createUser();
|
||||
await createTeamMember(teamA.id, joiner.id);
|
||||
|
||||
const res = await newClient().get(`/api/hunt/${hunt.id}/teams`);
|
||||
expect(res.status).toBe(200);
|
||||
const teams = await res.json();
|
||||
expect(teams).toEqual([
|
||||
{
|
||||
id: teamA.id,
|
||||
name: 'Alpha',
|
||||
description: '',
|
||||
owner: { name: 'Alice' },
|
||||
_count: { members: 1 }
|
||||
},
|
||||
expect.objectContaining({
|
||||
name: 'Zebra',
|
||||
owner: { name: 'Bob' },
|
||||
_count: { members: 0 }
|
||||
})
|
||||
]);
|
||||
// the invite password must not leak here
|
||||
expect(JSON.stringify(teams)).not.toContain('password');
|
||||
});
|
||||
});
|
||||
|
||||
describe('join via team password (POST /api/hunt/[huntId]/join)', () => {
|
||||
it('lets a second user join an existing team', async () => {
|
||||
const creator = await createUser();
|
||||
const hunt = await createHunt(creator.id);
|
||||
const owner = await createUser();
|
||||
const team = await createTeam(hunt.id, owner.id, { password: 'feedbe' });
|
||||
await createUser({ email: 'joiner@test.dev' });
|
||||
|
||||
const client = newClient();
|
||||
await client.login('joiner@test.dev', TEST_PASSWORD);
|
||||
const res = await client.post(`/api/hunt/${hunt.id}/join`, {
|
||||
team: team.id,
|
||||
password: 'feedbe'
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const teamsRes = await client.get(`/api/hunt/${hunt.id}/teams`);
|
||||
const teams = await teamsRes.json();
|
||||
expect(teams[0]._count.members).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user