Files
TREK/server/tests/unit/db/repositories/TripInviteTokens.repository.test.ts
T
Maurice d28e91d056 test(server): seed the repository specs through the ORM factories
The repository suites now write their fixtures and read back what a call
left behind through the factories and readers, not hand-written SQLite on
the better-sqlite3 handle. What stays raw is marked with test-sql-allow:
the legacy statements the parity cases hold a repository read to, rows
read as the table stores them (a BLOB, an integer the entity maps to a
boolean), and a write that has to land on the connection an open
transaction holds.

Cases that pinned a "raw UPDATE" between two repository reads now make
that write through a context of their own, which the repository's
identity map knows as little about, so they still prove the same thing.
2026-10-08 18:46:13 +02:00

127 lines
6.4 KiB
TypeScript

import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { createSnapshotTestDb } from '../../../helpers/db-mock';
import { resetTestDb } from '../../../helpers/test-db';
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
import { createTrip, createUser } from '../../../helpers/factories';
import { TripInviteTokens } from '../../../../src/db/entities/TripInviteTokens.entity';
import { countRows, findRow, insertRow } from '../../../helpers/factories/rows';
import type { TripInviteTokensRepository } from '../../../../src/db/repositories/TripInviteTokens.repository';
const testDb = createSnapshotTestDb();
let t: TestOrm;
let tripInviteTokens: TripInviteTokensRepository;
beforeAll(async () => {
t = await createTestOrm(testDb);
tripInviteTokens = t.repo(TripInviteTokens);
});
beforeEach(() => { resetTestDb(testDb); t.clear(); });
afterAll(async () => { await t.close(); testDb.close(); });
/** The trip's stored invite row as it is now; fails the case when there is none. */
async function storedToken(tripId: number) {
const row = await findRow(t, TripInviteTokens, { trip: tripId });
if (!row) throw new Error(`no invite token for trip ${tripId}`);
return row;
}
describe('TripInviteTokensRepository.findInfoByTrip / existsForTrip (Plan 4 Task 1)', () => {
it('TIREPO-001: findInfoByTrip returns token/expires_at/created_at only, undefined when no row', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
expect(await tripInviteTokens.findInfoByTrip(trip.id)).toBeUndefined();
expect(await tripInviteTokens.existsForTrip(trip.id)).toBe(false);
await insertRow(t, TripInviteTokens, { trip: trip.id, token: 'tok', createdByRef: user.id, expires_at: null });
// test-sql-allow: the legacy statement is the oracle the repository read is held to.
const legacy = testDb.prepare('SELECT token, expires_at, created_at FROM trip_invite_tokens WHERE trip_id = ?').get(trip.id);
expect(await tripInviteTokens.findInfoByTrip(trip.id)).toEqual(legacy);
expect(await tripInviteTokens.existsForTrip(trip.id)).toBe(true);
});
});
describe('TripInviteTokensRepository.insertForTrip / updateForTrip (Plan 4 Task 1)', () => {
it('TIREPO-002: insertForTrip writes the 4-column set, created_at defaults', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await tripInviteTokens.insertForTrip({ trip_id: trip.id, token: 'abc', created_by: user.id, expires_at: null });
const row = await storedToken(trip.id);
expect({ trip_id: row.trip_id, token: row.token, created_by: row.created_by, expires_at: row.expires_at }).toEqual({
trip_id: trip.id,
token: 'abc',
created_by: user.id,
expires_at: null,
});
expect(row.created_at).toBeTruthy();
});
it('TIREPO-003: updateForTrip rewrites token/expires_at/created_by AND bumps created_at, on the SAME row (single row per trip)', async () => {
const { user: owner } = createUser(testDb);
const { user: rotator } = createUser(testDb, { username: 'rotator' });
const trip = createTrip(testDb, owner.id);
await tripInviteTokens.insertForTrip({ trip_id: trip.id, token: 'first', created_by: owner.id, expires_at: null });
const before = await storedToken(trip.id);
await tripInviteTokens.updateForTrip(trip.id, { token: 'second', expires_at: '2030-01-01T00:00:00.000Z', created_by: rotator.id });
const after = await storedToken(trip.id);
expect(after.id).toBe(before.id); // same row, not a second insert
expect(after.token).toBe('second');
expect(after.expires_at).toBe('2030-01-01T00:00:00.000Z');
expect(after.created_by).toBe(rotator.id);
expect(await countRows(t, TripInviteTokens, { trip: trip.id })).toBe(1);
});
});
describe('TripInviteTokensRepository.deleteByTrip (Plan 4 Task 1)', () => {
it('TIREPO-004: removes the trip\'s row and only that trip\'s row', async () => {
const { user } = createUser(testDb);
const tripA = createTrip(testDb, user.id);
const tripB = createTrip(testDb, user.id);
await tripInviteTokens.insertForTrip({ trip_id: tripA.id, token: 'a', created_by: user.id, expires_at: null });
await tripInviteTokens.insertForTrip({ trip_id: tripB.id, token: 'b', created_by: user.id, expires_at: null });
await tripInviteTokens.deleteByTrip(tripA.id);
expect(await tripInviteTokens.existsForTrip(tripA.id)).toBe(false);
expect(await tripInviteTokens.existsForTrip(tripB.id)).toBe(true);
});
it('TIREPO-005: deleting a trip with no row is a no-op', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await expect(tripInviteTokens.deleteByTrip(trip.id)).resolves.toBeUndefined();
});
});
describe('TripInviteTokensRepository.resolveTokenToTrip (Plan 4 Task 1, mutation proof for token validation)', () => {
it('TIREPO-006: resolves a valid token to its trip id/title/expires_at', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Kyoto Loop' });
await tripInviteTokens.insertForTrip({ trip_id: trip.id, token: 'real-token', created_by: user.id, expires_at: '2030-01-01T00:00:00.000Z' });
expect(await tripInviteTokens.resolveTokenToTrip('real-token')).toEqual({ trip_id: trip.id, title: 'Kyoto Loop', expires_at: '2030-01-01T00:00:00.000Z' });
});
it('TIREPO-007: an unknown token resolves to undefined — never a row from a different trip', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await tripInviteTokens.insertForTrip({ trip_id: trip.id, token: 'real-token', created_by: user.id, expires_at: null });
expect(await tripInviteTokens.resolveTokenToTrip('forged-token')).toBeUndefined();
});
it("TIREPO-008: a token that used to resolve stops resolving once it's rotated away (mutation proof)", async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await tripInviteTokens.insertForTrip({ trip_id: trip.id, token: 'old-token', created_by: user.id, expires_at: null });
await tripInviteTokens.updateForTrip(trip.id, { token: 'new-token', expires_at: null, created_by: user.id });
expect(await tripInviteTokens.resolveTokenToTrip('old-token')).toBeUndefined();
expect((await tripInviteTokens.resolveTokenToTrip('new-token'))?.trip_id).toBe(trip.id);
});
});