mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
d28e91d056
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.
127 lines
6.4 KiB
TypeScript
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);
|
|
});
|
|
});
|