Files
Maurice 724c88c201 style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
2026-10-10 21:23:37 +02:00

153 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 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 { 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,
openDb: () => {},
registerReinitializeHook: () => {},
runDemoSeed: () => {},
};
}