Files
TREK/server/tests/helpers/db-mock.ts
T

142 lines
6.1 KiB
TypeScript

/**
* Imported from inside `vi.mock('…/db/database')` factories, so it must stay a
* leaf: anything under src/nest pulled in here would load `db/database` while
* its mock is still being built and capture the real module.
*
* `tests/helpers/test-db.ts` used to define `createSnapshotTestDb`/`buildDbMock`
* itself, but it also imports `AuthPublicController` (for `resetRateLimits`),
* which drags in the whole auth graph — including `jwt-verify.ts`, which
* imports `../../db/database`. A `vi.mock('../../src/db/database', async () =>
* { const { createSnapshotTestDb, buildDbMock } = await import('../helpers/
* test-db'); ... })` factory that re-enters `db/database` while building its
* own mock for that exact module gets handed the REAL, unmocked module for
* that one import — Vitest can't give a still-building mock to its own
* re-entrant importer. The real module's test-mode singleton then opens its
* OWN copy of the schema snapshot (`initDb()` → `readSchemaSnapshot()`), so
* whichever code path captured `db` this way sees a pristine, never-reset
* database (the snapshot's seeded `admin@trek.local`) instead of the mock
* `buildDbMock` builds — while everything that imports `db/database` AFTER
* the factory returns gets the real mock. Which one a given guard/service
* saw then depended on unrelated import order, not on anything the test
* controls — e.g. a fresh `role: 'user'` account reading as `role: 'admin'`,
* or a trip the caller owns coming back "not found".
*
* So this module only imports `better-sqlite3` and `schema-snapshot.ts`
* (node builtins only) — nothing that could transitively import
* `src/db/database.ts` again. Keep it that way.
*/
import Database from 'better-sqlite3';
import { readSchemaSnapshot } from '../../src/db/schema-snapshot';
/**
* A copy of the migrated-and-seeded database the vitest global setup wrote
* (`tests/global-setup.ts`), for suites that boot the app with `buildApp()`.
* Those run the MikroORM migrator and seeders against the mocked connection,
* which only works on a database the ORM's migration table already knows.
* The legacy `createTestDb()` (in test-db.ts) stays for unit suites that never
* boot the app.
*
* @returns a normalised copy: the snapshot's seeded first-run `admin` row and
* the `users` autoincrement sequence have already been removed (see below), so
* the first user any caller creates lands on id 1.
*/
export function createSnapshotTestDb(): Database.Database {
const snapshot = readSchemaSnapshot();
if (!snapshot) {
throw new Error('No schema snapshot: run under vitest (tests/global-setup.ts writes it once per run).');
}
const db = new Database(snapshot);
// No journal_mode PRAGMA here: this is a deserialised in-memory database, so
// WAL mode (a disk-file concept) is a no-op on it.
db.exec('PRAGMA busy_timeout = 5000');
db.exec('PRAGMA foreign_keys = ON');
// The snapshot carries the first-run seeded `admin` row (id 1); the legacy
// createTestDb() started with no users at all, so the first user a test
// created was always id 1. Normalise ONCE, here, rather than resetting the
// sequence on every resetTestDb() call: ids then keep growing across tests
// within a file exactly as they did under the legacy helper, which several
// suites (oauth.test.ts's per-user client cap, mcp.test.ts's in-memory
// session registry, memories-synology.test.ts's insert-once fixtures) rely
// on to stay disjoint from one test to the next.
db.exec('DELETE FROM users');
db.exec("DELETE FROM sqlite_sequence WHERE name = 'users'");
return db;
}
/**
* Byte-for-byte the statement in src/db/database.ts.
*
* Exported because the same query is copied into ~90 test files, and every one
* of those copies had dropped `t.currency` — so the budget domain, which reads
* exactly that column off the access row, was only ever exercising its 'EUR'
* fallback. Import this instead of retyping it.
*/
export const CAN_ACCESS_TRIP_SQL = `
SELECT t.id, t.user_id, t.currency FROM trips t
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ?
WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)
`;
/**
* Returns the mock factory for vi.mock('../../src/db/database', ...).
* The returned object mirrors the shape of database.ts exports.
*
* @example
* vi.mock('../../src/db/database', async () => {
* const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
* return buildDbMock(createSnapshotTestDb());
* });
*/
export function buildDbMock(testDb: Database.Database) {
return {
db: testDb,
closeDb: () => {},
reinitialize: () => {},
getPlaceWithTags: async (placeId: number | string) => {
interface PlaceRow {
id: number;
category_id: number | null;
category_name: string | null;
category_color: string | null;
category_icon: string | null;
[key: string]: unknown;
}
const place = testDb.prepare(`
SELECT p.*, c.name as category_name, c.color as category_color, c.icon as category_icon
FROM places p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.id = ?
`).get(placeId) as PlaceRow | undefined;
if (!place) return null;
const tags = testDb.prepare(`
SELECT t.* FROM tags t
JOIN place_tags pt ON t.id = pt.tag_id
WHERE pt.place_id = ?
`).all(placeId);
return {
...place,
category: place.category_id ? {
id: place.category_id,
name: place.category_name,
color: place.category_color,
icon: place.category_icon,
} : null,
tags,
};
},
canAccessTrip: async (tripId: number | string, userId: number) => {
return testDb.prepare(CAN_ACCESS_TRIP_SQL).get(userId, tripId, userId);
},
isOwner: async (tripId: number | string, userId: number) => {
return !!testDb.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId);
},
getRawConnection: () => testDb,
registerReinitializeHook: () => {},
runDemoSeed: () => {},
};
}