mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
0a4c6fbace
db/database.ts opened the connection as a side effect of being imported, so the moment anything reached it the database was open. index.ts had to defer loading the app until a first-boot restore was done, and nothing in the container decided when the connection opened or closed. The module now opens the connection on first use and opens nothing at import. A connection closed on purpose (a restore, the shutdown) stays closed until it is reopened, as before. The new DatabaseLifecycle provider owns the decisions: buildApp() opens through it, which also binds the ORM to later swaps (the hook attachOrm used to register) and runs the schema bootstrap in the same order as before, and the shutdown closes through it. Its file property is the one resolved database path. The module keeps every export, so the test suites that replace it with vi.mock keep working; buildDbMock gains the new openDb.
143 lines
6.1 KiB
TypeScript
143 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 on first use (`openDb()` → `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,
|
|
openDb: () => {},
|
|
registerReinitializeHook: () => {},
|
|
runDemoSeed: () => {},
|
|
};
|
|
}
|