mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
152 lines
6.2 KiB
TypeScript
152 lines
6.2 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 { readSchemaSnapshot } from '../../src/db/schema-snapshot';
|
|
|
|
import Database from 'better-sqlite3';
|
|
|
|
/**
|
|
* 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: () => {},
|
|
};
|
|
}
|