diff --git a/server/tests/helpers/test-db.ts b/server/tests/helpers/test-db.ts index 23e9f9dd6..93226b45d 100644 --- a/server/tests/helpers/test-db.ts +++ b/server/tests/helpers/test-db.ts @@ -43,105 +43,26 @@ import type { RateLimitService } from '../../src/nest/common/rate-limit.service' // that re-enters src/db/database. export { CAN_ACCESS_TRIP_SQL, buildDbMock } from './db-mock'; -// Tables to clear on reset, child-before-parent to be safe (FK checks are OFF during reset). -// Keep in sync with schema.ts + migrations.ts. Intentionally excluded: categories, addons, -// photo_providers, photo_provider_fields, schema_version (seed/config data, not user data). -const RESET_TABLES = [ - 'school_holiday_periods', - 'school_holiday_regions', - 'school_holiday_countries', - // Collab - 'file_links', - 'collab_message_reactions', - 'collab_poll_votes', - 'collab_messages', - 'collab_polls', - 'collab_notes', - // Road trip (#1797). Both hang off days, so they go before it like the rest - // of the day content; a new domain's tables belong here or its rows leak from - // one case into the next. - 'roadtrip_day_tracks', - 'roadtrip_vias', - // Dawarich (#2279). The suggestions reference places and bucket_list, so they - // are cleared before both; the connection row hangs off the user. - 'dawarich_visit_suggestions', - 'dawarich_connections', - // Day content - 'day_notes', - 'todo_category_assignees', - 'todo_items', - 'assignment_participants', - 'day_assignments', - // Places - 'place_regions', - 'place_tags', - 'places', - // Packing - 'packing_category_assignees', - 'packing_bag_members', - 'packing_bags', - 'packing_template_items', - 'packing_template_categories', - 'packing_templates', - 'packing_items', - // Budget - 'budget_item_members', - 'budget_items', - // Photos & files - 'trip_photos', - 'trip_album_links', - 'trip_files', - 'photos', - // Reservations - 'reservation_day_positions', - 'reservations', - // Accommodations & days - 'day_accommodations', - 'days', - // Trip - 'share_tokens', - 'trip_invite_tokens', - 'trip_members', - 'trips', - // Journey - 'journey_books', - 'journey_share_tokens', - 'journey_photos', - 'journey_entries', - 'journey_contributors', - 'journey_trips', - 'journeys', - // Vacay - 'vacay_user_settings', - 'vacay_shares', - 'vacay_entries', - 'vacay_company_holidays', - 'vacay_holiday_calendars', - 'vacay_plan_members', - 'vacay_user_colors', - 'vacay_user_years', - 'vacay_years', - 'vacay_plans', - // Atlas - 'visited_regions', - 'visited_countries', - 'bucket_list', - // Notifications & audit - 'notification_channel_preferences', - 'notifications', - 'audit_log', - // System notices - 'user_notice_dismissals', - // User data - 'settings', - 'mcp_tokens', - 'invite_tokens', - 'tags', - 'app_settings', - 'webauthn_challenges', - 'webauthn_credentials', - 'users', -]; +/** + * Tables `resetTestDb` must NOT clear: seed and config data every test assumes + * is present (categories, addons, the provider catalogues) plus the two + * migration bookkeeping tables. Everything else in the schema is user data and + * is derived from `sqlite_master` at reset time — see `resetTestDb`. + */ +const KEEP_TABLES = new Set([ + // Seeded reference data (`seedDefaults` below re-seeds the first three). + 'categories', + 'addons', + 'photo_providers', + 'photo_provider_fields', + 'document_providers', + 'document_provider_fields', + // Migration bookkeeping: clearing these would make the next boot replay the + // whole history over a schema that already has it. + 'schema_version', + 'migrations', + 'mikro_orm_migrations', +]); const DEFAULT_CATEGORIES = [ { name: 'Hotel', color: '#3b82f6', icon: '🏨' }, @@ -244,13 +165,18 @@ export function createTestDb(): Database.Database { */ export function resetTestDb(db: Database.Database): void { db.exec('PRAGMA foreign_keys = OFF'); - const existingTables = new Set( - (db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[]).map(r => r.name) - ); - for (const table of RESET_TABLES) { - if (existingTables.has(table)) { - db.exec(`DELETE FROM "${table}"`); - } + // Derived from the live schema, not from a list. This used to be a + // hand-mirrored `RESET_TABLES` array kept "in sync with schema.ts + + // migrations.ts" — by the time it was replaced it had drifted by ~51 tables + // (collections, oauth, plugins, settlements, …), every one of which leaked + // its rows from one test into the next. Deletion order does not matter: + // foreign_keys is OFF for the duration. + const tables = (db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[]) + .map(r => r.name) + // sqlite_* are SQLite's own (sqlite_sequence, sqlite_stat1) — never ours. + .filter(name => !name.startsWith('sqlite_') && !KEEP_TABLES.has(name)); + for (const table of tables) { + db.exec(`DELETE FROM "${table}"`); } // No sqlite_sequence reset here, on purpose: the legacy helper never reset // sequences either, so ids keep growing across tests within a file. Several diff --git a/server/tests/unit/db/repositories/DayNotes.repository.test.ts b/server/tests/unit/db/repositories/DayNotes.repository.test.ts index 0ab5bedf5..85d63d15e 100644 --- a/server/tests/unit/db/repositories/DayNotes.repository.test.ts +++ b/server/tests/unit/db/repositories/DayNotes.repository.test.ts @@ -1,12 +1,13 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { createTestDb, resetTestDb } from '../../../helpers/test-db'; +import { createSnapshotTestDb } from '../../../helpers/db-mock'; +import { resetTestDb } from '../../../helpers/test-db'; import { createTestOrm, type TestOrm } from '../../../helpers/test-orm'; import { createDay, createTrip, createUser } from '../../../helpers/factories'; import { DayNotes } from '../../../../src/db/entities/DayNotes.entity'; import type { DayNotesRepository } from '../../../../src/db/repositories/DayNotes.repository'; import { DB_TIMESTAMP_RE } from '../../../../src/db/types'; -const testDb = createTestDb(); +const testDb = createSnapshotTestDb(); let t: TestOrm; let notes: DayNotesRepository; diff --git a/server/tests/unit/db/repositories/Days.repository.test.ts b/server/tests/unit/db/repositories/Days.repository.test.ts index 89958de13..8dc223de8 100644 --- a/server/tests/unit/db/repositories/Days.repository.test.ts +++ b/server/tests/unit/db/repositories/Days.repository.test.ts @@ -1,11 +1,12 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { createTestDb, resetTestDb } from '../../../helpers/test-db'; +import { createSnapshotTestDb } from '../../../helpers/db-mock'; +import { resetTestDb } from '../../../helpers/test-db'; import { createTestOrm, type TestOrm } from '../../../helpers/test-orm'; import { createDay, createTrip, createUser } from '../../../helpers/factories'; import { Days } from '../../../../src/db/entities/Days.entity'; import type { DaysRepository } from '../../../../src/db/repositories/Days.repository'; -const testDb = createTestDb(); +const testDb = createSnapshotTestDb(); let t: TestOrm; let days: DaysRepository; diff --git a/server/tests/unit/db/repositories/Trips.repository.test.ts b/server/tests/unit/db/repositories/Trips.repository.test.ts index 6eb7d330d..2b6fb8efc 100644 --- a/server/tests/unit/db/repositories/Trips.repository.test.ts +++ b/server/tests/unit/db/repositories/Trips.repository.test.ts @@ -1,11 +1,12 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { CAN_ACCESS_TRIP_SQL, createTestDb, resetTestDb } from '../../../helpers/test-db'; +import { createSnapshotTestDb } from '../../../helpers/db-mock'; +import { CAN_ACCESS_TRIP_SQL, resetTestDb } from '../../../helpers/test-db'; import { createTestOrm, type TestOrm } from '../../../helpers/test-orm'; import { addTripMember, createTrip, createUser } from '../../../helpers/factories'; import { Trips } from '../../../../src/db/entities/Trips.entity'; import type { TripsRepository } from '../../../../src/db/repositories/Trips.repository'; -const testDb = createTestDb(); +const testDb = createSnapshotTestDb(); let t: TestOrm; let trips: TripsRepository; diff --git a/server/tests/unit/db/restore-migrates-forward.test.ts b/server/tests/unit/db/restore-migrates-forward.test.ts index 6b95dbfa2..214863577 100644 --- a/server/tests/unit/db/restore-migrates-forward.test.ts +++ b/server/tests/unit/db/restore-migrates-forward.test.ts @@ -36,11 +36,6 @@ function ormFor(dbName: string): Promise { migrations: { path: MIGRATIONS, pathTs: MIGRATIONS, snapshot: false, silent: true }, seeder: { path: SEEDERS, pathTs: SEEDERS, defaultSeeder: 'DatabaseSeeder' }, dbName, - // Same as MikroORM's own default, but declared here so the `import()` sits - // in a module vitest transforms. MikroORM's copy lives in node_modules and - // is loaded raw, where Node's resolver rejects the extensionless imports - // these CommonJS-target migrations use. - dynamicImportProvider: (id: string) => import(id), }); } diff --git a/server/tests/unit/db/test-orm.test.ts b/server/tests/unit/db/test-orm.test.ts index ca44e522f..d2cd0685d 100644 --- a/server/tests/unit/db/test-orm.test.ts +++ b/server/tests/unit/db/test-orm.test.ts @@ -1,10 +1,11 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { createTestDb, resetTestDb } from '../../helpers/test-db'; +import { createSnapshotTestDb } from '../../helpers/db-mock'; +import { resetTestDb } from '../../helpers/test-db'; import { createTestOrm, type TestOrm } from '../../helpers/test-orm'; import { createUser } from '../../helpers/factories'; import { Users } from '../../../src/db/entities/Users.entity'; -const testDb = createTestDb(); +const testDb = createSnapshotTestDb(); let t: TestOrm; beforeAll(async () => { t = await createTestOrm(testDb); }); diff --git a/server/tests/unit/db/types/db-timestamp.type.test.ts b/server/tests/unit/db/types/db-timestamp.type.test.ts index fbf863967..237f85697 100644 --- a/server/tests/unit/db/types/db-timestamp.type.test.ts +++ b/server/tests/unit/db/types/db-timestamp.type.test.ts @@ -1,9 +1,7 @@ import { describe, expect, it } from 'vitest'; import { DB_TIMESTAMP_RE, DbTimestampType, dbNow } from '../../../../src/db/types'; -import type { Platform } from '@mikro-orm/core'; - -// The type never touches the platform for value conversion; a stub is enough. -const platform = {} as Platform; +// The value converters take no platform: the wire format is the same text on +// every dialect (see db-timestamp.type.ts). const type = new DbTimestampType(); describe('dbNow', () => { @@ -18,29 +16,29 @@ describe('dbNow', () => { describe('DbTimestampType', () => { it('TS-003: passes stored text through unchanged on read', () => { - expect(type.convertToJSValue('2026-09-21 13:05:09', platform)).toBe('2026-09-21 13:05:09'); + expect(type.convertToJSValue('2026-09-21 13:05:09')).toBe('2026-09-21 13:05:09'); }); it('TS-004: formats a Date a driver hands back (Postgres later) to the same text', () => { - expect(type.convertToJSValue(new Date(Date.UTC(2026, 8, 21, 13, 5, 9)) as unknown as string, platform)) + expect(type.convertToJSValue(new Date(Date.UTC(2026, 8, 21, 13, 5, 9)) as unknown as string)) .toBe('2026-09-21 13:05:09'); }); it('TS-005: a millisecond integer (a Date written by the stock DateTimeType) reads back as text', () => { - expect(type.convertToJSValue(Date.UTC(2026, 8, 21, 13, 5, 9) as unknown as string, platform)) + expect(type.convertToJSValue(Date.UTC(2026, 8, 21, 13, 5, 9) as unknown as string)) .toBe('2026-09-21 13:05:09'); }); it('TS-006: writes text unchanged and a stray Date as text, never as a number', () => { - expect(type.convertToDatabaseValue('2026-09-21 13:05:09', platform)).toBe('2026-09-21 13:05:09'); - expect(type.convertToDatabaseValue(new Date(Date.UTC(2026, 8, 21, 13, 5, 9)) as unknown as string, platform)) + expect(type.convertToDatabaseValue('2026-09-21 13:05:09')).toBe('2026-09-21 13:05:09'); + expect(type.convertToDatabaseValue(new Date(Date.UTC(2026, 8, 21, 13, 5, 9)) as unknown as string)) .toBe('2026-09-21 13:05:09'); }); it('TS-007: null and undefined survive both directions', () => { - expect(type.convertToDatabaseValue(null, platform)).toBeNull(); - expect(type.convertToDatabaseValue(undefined, platform)).toBeUndefined(); - expect(type.convertToJSValue(null, platform)).toBeNull(); + expect(type.convertToDatabaseValue(null)).toBeNull(); + expect(type.convertToDatabaseValue(undefined)).toBeUndefined(); + expect(type.convertToJSValue(null)).toBeNull(); }); it('TS-008: compares as a string and reports a string runtime type', () => { diff --git a/server/tests/unit/nest/database/request-context.test.ts b/server/tests/unit/nest/database/request-context.test.ts index 55a6bd4cd..8c555de9b 100644 --- a/server/tests/unit/nest/database/request-context.test.ts +++ b/server/tests/unit/nest/database/request-context.test.ts @@ -1,11 +1,12 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { createTestDb, resetTestDb } from '../../../helpers/test-db'; +import { createSnapshotTestDb } from '../../../helpers/db-mock'; +import { resetTestDb } from '../../../helpers/test-db'; import { createTestOrm, type TestOrm } from '../../../helpers/test-orm'; import { createUser } from '../../../helpers/factories'; import { Users } from '../../../../src/db/entities/Users.entity'; import { withRequestContext } from '../../../../src/nest/database/request-context'; -const testDb = createTestDb(); +const testDb = createSnapshotTestDb(); let t: TestOrm; // Global context DISALLOWED here on purpose: this is the production setting. diff --git a/server/tests/unit/nest/database/unit-of-work.test.ts b/server/tests/unit/nest/database/unit-of-work.test.ts index d065826ff..f62603337 100644 --- a/server/tests/unit/nest/database/unit-of-work.test.ts +++ b/server/tests/unit/nest/database/unit-of-work.test.ts @@ -1,12 +1,13 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { createTestDb, resetTestDb } from '../../../helpers/test-db'; +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 { Days } from '../../../../src/db/entities/Days.entity'; import type { DaysRepository } from '../../../../src/db/repositories/Days.repository'; import { UnitOfWork } from '../../../../src/nest/database/unit-of-work'; -const testDb = createTestDb(); +const testDb = createSnapshotTestDb(); let t: TestOrm; let uow: UnitOfWork; let days: DaysRepository;