- 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
196 lines
5.8 KiB
TypeScript
196 lines
5.8 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest';
|
|
import { newClient } from '../helpers/api';
|
|
import { resetDatabase } from '../helpers/database';
|
|
import {
|
|
createAnswer,
|
|
createHunt,
|
|
createQuest,
|
|
createTeam,
|
|
createUser,
|
|
TEST_PASSWORD
|
|
} from '../helpers/factories';
|
|
import { prisma } from './setup';
|
|
|
|
beforeEach(resetDatabase);
|
|
|
|
/** Hunt (with revealed quests), a quest and a team owned by the given user. */
|
|
async function seedPlayground(ownerEmail: string) {
|
|
const creator = await createUser();
|
|
const hunt = await createHunt(creator.id, { revealQuests: true });
|
|
const quest = await createQuest(hunt.id, { textRequired: true });
|
|
const owner = await createUser({ email: ownerEmail });
|
|
const team = await createTeam(hunt.id, owner.id);
|
|
return { hunt, quest, owner, team };
|
|
}
|
|
|
|
function form(fields: Record<string, string>) {
|
|
const data = new FormData();
|
|
for (const [key, value] of Object.entries(fields)) {
|
|
data.append(key, value);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
describe('submit answer (POST /api/quest/[questId]/submit)', () => {
|
|
it('requires a session', async () => {
|
|
const res = await newClient().post(
|
|
'/api/quest/1/submit',
|
|
form({ answer: 'hello' })
|
|
);
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('returns 404 for unknown quests', async () => {
|
|
await createUser({ email: 'owner@test.dev' });
|
|
const client = newClient();
|
|
await client.login('owner@test.dev', TEST_PASSWORD);
|
|
const res = await client.post(
|
|
'/api/quest/999999/submit',
|
|
form({ answer: 'hello' })
|
|
);
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it('returns 403 when the user has no team in the hunt', async () => {
|
|
const { quest } = await seedPlayground('owner@test.dev');
|
|
const loner = await createUser({ email: 'loner@test.dev' });
|
|
|
|
const client = newClient();
|
|
await client.login('loner@test.dev', TEST_PASSWORD);
|
|
const res = await client.post(
|
|
`/api/quest/${quest.id}/submit`,
|
|
form({ answer: 'hello' })
|
|
);
|
|
expect(res.status).toBe(403);
|
|
expect(
|
|
await prisma.questAnswer.count({ where: { memberId: loner.id } })
|
|
).toBe(0);
|
|
});
|
|
|
|
it('stores a text-only answer', async () => {
|
|
const { quest, team, owner } = await seedPlayground('owner@test.dev');
|
|
|
|
const client = newClient();
|
|
await client.login('owner@test.dev', TEST_PASSWORD);
|
|
const res = await client.post(
|
|
`/api/quest/${quest.id}/submit`,
|
|
form({ answer: 'the answer', lang: 'en' })
|
|
);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.json()).toEqual({ ok: true });
|
|
|
|
const answer = await prisma.questAnswer.findFirst({
|
|
where: { teamId: team.id, questId: quest.id }
|
|
});
|
|
expect(answer).toMatchObject({
|
|
text: 'the answer',
|
|
lang: 'en',
|
|
final: false,
|
|
memberId: owner.id
|
|
});
|
|
});
|
|
|
|
it('exposes the stored answer with ISO dates via the quest endpoint', async () => {
|
|
const { quest } = await seedPlayground('owner@test.dev');
|
|
|
|
const client = newClient();
|
|
await client.login('owner@test.dev', TEST_PASSWORD);
|
|
await client.post(
|
|
`/api/quest/${quest.id}/submit`,
|
|
form({ answer: 'the answer' })
|
|
);
|
|
|
|
const res = await client.get(`/api/quest/${quest.id}`);
|
|
expect(res.status).toBe(200);
|
|
const body = await res.json();
|
|
expect(body.answer.text).toBe('the answer');
|
|
expect(new Date(body.answer.createdAt).getTime()).not.toBeNaN();
|
|
expect(new Date(body.answer.updatedAt).getTime()).not.toBeNaN();
|
|
});
|
|
|
|
it('updates an existing answer when updatedAt is current', async () => {
|
|
const { quest, team } = await seedPlayground('owner@test.dev');
|
|
|
|
const client = newClient();
|
|
await client.login('owner@test.dev', TEST_PASSWORD);
|
|
await client.post(
|
|
`/api/quest/${quest.id}/submit`,
|
|
form({ answer: 'first try' })
|
|
);
|
|
|
|
const existing = await prisma.questAnswer.findFirstOrThrow({
|
|
where: { teamId: team.id, questId: quest.id }
|
|
});
|
|
|
|
const res = await client.post(
|
|
`/api/quest/${quest.id}/submit`,
|
|
form({
|
|
answer: 'final try',
|
|
updatedAt: existing.updatedAt.toISOString(),
|
|
final: 'true'
|
|
})
|
|
);
|
|
expect(res.status).toBe(200);
|
|
|
|
const updated = await prisma.questAnswer.findFirstOrThrow({
|
|
where: { teamId: team.id, questId: quest.id }
|
|
});
|
|
expect(updated.id).toBe(existing.id);
|
|
expect(updated.text).toBe('final try');
|
|
expect(updated.final).toBe(true);
|
|
});
|
|
|
|
it('rejects updates based on stale data with 409', async () => {
|
|
const { quest } = await seedPlayground('owner@test.dev');
|
|
|
|
const client = newClient();
|
|
await client.login('owner@test.dev', TEST_PASSWORD);
|
|
await client.post(
|
|
`/api/quest/${quest.id}/submit`,
|
|
form({ answer: 'first try' })
|
|
);
|
|
|
|
// missing updatedAt although an answer exists
|
|
expect(
|
|
(
|
|
await client.post(
|
|
`/api/quest/${quest.id}/submit`,
|
|
form({ answer: 'second try' })
|
|
)
|
|
).status
|
|
).toBe(409);
|
|
|
|
// outdated updatedAt
|
|
expect(
|
|
(
|
|
await client.post(
|
|
`/api/quest/${quest.id}/submit`,
|
|
form({
|
|
answer: 'second try',
|
|
updatedAt: new Date(Date.now() - 60_000).toISOString()
|
|
})
|
|
)
|
|
).status
|
|
).toBe(409);
|
|
});
|
|
|
|
it('rejects submissions for finalized answers with 418', async () => {
|
|
const { quest, team, owner } = await seedPlayground('owner@test.dev');
|
|
await createAnswer(
|
|
{ teamId: team.id, questId: quest.id, memberId: owner.id },
|
|
{ text: 'locked in', final: true }
|
|
);
|
|
|
|
const client = newClient();
|
|
await client.login('owner@test.dev', TEST_PASSWORD);
|
|
const res = await client.post(
|
|
`/api/quest/${quest.id}/submit`,
|
|
form({
|
|
answer: 'too late',
|
|
updatedAt: new Date().toISOString()
|
|
})
|
|
);
|
|
expect(res.status).toBe(418);
|
|
});
|
|
});
|