Files
TREK/server/tests/unit/demo/demo-reset-sessions.test.ts
T
Maurice 0fa47e4297 test(demo): give the session reset spec a baseline to reset to
resetDemoUser returns early when no baseline file exists and asks the
port whether it can snapshot, so the spec needs both to reach the swap.
2026-10-08 19:18:42 +02:00

112 lines
4.8 KiB
TypeScript

/**
* The hourly demo reset keeps everybody signed in.
*
* The reset copies the baseline over the live database file. That baseline
* was saved at first seed, before anybody logged in, so it holds no
* `user_sessions` rows, and a session token whose row is gone is refused.
* Without carrying the active rows across the swap, every demo visitor and
* the demo admin would get a 401 on the hour.
*
* The database port is a stand-in whose `replace()` plays the file swap by
* emptying `user_sessions`, which is exactly what the baseline brings back.
* The reads and writes around it run for real against the snapshot schema.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import type Database from 'better-sqlite3';
import { createSnapshotTestDb } from '../../helpers/db-mock';
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
import { makeUser } from '../../helpers/factories/users';
import { countRows, deleteRows, findRows } from '../../helpers/factories/rows';
import { withRequestContext } from '../../../src/nest/database/request-context';
import type { DatabaseBackupStrategy } from '../../../src/nest/database/database-backup.interface';
import { resetDemoUser } from '../../../src/demo/demo-reset';
import { SessionsService } from '../../../src/nest/sessions/sessions.service';
import { verifyJwtAndLoadUser } from '../../../src/nest/auth/jwt-verify';
import { Users } from '../../../src/db/entities/Users.entity';
import { UserSessions } from '../../../src/db/entities/UserSessions.entity';
const BASELINE = path.resolve(__dirname, '..', '..', '..', 'data', 'travel-baseline.db');
describe('demo reset and the signed-in sessions', () => {
let ormDb: Database.Database;
let t: TestOrm;
/** What the baseline swap does to the live rows; the default empties user_sessions. */
let swap: () => Promise<void>;
let database: DatabaseBackupStrategy;
beforeEach(async () => {
vi.spyOn(console, 'log').mockImplementation(() => undefined);
ormDb = createSnapshotTestDb();
t = await createTestOrm(ormDb);
// After the snapshot DB is built: it locates the migrations through fs.existsSync.
vi.spyOn(fs, 'existsSync').mockImplementation(((p: fs.PathLike) => String(p) === BASELINE) as typeof fs.existsSync);
swap = async () => {
await deleteRows(t, UserSessions);
};
database = {
canSnapshot: () => true,
checkpoint: vi.fn(async () => undefined),
replace: vi.fn(async () => {
await swap();
return { reopenError: undefined };
}),
} as unknown as DatabaseBackupStrategy;
});
afterEach(async () => {
vi.restoreAllMocks();
await t.close();
ormDb.close();
});
const verify = (token: string) => verifyJwtAndLoadUser(token, t.repo(Users), t.repo(UserSessions));
it('DEMORESET-SESS-001: a session signed in before the reset still authenticates after it', async () => {
const { user } = await makeUser(t, { email: 'demo-visitor@example.test' });
const sessions = new SessionsService(t.repo(UserSessions));
const token = await sessions.issue({ id: user.id, pv: 0 }, undefined, { userAgent: 'Visitor' });
expect((await verify(token))?.id).toBe(user.id);
await withRequestContext(t.orm, () => resetDemoUser(database));
t.clear();
expect(database.replace).toHaveBeenCalled();
expect((await verify(token))?.id).toBe(user.id);
const rows = await findRows(t, UserSessions);
expect(rows.map(({ user_id, user_agent, revoked_at }) => ({ user_id, user_agent, revoked_at }))).toEqual([
{ user_id: user.id, user_agent: 'Visitor', revoked_at: null },
]);
});
it('DEMORESET-SESS-002: a session that had already ended stays ended', async () => {
const { user } = await makeUser(t, { email: 'demo-ended@example.test' });
const sessions = new SessionsService(t.repo(UserSessions));
const token = await sessions.issue({ id: user.id, pv: 0 });
await sessions.revokeAll(user.id);
await withRequestContext(t.orm, () => resetDemoUser(database));
t.clear();
expect(await verify(token)).toBeNull();
expect(await countRows(t, UserSessions)).toBe(0);
});
it('DEMORESET-SESS-003: a session whose account the baseline does not hold is not brought back', async () => {
const { user } = await makeUser(t, { email: 'registered-after-seed@example.test' });
const sessions = new SessionsService(t.repo(UserSessions));
const token = await sessions.issue({ id: user.id, pv: 0 });
swap = async () => {
await deleteRows(t, UserSessions);
await deleteRows(t, Users, { id: user.id });
};
await withRequestContext(t.orm, () => resetDemoUser(database));
t.clear();
expect(await verify(token)).toBeNull();
expect(await countRows(t, UserSessions)).toBe(0);
});
});