v1.1.0: session guard on all game routes + register + sign-in screen (ISS-107)
CI / build + unit + e2e (pull_request) Failing after 26m20s
CI / build + unit + e2e (pull_request) Failing after 26m20s
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { getGameDb } from '@/lib/db/client';
|
||||
import { createSession, hashPassword, requireAuth, verifyPassword } from './auth';
|
||||
|
||||
// v1.1.0 (ISS-107) — the session guard every game route funnels through. Same temp-DB pattern as
|
||||
// the other API tests: FLIGHT_DB_PATH points at a throwaway file, fresh singleton per test file.
|
||||
|
||||
let dir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'flight-auth-'));
|
||||
process.env.FLIGHT_DB_PATH = path.join(dir, 'auth.db');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
delete process.env.FLIGHT_DB_PATH;
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const reqWith = (sessionId?: string) =>
|
||||
new Request('http://localhost/api/state', { headers: sessionId ? { 'X-Flight-Session': sessionId } : {} });
|
||||
|
||||
describe('hashPassword / verifyPassword', () => {
|
||||
it('round-trips a password through the stored digest', () => {
|
||||
const hash = hashPassword('correct-horse-battery-staple');
|
||||
expect(hash).toMatch(/^[0-9a-f]{32}\$/); // 16-byte hex salt + 32-byte scrypt, like the seeded admin's
|
||||
expect(verifyPassword('correct-horse-battery-staple', hash)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a wrong password and malformed digests', () => {
|
||||
const hash = hashPassword('some-password');
|
||||
expect(verifyPassword('other-password', hash)).toBe(false);
|
||||
expect(verifyPassword('x', 'not-a-digest')).toBe(false);
|
||||
expect(verifyPassword('x', '')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requireAuth', () => {
|
||||
it('401s when the header is missing or the session id is unknown', async () => {
|
||||
for (const req of [reqWith(), reqWith('no-such-session')]) {
|
||||
const guard = await requireAuth(req);
|
||||
expect(guard.ok).toBe(false);
|
||||
if (!guard.ok) {
|
||||
expect(guard.response.status).toBe(401);
|
||||
expect(((await guard.response.json()) as { error: string }).error).toMatch(/authentication required/i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('401s an expired session', async () => {
|
||||
const inst = await getGameDb();
|
||||
inst.raw.run("INSERT INTO sessions (id, user_id, expires_at) VALUES ('expired-sess', 'admin-kamilkrzywda', ?)", [Date.now() - 1]);
|
||||
const guard = await requireAuth(reqWith('expired-sess'));
|
||||
expect(guard.ok).toBe(false);
|
||||
if (!guard.ok) expect(guard.response.status).toBe(401);
|
||||
});
|
||||
|
||||
it('resolves the seeded admin session to the local player row (joined on username)', async () => {
|
||||
const inst = await getGameDb();
|
||||
const sessionId = await createSession(inst, 'admin-kamilkrzywda');
|
||||
const guard = await requireAuth(reqWith(sessionId));
|
||||
expect(guard.ok).toBe(true);
|
||||
if (guard.ok) {
|
||||
expect(guard.user.username).toBe('kamilkrzywda');
|
||||
expect(guard.player.id).toBe('local'); // the v1.0.0 migration's local player row
|
||||
expect(guard.player.cash).toBe(20_000); // starter economy from the seed
|
||||
}
|
||||
});
|
||||
|
||||
it('500s when the account has no players row (broken data, not a client error)', async () => {
|
||||
const inst = await getGameDb();
|
||||
inst.raw.run("INSERT INTO users (id, username, password_hash, admin, created_at) VALUES ('user-no-player', 'no-player-user', ?, 0, ?)", [
|
||||
hashPassword('whatever-123'),
|
||||
Date.now(),
|
||||
]);
|
||||
const sessionId = await createSession(inst, 'user-no-player');
|
||||
const guard = await requireAuth(reqWith(sessionId));
|
||||
expect(guard.ok).toBe(false);
|
||||
if (!guard.ok) {
|
||||
expect(guard.response.status).toBe(500);
|
||||
expect(((await guard.response.json()) as { error: string }).error).toMatch(/player row missing/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user