From ae2af05cad8eaa4a6de7a9eddfae29de6e7f0f64 Mon Sep 17 00:00:00 2001 From: jubnl Date: Tue, 22 Sep 2026 07:54:09 +0200 Subject: [PATCH] =?UTF-8?q?fix(server):=20phase=201=20review=20follow-ups?= =?UTF-8?q?=20=E2=80=94=20awaited=20returns,=20logged=20upload-filter=20re?= =?UTF-8?q?jections,=20covered=20handshake/notice=20branches,=20call-graph?= =?UTF-8?q?=20gate=20in=20CI,=20dead=20test=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 3 + server/scripts/db-call-graph.mjs | 4 +- .../accommodations/accommodations.service.ts | 2 +- server/src/nest/atlas/atlas.service.ts | 2 +- server/src/nest/backup/auto-backup.job.ts | 6 +- .../doc-sync/doc-sync-webhook.controller.ts | 13 ++- server/src/nest/files/files.controller.ts | 8 +- server/src/nest/journey/journey.controller.ts | 8 +- server/src/nest/realtime/realtime.gateway.ts | 4 +- .../school-holidays.service.ts | 8 +- .../src/nest/settings/settings.controller.ts | 2 +- server/src/systemNotices/service.ts | 2 +- server/tests/e2e/addons.e2e.test.ts | 2 - server/tests/e2e/admin.e2e.test.ts | 2 - server/tests/e2e/airports.e2e.test.ts | 2 - server/tests/e2e/assignments.e2e.test.ts | 2 - server/tests/e2e/atlas.e2e.test.ts | 2 - server/tests/e2e/auth.e2e.test.ts | 2 - server/tests/e2e/backup.e2e.test.ts | 2 - server/tests/e2e/budget.e2e.test.ts | 2 - .../e2e/calendar-feed-visibility.e2e.test.ts | 2 - server/tests/e2e/categories.e2e.test.ts | 2 - server/tests/e2e/collab.e2e.test.ts | 2 - server/tests/e2e/collections.e2e.test.ts | 2 - server/tests/e2e/dawarich.e2e.test.ts | 2 - server/tests/e2e/days.e2e.test.ts | 2 - server/tests/e2e/doc-sync.e2e.test.ts | 2 - server/tests/e2e/feeds.e2e.test.ts | 2 - server/tests/e2e/files.e2e.test.ts | 2 - server/tests/e2e/journey.e2e.test.ts | 2 - server/tests/e2e/maps.e2e.test.ts | 2 - server/tests/e2e/memories.e2e.test.ts | 2 - server/tests/e2e/notifications.e2e.test.ts | 2 - server/tests/e2e/oauth.e2e.test.ts | 2 - server/tests/e2e/oidc.e2e.test.ts | 2 - server/tests/e2e/packing.e2e.test.ts | 2 - server/tests/e2e/place-enrichment.e2e.test.ts | 2 - server/tests/e2e/place-shadow.e2e.test.ts | 2 - server/tests/e2e/places.e2e.test.ts | 2 - server/tests/e2e/public-api.e2e.test.ts | 2 - server/tests/e2e/reservations.e2e.test.ts | 2 - server/tests/e2e/roadtrip.e2e.test.ts | 2 - server/tests/e2e/school-holidays.e2e.test.ts | 2 - server/tests/e2e/settings.e2e.test.ts | 2 - server/tests/e2e/share.e2e.test.ts | 2 - server/tests/e2e/storage-admin.e2e.test.ts | 2 - server/tests/e2e/system-notices.e2e.test.ts | 2 - server/tests/e2e/tags.e2e.test.ts | 2 - server/tests/e2e/todo.e2e.test.ts | 2 - server/tests/e2e/transit-provider.e2e.test.ts | 2 - server/tests/e2e/transit.e2e.test.ts | 2 - server/tests/e2e/trip-invite.e2e.test.ts | 2 - server/tests/e2e/trips.e2e.test.ts | 2 - server/tests/e2e/vacay.e2e.test.ts | 2 - server/tests/e2e/weather.e2e.test.ts | 2 - .../tests/helpers/accommodations-service.ts | 1 - server/tests/helpers/mcp-test-controllers.ts | 1 - server/tests/helpers/plugin-host.ts | 1 - server/tests/integration/dayReorder.test.ts | 1 - .../plugins/plugin-runtime.test.ts | 4 +- .../tests/integration/systemNotices.test.ts | 41 +++++++ server/tests/unit/idempotency-cleanup.test.ts | 10 +- server/tests/unit/mcp/tools-airtrail.test.ts | 1 - .../unit/mcp/tools-budget-advanced.test.ts | 1 - .../tests/unit/mcp/tools-categories.test.ts | 1 - server/tests/unit/mcp/tools-prompts.test.ts | 1 - server/tests/unit/mcp/tools-roadtrip.test.ts | 1 - server/tests/unit/mcp/tools-transit.test.ts | 1 - .../unit/nest/accommodations.service.test.ts | 1 - ...n-default-user-settings.controller.test.ts | 11 ++ server/tests/unit/nest/admin.service.test.ts | 1 - .../nest/admin.version-notification.test.ts | 1 - .../unit/nest/assignments.service.test.ts | 7 +- server/tests/unit/nest/auth.service.test.ts | 1 - .../tests/unit/nest/budget.service.db.test.ts | 1 - .../tests/unit/nest/calendar.service.test.ts | 7 +- server/tests/unit/nest/collab.service.test.ts | 1 - .../unit/nest/collections.service.test.ts | 1 - .../nest/dawarich-suggestions.service.test.ts | 1 - server/tests/unit/nest/dawarich.mcp.test.ts | 10 +- .../tests/unit/nest/day-notes.service.test.ts | 1 - server/tests/unit/nest/days.service.test.ts | 1 - .../unit/nest/doc-sync/doc-sync.mcp.test.ts | 10 +- .../unit/nest/files/upload-filters.test.ts | 29 +++++ server/tests/unit/nest/google-route.test.ts | 1 - .../unit/nest/mcp-shared.service.test.ts | 1 - server/tests/unit/nest/oidc.service.test.ts | 1 - .../tests/unit/nest/passkey.service.test.ts | 1 - .../unit/nest/permissions.service.test.ts | 1 - .../tests/unit/nest/places.directions.test.ts | 1 - server/tests/unit/nest/places.service.test.ts | 1 - .../tests/unit/nest/realtime.gateway.test.ts | 39 +++++++ .../unit/nest/reservations.service.test.ts | 1 - server/tests/unit/nest/share.service.test.ts | 50 ++++----- server/tests/unit/nest/todo.service.test.ts | 1 - .../tests/unit/nest/trek-ws-adapter.test.ts | 20 ++-- .../unit/nest/trip-invite.service.test.ts | 1 - .../unit/nest/trip-members.service.test.ts | 1 - .../unit/nest/trip-read-model.service.test.ts | 1 - server/tests/unit/nest/trips.service.test.ts | 1 - .../unit/nest/user-cleanup.service.test.ts | 3 +- .../tests/unit/scripts/db-call-graph.test.ts | 100 ++++++++++++++++++ .../unit/services/airtrailImport.test.ts | 1 - .../unit/services/conflictUpdate.test.ts | 1 - server/vitest.config.ts | 5 + wiki/Real-Time-Collaboration.md | 2 +- wiki/Troubleshooting.md | 2 +- 107 files changed, 324 insertions(+), 209 deletions(-) create mode 100644 server/tests/unit/scripts/db-call-graph.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d7a54e81..bdc2ff274 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -183,6 +183,9 @@ jobs: - name: Lint run: cd server && npm run lint:check + - name: DB call-graph gate (no sync DB-touching methods, no raw transactions) + run: cd server && npm run db:call-graph -- --sync --tx + client-quality: name: Client Types & Lint runs-on: ubuntu-latest diff --git a/server/scripts/db-call-graph.mjs b/server/scripts/db-call-graph.mjs index 35d5948c4..9642616f0 100644 --- a/server/scripts/db-call-graph.mjs +++ b/server/scripts/db-call-graph.mjs @@ -86,8 +86,8 @@ function parseArgs(argv) { else if (a === '--json') opts.json = true; else if (a === '--domains') opts.domains = splitDomains(argv[++i]); else if (a.startsWith('--domains=')) opts.domains = splitDomains(a.slice('--domains='.length)); - // --root is intentionally undocumented: it exists so the __fixtures__ suite can point - // the whole analysis at a tiny sample tree instead of server/src. + // --root is intentionally undocumented: it exists so tests/unit/scripts/db-call-graph.test.ts + // can point the whole analysis at a tiny sample tree instead of server/src. else if (a === '--root') opts.root = argv[++i]; else if (a.startsWith('--root=')) opts.root = a.slice('--root='.length); else if (a === '--help' || a === '-h') opts.help = true; diff --git a/server/src/nest/accommodations/accommodations.service.ts b/server/src/nest/accommodations/accommodations.service.ts index 9405bc426..31f0b78c7 100644 --- a/server/src/nest/accommodations/accommodations.service.ts +++ b/server/src/nest/accommodations/accommodations.service.ts @@ -755,7 +755,7 @@ export class AccommodationsService { deletedBudgetItemIds: number[]; mirror: AccommodationMirror; }> { - return this.uow.transactional(async () => { + return await this.uow.transactional(async () => { const linkedRes = this.db.all<{ id: number }>('SELECT id FROM reservations WHERE accommodation_id = ?', Number(id)); const deletedBudgetItemIds: number[] = []; for (const res of linkedRes) { diff --git a/server/src/nest/atlas/atlas.service.ts b/server/src/nest/atlas/atlas.service.ts index aa83b6a79..64758bd37 100644 --- a/server/src/nest/atlas/atlas.service.ts +++ b/server/src/nest/atlas/atlas.service.ts @@ -594,7 +594,7 @@ export class AtlasService { * "3 countries added" is not counting the ones that were already there. */ async markCountry(userId: number, code: string, source: 'manual' | 'dawarich' = 'manual'): Promise { - return this.uow.transactional(async () => { + return await this.uow.transactional(async () => { const inserted = this.db .prepare('INSERT OR IGNORE INTO visited_countries (user_id, country_code, source) VALUES (?, ?, ?)') .run(userId, code, source).changes > 0; diff --git a/server/src/nest/backup/auto-backup.job.ts b/server/src/nest/backup/auto-backup.job.ts index bdcaef623..1111f0706 100644 --- a/server/src/nest/backup/auto-backup.job.ts +++ b/server/src/nest/backup/auto-backup.job.ts @@ -68,7 +68,11 @@ export class AutoBackupJob implements OnApplicationBootstrap { const settings = loadSettings(); if (settings.keep_days > 0) { - await cleanupOldBackups(this.storage, settings.keep_days); + try { + await cleanupOldBackups(this.storage, settings.keep_days); + } catch (err: unknown) { + logError(`Auto-Backup cleanup: ${err instanceof Error ? err.message : String(err)}`); + } } } diff --git a/server/src/nest/doc-sync/doc-sync-webhook.controller.ts b/server/src/nest/doc-sync/doc-sync-webhook.controller.ts index 025c73361..83884894b 100644 --- a/server/src/nest/doc-sync/doc-sync-webhook.controller.ts +++ b/server/src/nest/doc-sync/doc-sync-webhook.controller.ts @@ -118,13 +118,12 @@ export class DocSyncWebhookController implements OnModuleDestroy { const fresh = await reload(); if (!fresh || fresh.sync_enabled !== 1) return; if (!(await this.syncIsOn(fresh))) return; - void this.sync.syncLink(fresh).then((res) => { - // A run that was already in flight answers `busy`, and the changes this - // nudge was about may have landed after that run read the folder. Ask - // again once rather than waiting out a whole poll interval: once, and - // only for `busy`, so this cannot become a loop. - if (res?.state === 'busy' && !isRetry) this.schedule(linkId, reload, true); - }); + const res = await this.sync.syncLink(fresh); + // A run that was already in flight answers `busy`, and the changes this + // nudge was about may have landed after that run read the folder. Ask + // again once rather than waiting out a whole poll interval: once, and + // only for `busy`, so this cannot become a loop. + if (res?.state === 'busy' && !isRetry) this.schedule(linkId, reload, true); })().catch((err: unknown) => { logError(`Document sync webhook nudge failed for link ${linkId}: ${err instanceof Error ? err.message : String(err)}`); }); diff --git a/server/src/nest/files/files.controller.ts b/server/src/nest/files/files.controller.ts index 490053f5d..57aeb8a0e 100644 --- a/server/src/nest/files/files.controller.ts +++ b/server/src/nest/files/files.controller.ts @@ -32,6 +32,7 @@ import { Trip } from '../permissions/trip.decorator'; import { MAX_FILE_SIZE, BLOCKED_EXTENSIONS, isVideoExtension } from './files.constants'; import { FileUploadDto, FileUpdateDto, FileLinkDto } from './files.dto'; import { AllowedFileTypesService } from './allowed-file-types.service'; +import { logError } from '../audit/audit-log.logger'; /** * Trip-file upload filter, built from the container. @@ -59,7 +60,7 @@ export function filesUploadFileFilter(allowedTypes: AllowedFileTypesService): Op // rejection refuses the file, so the filter still fails closed. Only the // await is wrapped in try/catch, so a throw from cb() itself is never // re-routed into a second cb() call. - void (async () => { + (async () => { let allowed: string[]; try { allowed = (await allowedTypes.get()).split(',').map((e) => e.trim().toLowerCase()); @@ -70,7 +71,10 @@ export function filesUploadFileFilter(allowedTypes: AllowedFileTypesService): Op // Video is accepted as media regardless of the admin doc-types allowlist (#823). if (allowed.includes(fileExt) || isVideoExtension(fileExt) || (allowed.includes('*') && !BLOCKED_EXTENSIONS.includes(ext))) return cb(null, true); reject(); - })(); + })().catch((err: unknown) => { + logError(`files upload fileFilter: ${err instanceof Error ? err.message : String(err)}`); + cb(err instanceof Error ? err : new Error(String(err))); + }); }; } diff --git a/server/src/nest/journey/journey.controller.ts b/server/src/nest/journey/journey.controller.ts index 7563fc2d6..a6ac63fbf 100644 --- a/server/src/nest/journey/journey.controller.ts +++ b/server/src/nest/journey/journey.controller.ts @@ -41,6 +41,7 @@ import { } from './journey.dto'; import { isVideoMime, isVideoExtension, MAX_VIDEO_SIZE } from '../files/files.constants'; import { AllowedFileTypesService } from '../files/allowed-file-types.service'; +import { logError } from '../audit/audit-log.logger'; /** * One filename hook for all four journey upload routes (consumed by @@ -89,7 +90,7 @@ export function journeyImageFileFilter(allowedTypes: AllowedFileTypesService): O // refuses the file, so the filter still fails closed. Only the await is // wrapped in try/catch, so a throw from cb() itself is never re-routed // into a second cb() call. - void (async () => { + (async () => { let allowed: string[]; try { allowed = (await allowedTypes.get()).split(',').map((e) => e.trim().toLowerCase()); @@ -98,7 +99,10 @@ export function journeyImageFileFilter(allowedTypes: AllowedFileTypesService): O } if (!allowed.includes('*') && !allowed.includes(ext)) return reject(); cb(null, true); - })(); + })().catch((err: unknown) => { + logError(`journey photo fileFilter: ${err instanceof Error ? err.message : String(err)}`); + cb(err instanceof Error ? err : new Error(String(err))); + }); }; } diff --git a/server/src/nest/realtime/realtime.gateway.ts b/server/src/nest/realtime/realtime.gateway.ts index a7defb6de..08ca69330 100644 --- a/server/src/nest/realtime/realtime.gateway.ts +++ b/server/src/nest/realtime/realtime.gateway.ts @@ -91,7 +91,9 @@ export class RealtimeGateway * Every rejection closes with the code the client already handles: 4001 for * anything about identity, 4403 for the MFA policy. The order matters and is * unchanged — a missing token is refused before the store is touched, and the - * password-version gate runs before the MFA one. + * password-version gate runs before the MFA one. An unexpected error anywhere + * in the handshake is also closed as 4001, reason 'connection setup failed', + * instead of escaping as an uncaught exception. */ async handleConnection(socket: TrekWebSocket, request: IncomingMessage): Promise { // TrekWsAdapter.bindClientConnect fires this from a plain 'connection' diff --git a/server/src/nest/school-holidays/school-holidays.service.ts b/server/src/nest/school-holidays/school-holidays.service.ts index c1eeecfbc..0ec89a26c 100644 --- a/server/src/nest/school-holidays/school-holidays.service.ts +++ b/server/src/nest/school-holidays/school-holidays.service.ts @@ -30,7 +30,7 @@ export class SchoolHolidaysService { } async deleteCountry(code: string) { - return this.uow.transactional(async () => { + return await this.uow.transactional(async () => { await this.country(code); if (this.db.get('SELECT id FROM school_holiday_regions WHERE country = ? LIMIT 1', code)) { throw new ConflictException('Remove the regions before deleting this country'); @@ -59,7 +59,7 @@ export class SchoolHolidaysService { } async createRegion(country: string, body: SchoolHolidayRegionRequest) { - return this.uow.transactional(async () => { + return await this.uow.transactional(async () => { await this.country(country); await this.checkName(country, body.name, 0); if (body.revision !== 0) throw new ConflictException('New regions must have revision zero'); @@ -71,7 +71,7 @@ export class SchoolHolidaysService { } async updateRegion(id: number, body: SchoolHolidayRegionRequest) { - return this.uow.transactional(async () => { + return await this.uow.transactional(async () => { const region = await this.region(id); await this.checkName(region.country, body.name, id); const updated = this.db.run('UPDATE school_holiday_regions SET name = ?, revision = revision + 1 WHERE id = ? AND revision = ?', body.name, id, body.revision); @@ -82,7 +82,7 @@ export class SchoolHolidaysService { } async deleteRegion(id: number, revision: number) { - return this.uow.transactional(async () => { + return await this.uow.transactional(async () => { const region = await this.region(id); if (region.revision !== revision) throw new ConflictException('This region changed. Reload before deleting it.'); if (this.db.get("SELECT id FROM vacay_holiday_calendars WHERE type = 'school_holiday' AND region = ? LIMIT 1", region.code)) { diff --git a/server/src/nest/settings/settings.controller.ts b/server/src/nest/settings/settings.controller.ts index cf22933c2..10a314a66 100644 --- a/server/src/nest/settings/settings.controller.ts +++ b/server/src/nest/settings/settings.controller.ts @@ -132,7 +132,7 @@ export class AdminDefaultUserSettingsController { }); // Answer with the stored defaults, not the request body: the service normalises // and drops unknown keys, and the admin panel renders straight from this. - return this.settings.getAdminUserDefaults(); + return await this.settings.getAdminUserDefaults(); } catch (err) { throw new HttpException({ error: err instanceof Error ? err.message : String(err) }, 400); } diff --git a/server/src/systemNotices/service.ts b/server/src/systemNotices/service.ts index d5ab23c02..098477159 100644 --- a/server/src/systemNotices/service.ts +++ b/server/src/systemNotices/service.ts @@ -73,7 +73,7 @@ export async function getActiveNoticesFor( user: { ...user, noTrips: tripCount }, currentAppVersion, now, - addonEnabled: (addonId: string) => addonFlags.get(addonId) ?? false, + addonEnabled: (addonId: string) => addonFlags.get(addonId), managed, }; const appVer = semver.coerce(currentAppVersion)?.version ?? '0.0.0'; diff --git a/server/tests/e2e/addons.e2e.test.ts b/server/tests/e2e/addons.e2e.test.ts index 4d2378504..1c848a5f9 100644 --- a/server/tests/e2e/addons.e2e.test.ts +++ b/server/tests/e2e/addons.e2e.test.ts @@ -40,8 +40,6 @@ vi.mock('../../src/nest/memories/memories.helpers', () => ({ getPhotoProviderCon import { AddonsModule } from '../../src/nest/addons/addons.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('GET /api/addons e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/admin.e2e.test.ts b/server/tests/e2e/admin.e2e.test.ts index 653a68fc2..84b06af0e 100644 --- a/server/tests/e2e/admin.e2e.test.ts +++ b/server/tests/e2e/admin.e2e.test.ts @@ -92,8 +92,6 @@ import { SettingsModule } from '../../src/nest/settings/settings.module'; import { NotificationsModule } from '../../src/nest/notifications/notifications.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Admin e2e (real auth + admin guard + temp SQLite)', () => { diff --git a/server/tests/e2e/airports.e2e.test.ts b/server/tests/e2e/airports.e2e.test.ts index 06ce7116e..bb05c382c 100644 --- a/server/tests/e2e/airports.e2e.test.ts +++ b/server/tests/e2e/airports.e2e.test.ts @@ -30,8 +30,6 @@ vi.mock('../../src/nest/airports/airports.data', async (importActual) => { import { AirportsModule } from '../../src/nest/airports/airports.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; const BER = { diff --git a/server/tests/e2e/assignments.e2e.test.ts b/server/tests/e2e/assignments.e2e.test.ts index 8fac42ff9..8b4fb0348 100644 --- a/server/tests/e2e/assignments.e2e.test.ts +++ b/server/tests/e2e/assignments.e2e.test.ts @@ -75,8 +75,6 @@ let checkPermission: MockInstance; import { AssignmentsModule } from '../../src/nest/assignments/assignments.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Assignments e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/atlas.e2e.test.ts b/server/tests/e2e/atlas.e2e.test.ts index fc9993e32..a5dddab22 100644 --- a/server/tests/e2e/atlas.e2e.test.ts +++ b/server/tests/e2e/atlas.e2e.test.ts @@ -41,8 +41,6 @@ import { AtlasModule } from '../../src/nest/atlas/atlas.module'; import { DatabaseModule } from '../../src/nest/database/database.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Atlas e2e (real auth guard + real service + temp SQLite)', () => { diff --git a/server/tests/e2e/auth.e2e.test.ts b/server/tests/e2e/auth.e2e.test.ts index 7170901dc..0583a3e52 100644 --- a/server/tests/e2e/auth.e2e.test.ts +++ b/server/tests/e2e/auth.e2e.test.ts @@ -56,8 +56,6 @@ import { SessionRenewalInterceptor } from '../../src/nest/auth/session-renewal.i import { DatabaseModule } from '../../src/nest/database/database.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Auth e2e (real auth guard + real service + real cookie service + temp SQLite)', () => { diff --git a/server/tests/e2e/backup.e2e.test.ts b/server/tests/e2e/backup.e2e.test.ts index cbaf2a31e..2aba69d83 100644 --- a/server/tests/e2e/backup.e2e.test.ts +++ b/server/tests/e2e/backup.e2e.test.ts @@ -47,8 +47,6 @@ vi.mock('../../src/nest/backup/backup.impl', () => backupSvc); import { BackupModule } from '../../src/nest/backup/backup.module'; import { DatabaseModule } from '../../src/nest/database/database.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Backup e2e (real auth + admin guard + temp SQLite)', () => { diff --git a/server/tests/e2e/budget.e2e.test.ts b/server/tests/e2e/budget.e2e.test.ts index 283a2a3b5..0c1bc6c43 100644 --- a/server/tests/e2e/budget.e2e.test.ts +++ b/server/tests/e2e/budget.e2e.test.ts @@ -46,8 +46,6 @@ import { BudgetModule } from '../../src/nest/budget/budget.module'; import { ExchangeRatesService } from '../../src/nest/budget/exchange-rates.service'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Budget e2e (real auth guard + temp SQLite, real budget SQL)', () => { diff --git a/server/tests/e2e/calendar-feed-visibility.e2e.test.ts b/server/tests/e2e/calendar-feed-visibility.e2e.test.ts index 74a5afc1e..704a1ace5 100644 --- a/server/tests/e2e/calendar-feed-visibility.e2e.test.ts +++ b/server/tests/e2e/calendar-feed-visibility.e2e.test.ts @@ -36,8 +36,6 @@ import { runMigrations } from '../../src/db/migrations'; import { DatabaseModule } from '../../src/nest/database/database.module'; import { FeedsModule } from '../../src/nest/feeds/feeds.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Calendar feed visibility e2e (real CalendarService over temp SQLite)', () => { diff --git a/server/tests/e2e/categories.e2e.test.ts b/server/tests/e2e/categories.e2e.test.ts index bd48cd8cc..d122110a1 100644 --- a/server/tests/e2e/categories.e2e.test.ts +++ b/server/tests/e2e/categories.e2e.test.ts @@ -37,8 +37,6 @@ import { DatabaseModule } from '../../src/nest/database/database.module'; import { RealtimeModule } from '../../src/nest/realtime/realtime.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; function insertCategory(name: string, color = '#6366f1', icon = '📍', userId = 1): number { diff --git a/server/tests/e2e/collab.e2e.test.ts b/server/tests/e2e/collab.e2e.test.ts index 52c41fb7c..c9c2de0e4 100644 --- a/server/tests/e2e/collab.e2e.test.ts +++ b/server/tests/e2e/collab.e2e.test.ts @@ -76,8 +76,6 @@ import { CollabModule } from '../../src/nest/collab/collab.module'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { RateLimitService } from '../../src/nest/common/rate-limit.service'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Collab e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/collections.e2e.test.ts b/server/tests/e2e/collections.e2e.test.ts index 3fb693690..cb86fa9fd 100644 --- a/server/tests/e2e/collections.e2e.test.ts +++ b/server/tests/e2e/collections.e2e.test.ts @@ -45,8 +45,6 @@ import { RealtimeModule } from '../../src/nest/realtime/realtime.module'; import { AddonsService } from '../../src/nest/addons/addons.service'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Collections e2e (real auth guard + real service + temp SQLite)', () => { diff --git a/server/tests/e2e/dawarich.e2e.test.ts b/server/tests/e2e/dawarich.e2e.test.ts index 2c6509712..e1bdc0325 100644 --- a/server/tests/e2e/dawarich.e2e.test.ts +++ b/server/tests/e2e/dawarich.e2e.test.ts @@ -59,8 +59,6 @@ import { DatabaseModule } from '../../src/nest/database/database.module'; import { AddonsService } from '../../src/nest/addons/addons.service'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; /** diff --git a/server/tests/e2e/days.e2e.test.ts b/server/tests/e2e/days.e2e.test.ts index 59a2e8c9a..eea315fba 100644 --- a/server/tests/e2e/days.e2e.test.ts +++ b/server/tests/e2e/days.e2e.test.ts @@ -90,8 +90,6 @@ import { DaysModule } from '../../src/nest/days/days.module'; import { DayNotesModule } from '../../src/nest/day-notes/day-notes.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Days + day-notes e2e (real auth guard + temp SQLite, real day SQL)', () => { diff --git a/server/tests/e2e/doc-sync.e2e.test.ts b/server/tests/e2e/doc-sync.e2e.test.ts index 7239f0488..8caed6d49 100644 --- a/server/tests/e2e/doc-sync.e2e.test.ts +++ b/server/tests/e2e/doc-sync.e2e.test.ts @@ -83,8 +83,6 @@ import { SynologyDriveDocumentProvider } from '../../src/nest/doc-sync/providers import { NextcloudDocumentProvider, OpencloudDocumentProvider } from '../../src/nest/doc-sync/providers/webdav.provider'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; /** A provider that answers plausibly but never opens a socket. */ diff --git a/server/tests/e2e/feeds.e2e.test.ts b/server/tests/e2e/feeds.e2e.test.ts index c541c9deb..ec9b201af 100644 --- a/server/tests/e2e/feeds.e2e.test.ts +++ b/server/tests/e2e/feeds.e2e.test.ts @@ -77,8 +77,6 @@ import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter import { AppConfigModule } from '../../src/nest/app-config/app-config.module'; import { GlobalAuthGuard } from '../../src/nest/auth/global-auth.guard'; import { MfaPolicyGuard } from '../../src/nest/auth/mfa-policy.guard'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; const BASE = 'https://trek.example.test'; diff --git a/server/tests/e2e/files.e2e.test.ts b/server/tests/e2e/files.e2e.test.ts index 0998d8aa7..edbbcbc2c 100644 --- a/server/tests/e2e/files.e2e.test.ts +++ b/server/tests/e2e/files.e2e.test.ts @@ -81,8 +81,6 @@ import { RealtimeModule } from '../../src/nest/realtime/realtime.module'; import { FilesModule } from '../../src/nest/files/files.module'; import { PhotosModule } from '../../src/nest/photos/photos.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Files + photos e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/journey.e2e.test.ts b/server/tests/e2e/journey.e2e.test.ts index 0db0ce545..15413f577 100644 --- a/server/tests/e2e/journey.e2e.test.ts +++ b/server/tests/e2e/journey.e2e.test.ts @@ -70,8 +70,6 @@ import { JourneyModule } from '../../src/nest/journey/journey.module'; import { AddonsService } from '../../src/nest/addons/addons.service'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Journey e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/maps.e2e.test.ts b/server/tests/e2e/maps.e2e.test.ts index b26dd4120..ce78f9c80 100644 --- a/server/tests/e2e/maps.e2e.test.ts +++ b/server/tests/e2e/maps.e2e.test.ts @@ -30,8 +30,6 @@ import { MapsService } from '../../src/nest/maps/maps.service'; import { DatabaseModule } from '../../src/nest/database/database.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Maps e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/memories.e2e.test.ts b/server/tests/e2e/memories.e2e.test.ts index e0167f3e8..dea867fa8 100644 --- a/server/tests/e2e/memories.e2e.test.ts +++ b/server/tests/e2e/memories.e2e.test.ts @@ -67,8 +67,6 @@ import { SynologyService } from '../../src/nest/memories/synology.service'; import { MemoriesAccessService } from '../../src/nest/memories/memories-access.service'; import { RealtimeModule } from '../../src/nest/realtime/realtime.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; const BASE = '/api/integrations/memories'; diff --git a/server/tests/e2e/notifications.e2e.test.ts b/server/tests/e2e/notifications.e2e.test.ts index 2bb9d296a..76828c474 100644 --- a/server/tests/e2e/notifications.e2e.test.ts +++ b/server/tests/e2e/notifications.e2e.test.ts @@ -70,8 +70,6 @@ import { NotificationPreferencesService } from '../../src/nest/notifications/not import { NtfyService } from '../../src/nest/notifications/transports/ntfy.service'; import { WebhookService } from '../../src/nest/notifications/transports/webhook.service'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; function seedNotification(recipientId: number, overrides: { is_read?: number } = {}): number { diff --git a/server/tests/e2e/oauth.e2e.test.ts b/server/tests/e2e/oauth.e2e.test.ts index 2f186168c..1feffb007 100644 --- a/server/tests/e2e/oauth.e2e.test.ts +++ b/server/tests/e2e/oauth.e2e.test.ts @@ -56,8 +56,6 @@ import { OauthService } from '../../src/nest/oauth/oauth.service'; import { AddonsService } from '../../src/nest/addons/addons.service'; import { DatabaseModule } from '../../src/nest/database/database.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('OAuth e2e (real guards + temp SQLite)', () => { diff --git a/server/tests/e2e/oidc.e2e.test.ts b/server/tests/e2e/oidc.e2e.test.ts index c74942951..9af1ec102 100644 --- a/server/tests/e2e/oidc.e2e.test.ts +++ b/server/tests/e2e/oidc.e2e.test.ts @@ -50,8 +50,6 @@ import { OidcService } from '../../src/nest/oidc/oidc.service'; import { DatabaseModule } from '../../src/nest/database/database.module'; import { AuthService } from '../../src/nest/auth/auth.service'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('OIDC e2e (real cookie service)', () => { diff --git a/server/tests/e2e/packing.e2e.test.ts b/server/tests/e2e/packing.e2e.test.ts index 7f94b6d60..91afe98de 100644 --- a/server/tests/e2e/packing.e2e.test.ts +++ b/server/tests/e2e/packing.e2e.test.ts @@ -125,8 +125,6 @@ import { DatabaseModule } from '../../src/nest/database/database.module'; import { RealtimeModule } from '../../src/nest/realtime/realtime.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; function insertItem(tripId: number, name: string, extra: Partial<{ sort_order: number; category: string; is_private: number; owner_id: number }> = {}): number { diff --git a/server/tests/e2e/place-enrichment.e2e.test.ts b/server/tests/e2e/place-enrichment.e2e.test.ts index af0373944..fd35444d3 100644 --- a/server/tests/e2e/place-enrichment.e2e.test.ts +++ b/server/tests/e2e/place-enrichment.e2e.test.ts @@ -37,8 +37,6 @@ import { DatabaseModule } from '../../src/nest/database/database.module'; import { RateLimitService } from '../../src/nest/common/rate-limit.service'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; const BODY = { lat: 50.9, lng: 6.96, name: 'Museum Ludwig', placeId: 'way:12345' }; diff --git a/server/tests/e2e/place-shadow.e2e.test.ts b/server/tests/e2e/place-shadow.e2e.test.ts index 6af0b3dfa..f18ab6270 100644 --- a/server/tests/e2e/place-shadow.e2e.test.ts +++ b/server/tests/e2e/place-shadow.e2e.test.ts @@ -40,8 +40,6 @@ vi.mock('../../src/db/database', () => ({ import { PlaceShadowModule } from '../../src/nest/place-shadow/place-shadow.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; const PICK = { diff --git a/server/tests/e2e/places.e2e.test.ts b/server/tests/e2e/places.e2e.test.ts index f6ea954d1..11359fd78 100644 --- a/server/tests/e2e/places.e2e.test.ts +++ b/server/tests/e2e/places.e2e.test.ts @@ -85,8 +85,6 @@ import { PlacesModule } from '../../src/nest/places/places.module'; import { PlacesService } from '../../src/nest/places/places.service'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Places e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/public-api.e2e.test.ts b/server/tests/e2e/public-api.e2e.test.ts index 9ea7eff2d..773cdd642 100644 --- a/server/tests/e2e/public-api.e2e.test.ts +++ b/server/tests/e2e/public-api.e2e.test.ts @@ -86,8 +86,6 @@ import { TokensModule } from '../../src/nest/tokens/tokens.module'; import { PublicStatsController } from '../../src/nest/atlas/public-stats.controller'; import { AtlasService } from '../../src/nest/atlas/atlas.service'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; /** Mints a token the way TokenService does, so the guard's hash lookup is real. */ diff --git a/server/tests/e2e/reservations.e2e.test.ts b/server/tests/e2e/reservations.e2e.test.ts index 2a6ef4495..a3e2b880e 100644 --- a/server/tests/e2e/reservations.e2e.test.ts +++ b/server/tests/e2e/reservations.e2e.test.ts @@ -57,8 +57,6 @@ let checkPermission: MockInstance; import { createTables } from '../../src/db/schema'; import { runMigrations } from '../../src/db/migrations'; import { NotificationsService } from '../../src/nest/notifications/notifications.service'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Reservations + accommodations e2e (real auth guard + temp SQLite, real reservation SQL)', () => { diff --git a/server/tests/e2e/roadtrip.e2e.test.ts b/server/tests/e2e/roadtrip.e2e.test.ts index 21a2bf516..4586f3a96 100644 --- a/server/tests/e2e/roadtrip.e2e.test.ts +++ b/server/tests/e2e/roadtrip.e2e.test.ts @@ -28,8 +28,6 @@ import cookieParser from 'cookie-parser'; import type { Server } from 'http'; import request from 'supertest'; import { describe, it, expect, beforeAll, afterAll, beforeEach, vi, type MockInstance } from 'vitest'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; const { db } = vi.hoisted(() => { diff --git a/server/tests/e2e/school-holidays.e2e.test.ts b/server/tests/e2e/school-holidays.e2e.test.ts index b16472b99..2b01fd283 100644 --- a/server/tests/e2e/school-holidays.e2e.test.ts +++ b/server/tests/e2e/school-holidays.e2e.test.ts @@ -22,8 +22,6 @@ import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter import { validateBodyContracts } from '../../src/nest/common/validate-body-contracts'; import { createTestRegistry } from '../../src/nest-mcp'; import { trekMcpAccessPolicy, trekMcpValidateAccess } from '../../src/mcp/nest-mcp-policy'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; const base = '/api/school-holiday-catalog'; diff --git a/server/tests/e2e/settings.e2e.test.ts b/server/tests/e2e/settings.e2e.test.ts index 08b3df828..aea764ad4 100644 --- a/server/tests/e2e/settings.e2e.test.ts +++ b/server/tests/e2e/settings.e2e.test.ts @@ -31,8 +31,6 @@ vi.mock('../../src/db/database', () => ({ db, closeDb: () => {}, reinitialize: ( import { SettingsModule } from '../../src/nest/settings/settings.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Settings e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/share.e2e.test.ts b/server/tests/e2e/share.e2e.test.ts index 02bd65eaa..368e27f2f 100644 --- a/server/tests/e2e/share.e2e.test.ts +++ b/server/tests/e2e/share.e2e.test.ts @@ -58,8 +58,6 @@ import { PlacePhotoCacheService } from '../../src/nest/place-photos/place-photo- import { DatabaseModule } from '../../src/nest/database/database.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Share-link e2e (real auth guard + real SQL over temp SQLite)', () => { diff --git a/server/tests/e2e/storage-admin.e2e.test.ts b/server/tests/e2e/storage-admin.e2e.test.ts index 5f170fa2e..26b4b9e26 100644 --- a/server/tests/e2e/storage-admin.e2e.test.ts +++ b/server/tests/e2e/storage-admin.e2e.test.ts @@ -57,8 +57,6 @@ vi.mock('../../src/config', () => ({ ENCRYPTION_KEY: 'e2e-storage-key', JWT_SECR import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Storage admin e2e (real auth + admin guard + managed guard + temp SQLite)', () => { diff --git a/server/tests/e2e/system-notices.e2e.test.ts b/server/tests/e2e/system-notices.e2e.test.ts index b948095f7..991e2933d 100644 --- a/server/tests/e2e/system-notices.e2e.test.ts +++ b/server/tests/e2e/system-notices.e2e.test.ts @@ -33,8 +33,6 @@ vi.mock('../../src/systemNotices/service', () => ({ import { SystemNoticesModule } from '../../src/nest/system-notices/system-notices.module'; import { DatabaseModule } from '../../src/nest/database/database.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; const notice = { diff --git a/server/tests/e2e/tags.e2e.test.ts b/server/tests/e2e/tags.e2e.test.ts index f2007c3e0..0f59ea507 100644 --- a/server/tests/e2e/tags.e2e.test.ts +++ b/server/tests/e2e/tags.e2e.test.ts @@ -37,8 +37,6 @@ vi.mock('../../src/db/database', () => ({ db, closeDb: () => {}, reinitialize: ( import { DatabaseModule } from '../../src/nest/database/database.module'; import { TagsModule } from '../../src/nest/tags/tags.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; function insertTag(userId: number, name: string, color = '#10b981'): number { diff --git a/server/tests/e2e/todo.e2e.test.ts b/server/tests/e2e/todo.e2e.test.ts index 58b1c4843..69d41093b 100644 --- a/server/tests/e2e/todo.e2e.test.ts +++ b/server/tests/e2e/todo.e2e.test.ts @@ -77,8 +77,6 @@ import { DatabaseModule } from '../../src/nest/database/database.module'; import { RealtimeModule } from '../../src/nest/realtime/realtime.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; function insertItem(tripId: number, name: string, extra: Partial<{ sort_order: number; due_date: string }> = {}): number { diff --git a/server/tests/e2e/transit-provider.e2e.test.ts b/server/tests/e2e/transit-provider.e2e.test.ts index 669fdd29e..7194e901b 100644 --- a/server/tests/e2e/transit-provider.e2e.test.ts +++ b/server/tests/e2e/transit-provider.e2e.test.ts @@ -32,8 +32,6 @@ import { DatabaseModule } from '../../src/nest/database/database.module'; import { RealtimeModule } from '../../src/nest/realtime/realtime.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { clearGoogleTransitCache } from '../../src/nest/transit/google-transit.provider'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; const ADMIN = 1; diff --git a/server/tests/e2e/transit.e2e.test.ts b/server/tests/e2e/transit.e2e.test.ts index 8c7def042..a2c0e8675 100644 --- a/server/tests/e2e/transit.e2e.test.ts +++ b/server/tests/e2e/transit.e2e.test.ts @@ -30,8 +30,6 @@ import { TransitService } from '../../src/nest/transit/transit.service'; import { DatabaseModule } from '../../src/nest/database/database.module'; import { RealtimeModule } from '../../src/nest/realtime/realtime.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Transit proxy e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/trip-invite.e2e.test.ts b/server/tests/e2e/trip-invite.e2e.test.ts index 3b8cc365d..af58749b7 100644 --- a/server/tests/e2e/trip-invite.e2e.test.ts +++ b/server/tests/e2e/trip-invite.e2e.test.ts @@ -62,8 +62,6 @@ import { TripInviteModule } from '../../src/nest/trip-invite/trip-invite.module' import { TripMembershipService } from '../../src/nest/trip-membership/trip-membership.service'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Trip invite-link e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/trips.e2e.test.ts b/server/tests/e2e/trips.e2e.test.ts index f1c85f5fa..e2d9a2c45 100644 --- a/server/tests/e2e/trips.e2e.test.ts +++ b/server/tests/e2e/trips.e2e.test.ts @@ -132,8 +132,6 @@ let checkPermission: MockInstance; import { BudgetService } from '../../src/nest/budget/budget.service'; import { TripsModule } from '../../src/nest/trips/trips.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Trips e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/vacay.e2e.test.ts b/server/tests/e2e/vacay.e2e.test.ts index b871d9dbb..cc63222ba 100644 --- a/server/tests/e2e/vacay.e2e.test.ts +++ b/server/tests/e2e/vacay.e2e.test.ts @@ -79,8 +79,6 @@ import { VacayModule } from '../../src/nest/vacay/vacay.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe'; import { broadcastToUser } from '../../src/websocket'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Vacay e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/e2e/weather.e2e.test.ts b/server/tests/e2e/weather.e2e.test.ts index 2b1c7120e..0a45985c2 100644 --- a/server/tests/e2e/weather.e2e.test.ts +++ b/server/tests/e2e/weather.e2e.test.ts @@ -30,8 +30,6 @@ vi.mock('../../src/nest/weather/weather.impl', async (importActual) => { import { WeatherModule } from '../../src/nest/weather/weather.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; -import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { TestUnitOfWorkModule } from '../helpers/test-uow'; describe('Weather e2e (real auth guard + temp SQLite)', () => { diff --git a/server/tests/helpers/accommodations-service.ts b/server/tests/helpers/accommodations-service.ts index 3ac7e891e..fdc13071b 100644 --- a/server/tests/helpers/accommodations-service.ts +++ b/server/tests/helpers/accommodations-service.ts @@ -8,7 +8,6 @@ import { QueryHelpersService } from '../../src/nest/query-helpers/query-helpers. import { RealtimeService } from '../../src/nest/realtime/realtime.service'; import { TrekPhotosRepository } from '../../src/nest/photos/trek-photos.repository'; import { createTestUnitOfWork } from './test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; /** * AccommodationsService over a test connection. diff --git a/server/tests/helpers/mcp-test-controllers.ts b/server/tests/helpers/mcp-test-controllers.ts index fa66293e4..8e864bc07 100644 --- a/server/tests/helpers/mcp-test-controllers.ts +++ b/server/tests/helpers/mcp-test-controllers.ts @@ -115,7 +115,6 @@ import { ReservationImportMcp } from '../../src/nest/reservation-import/reservat import { HelpMcp } from '../../src/nest/help/help.mcp'; import { AddonsMcp } from '../../src/nest/addons/addons.mcp'; import { createTestUnitOfWork } from './test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; /** * Hand-wired counterpart of the boot-time discovery in McpRegistryService, diff --git a/server/tests/helpers/plugin-host.ts b/server/tests/helpers/plugin-host.ts index 65291e1af..ea423cdfb 100644 --- a/server/tests/helpers/plugin-host.ts +++ b/server/tests/helpers/plugin-host.ts @@ -73,7 +73,6 @@ import { TrekPhotosRepository } from '../../src/nest/photos/trek-photos.reposito import { RuntimeEnvService } from '../../src/nest/app-config/runtime-env.service'; import { makeStorageFixture } from './storage-fixture'; import { createTestUnitOfWork } from './test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; /** * Hand-wired counterpart of the PluginsModule DI graph for no-Nest tests diff --git a/server/tests/integration/dayReorder.test.ts b/server/tests/integration/dayReorder.test.ts index 9ff0816cf..48cdd2d4e 100644 --- a/server/tests/integration/dayReorder.test.ts +++ b/server/tests/integration/dayReorder.test.ts @@ -26,7 +26,6 @@ import { DaysService, DayReorderError } from '../../src/nest/days/days.service'; import { RealtimeService } from '../../src/nest/realtime/realtime.service'; import { QueryHelpersService } from '../../src/nest/query-helpers/query-helpers.service'; import { createTestUnitOfWork } from '../helpers/test-uow'; -import { UnitOfWork } from '../../src/nest/database/unit-of-work'; let svc: DaysService; beforeAll(async () => { diff --git a/server/tests/integration/plugins/plugin-runtime.test.ts b/server/tests/integration/plugins/plugin-runtime.test.ts index 687e7caf9..1e11ed587 100644 --- a/server/tests/integration/plugins/plugin-runtime.test.ts +++ b/server/tests/integration/plugins/plugin-runtime.test.ts @@ -208,7 +208,7 @@ describe('PluginRuntimeService (M2 end-to-end)', () => { // erasure: enqueue (durable) + drain → the plugin deletes its rows and the queue clears // eslint-disable-next-line @typescript-eslint/no-explicit-any - (runtime as any).enqueueUserErasure(5); + await (runtime as any).enqueueUserErasure(5); // eslint-disable-next-line @typescript-eslint/no-explicit-any await (runtime as any).drainUserErasures(); expect(testDb.prepare("SELECT COUNT(*) c FROM plugin_user_erasure_queue WHERE plugin_id='gdpr'").get()).toMatchObject({ c: 0 }); @@ -245,7 +245,7 @@ describe('PluginRuntimeService (M2 end-to-end)', () => { // granted the hook but NOT active → enqueue keeps the row, drain leaves it testDb.prepare("INSERT INTO plugins (id, status, permissions, config) VALUES ('offliner','inactive','[\"db:own\",\"hook:user-data\"]','{}')").run(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - (runtime as any).enqueueUserErasure(9); + await (runtime as any).enqueueUserErasure(9); // eslint-disable-next-line @typescript-eslint/no-explicit-any await (runtime as any).drainUserErasures(); expect(testDb.prepare("SELECT COUNT(*) c FROM plugin_user_erasure_queue WHERE plugin_id='offliner' AND user_id=9").get()).toMatchObject({ c: 1 }); diff --git a/server/tests/integration/systemNotices.test.ts b/server/tests/integration/systemNotices.test.ts index 2395a0b2b..9ef8e3bd7 100644 --- a/server/tests/integration/systemNotices.test.ts +++ b/server/tests/integration/systemNotices.test.ts @@ -33,6 +33,7 @@ import { authCookie } from '../helpers/auth'; import { SYSTEM_NOTICES } from '../../src/systemNotices/registry'; import { getCurrentAppVersion } from '../../src/systemNotices/service'; import type { SystemNotice } from '../../src/systemNotices/types'; +import { ADDON_IDS } from '../../src/addons'; let nestApp: INestApplication; let app: Application; @@ -50,6 +51,21 @@ const TEST_NOTICE: SystemNotice = { priority: 0, }; +// Test notice gated on an addon, so the addonFlags lookup in getActiveNoticesFor +// (built once per call from every addonEnabled condition in SYSTEM_NOTICES) is +// exercised for both the enabled and the disabled case. +const TEST_NOTICE_ADDON: SystemNotice = { + id: 'test-addon-notice', + display: 'modal', + severity: 'info', + titleKey: 'system_notice.test_addon_notice.title', + bodyKey: 'system_notice.test_addon_notice.body', + dismissible: true, + conditions: [{ kind: 'addonEnabled', addonId: ADDON_IDS.JOURNEY }], + publishedAt: '2026-01-01T00:00:00Z', + priority: 0, +}; + beforeAll(async () => { nestApp = await buildApp(); app = nestApp.getHttpAdapter().getInstance(); @@ -119,6 +135,31 @@ describe('GET /api/system-notices/active', () => { } }); + it('an addonEnabled-gated notice appears only while the addon is on', async () => { + SYSTEM_NOTICES.push(TEST_NOTICE_ADDON); + try { + const { user } = createUser(testDb); + testDb.prepare('UPDATE addons SET enabled = 0 WHERE id = ?').run(ADDON_IDS.JOURNEY); + + const off = await request(app) + .get('/api/system-notices/active') + .set('Cookie', authCookie(user.id)); + expect(off.status).toBe(200); + expect(off.body.find((n: { id: string }) => n.id === TEST_NOTICE_ADDON.id)).toBeUndefined(); + + testDb.prepare('UPDATE addons SET enabled = 1 WHERE id = ?').run(ADDON_IDS.JOURNEY); + + const on = await request(app) + .get('/api/system-notices/active') + .set('Cookie', authCookie(user.id)); + expect(on.status).toBe(200); + expect(on.body.find((n: { id: string }) => n.id === TEST_NOTICE_ADDON.id)).toBeDefined(); + } finally { + const idx = SYSTEM_NOTICES.indexOf(TEST_NOTICE_ADDON); + if (idx !== -1) SYSTEM_NOTICES.splice(idx, 1); + } + }); + it('does not return firstLogin notice for user with login_count > 1', async () => { SYSTEM_NOTICES.push(TEST_NOTICE); try { diff --git a/server/tests/unit/idempotency-cleanup.test.ts b/server/tests/unit/idempotency-cleanup.test.ts index d51d12889..a0be7e32f 100644 --- a/server/tests/unit/idempotency-cleanup.test.ts +++ b/server/tests/unit/idempotency-cleanup.test.ts @@ -74,7 +74,7 @@ describe('IdempotencyCleanupJob', () => { return { job, registrar }; } - it('registers the nightly 3 AM cron, and stays out of the registry under the test gate', () => { + it('registers the nightly 3 AM cron, and stays out of the registry under the test gate', async () => { const on = makeJob(); on.job.onApplicationBootstrap(); expect(on.registrar.register).toHaveBeenCalledWith('idempotency-cleanup', '0 3 * * *', expect.any(Function)); @@ -82,7 +82,7 @@ describe('IdempotencyCleanupJob', () => { // The registered callback IS the tick — drive it once over an empty table // (nothing to purge → no log, no throw). const onTick = on.registrar.register.mock.calls[0][2]; - expect(() => onTick()).not.toThrow(); + await expect(onTick()).resolves.toBeUndefined(); const off = makeJob(false); off.job.onApplicationBootstrap(); @@ -107,14 +107,14 @@ describe('IdempotencyCleanupJob', () => { expect(keys).toEqual(['fresh']); }); - it('a failing purge is contained to the Idempotency cleanup log line', () => { + it('a failing purge is contained to the Idempotency cleanup log line', async () => { const broken = { prepare: () => { throw new Error('db gone'); } } as unknown as DatabaseService; const job = new IdempotencyCleanupJob(broken, { isEnabled: () => true } as unknown as CronRegistrarService); - expect(() => job.tick()).not.toThrow(); + await expect(job.tick()).resolves.toBeUndefined(); // Non-Error throws are stringified rather than crashing the catch itself. const brokenString = { prepare: () => { throw 'db string'; } } as unknown as DatabaseService; const job2 = new IdempotencyCleanupJob(brokenString, { isEnabled: () => true } as unknown as CronRegistrarService); - expect(() => job2.tick()).not.toThrow(); + await expect(job2.tick()).resolves.toBeUndefined(); }); }); diff --git a/server/tests/unit/mcp/tools-airtrail.test.ts b/server/tests/unit/mcp/tools-airtrail.test.ts index 8cb8dce93..453f89c92 100644 --- a/server/tests/unit/mcp/tools-airtrail.test.ts +++ b/server/tests/unit/mcp/tools-airtrail.test.ts @@ -50,7 +50,6 @@ import { AirtrailImportService } from '../../../src/nest/integrations/airtrail-i import { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import { DatabaseService } from '../../../src/nest/database/database.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // The permissions cache is module-scoped, so a write through any instance is // what the tool's own checkPermission call reads back. diff --git a/server/tests/unit/mcp/tools-budget-advanced.test.ts b/server/tests/unit/mcp/tools-budget-advanced.test.ts index cc36bb93b..501bce89d 100644 --- a/server/tests/unit/mcp/tools-budget-advanced.test.ts +++ b/server/tests/unit/mcp/tools-budget-advanced.test.ts @@ -48,7 +48,6 @@ import { RealtimeService } from '../../../src/nest/realtime/realtime.service'; import type { TripAccess } from '../../../src/nest/database/database.service'; import type { User } from '../../../src/types'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; beforeAll(() => { createTables(testDb); diff --git a/server/tests/unit/mcp/tools-categories.test.ts b/server/tests/unit/mcp/tools-categories.test.ts index f5371d335..e88fa39b3 100644 --- a/server/tests/unit/mcp/tools-categories.test.ts +++ b/server/tests/unit/mcp/tools-categories.test.ts @@ -51,7 +51,6 @@ import { PermissionsService } from '../../../src/nest/permissions/permissions.se import { RealtimeService } from '../../../src/nest/realtime/realtime.service'; import { McpToolGuardsService } from '../../../src/nest/mcp-shared/mcp-tool-guards.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; beforeAll(() => { createTables(testDb); diff --git a/server/tests/unit/mcp/tools-prompts.test.ts b/server/tests/unit/mcp/tools-prompts.test.ts index 3a7d14fe9..8314b0a2b 100644 --- a/server/tests/unit/mcp/tools-prompts.test.ts +++ b/server/tests/unit/mcp/tools-prompts.test.ts @@ -88,7 +88,6 @@ import type { CollabService } from '../../../src/nest/collab/collab.service'; import { AddonsService } from '../../../src/nest/addons/addons.service'; import { notificationsStub } from '../../helpers/notifications'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // The trip-summary prompt moved to the DI-discovered TripsMcp — its cases below // exercise it through a hand-built registry over a stub TripsService whose diff --git a/server/tests/unit/mcp/tools-roadtrip.test.ts b/server/tests/unit/mcp/tools-roadtrip.test.ts index de94de44f..5b81cd45f 100644 --- a/server/tests/unit/mcp/tools-roadtrip.test.ts +++ b/server/tests/unit/mcp/tools-roadtrip.test.ts @@ -49,7 +49,6 @@ import { PermissionsService } from '../../../src/nest/permissions/permissions.se import { DatabaseService } from '../../../src/nest/database/database.service'; import { DEMO_EMAIL_PRIMARY } from '../../../src/nest/common/demo'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // The permissions cache is module-scoped, so a write through any instance is // what the tool's own check reads back. diff --git a/server/tests/unit/mcp/tools-transit.test.ts b/server/tests/unit/mcp/tools-transit.test.ts index 1eaba121f..61b7c0945 100644 --- a/server/tests/unit/mcp/tools-transit.test.ts +++ b/server/tests/unit/mcp/tools-transit.test.ts @@ -46,7 +46,6 @@ import { ReservationsService } from '../../../src/nest/reservations/reservations import type { TransitPlace } from '../../../src/nest/transit/transit.helpers'; import { TransitService } from '../../../src/nest/transit/transit.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // savePermissions is no longer bridged; write through a service instance — the // permissions cache is module-scoped, so the MCP _shared checkPermission path diff --git a/server/tests/unit/nest/accommodations.service.test.ts b/server/tests/unit/nest/accommodations.service.test.ts index 8f5145a15..6c3d45ad7 100644 --- a/server/tests/unit/nest/accommodations.service.test.ts +++ b/server/tests/unit/nest/accommodations.service.test.ts @@ -50,7 +50,6 @@ import { AccommodationsDomainModule } from '../../../src/nest/accommodations/acc import { AccommodationsController } from '../../../src/nest/accommodations/accommodations.controller'; import { expectRegisteredProvider, expectRegisteredController } from '../../helpers/module-providers'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // Named `svc` so the moved cases read exactly as they did on DaysService. let svc: Awaited>; diff --git a/server/tests/unit/nest/admin-default-user-settings.controller.test.ts b/server/tests/unit/nest/admin-default-user-settings.controller.test.ts index da7d847e5..7c8df56bf 100644 --- a/server/tests/unit/nest/admin-default-user-settings.controller.test.ts +++ b/server/tests/unit/nest/admin-default-user-settings.controller.test.ts @@ -80,4 +80,15 @@ describe('AdminDefaultUserSettingsController', () => { it('DEFAULTS-005 the class is listed in its module controllers', () => { expectRegisteredController(SettingsModule, AdminDefaultUserSettingsController); }); + + it('DEFAULTS-006 a rejected read-back after a successful write is still a 400, not a 500', async () => { + const { c, settings } = controller({ + getAdminUserDefaults: vi.fn(() => Promise.reject(new Error('read-back failed'))), + } as Partial); + expect(await thrown(() => c.update(user, { theme: 'light' } as never, req))).toEqual({ + status: 400, + body: { error: 'read-back failed' }, + }); + expect(settings.setAdminUserDefaults).toHaveBeenCalledWith({ theme: 'light' }); + }); }); diff --git a/server/tests/unit/nest/admin.service.test.ts b/server/tests/unit/nest/admin.service.test.ts index 021f2ce4c..6352e55cd 100644 --- a/server/tests/unit/nest/admin.service.test.ts +++ b/server/tests/unit/nest/admin.service.test.ts @@ -83,7 +83,6 @@ import { makeNotificationsService, makeNotificationPreferencesService } from '.. import { EphemeralTokenService } from '../../../src/nest/auth/ephemeral-token.service'; import { AllowedFileTypesService } from '../../../src/nest/files/allowed-file-types.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const dbs = new DatabaseService(testDb); const realtime = new RealtimeService(); diff --git a/server/tests/unit/nest/admin.version-notification.test.ts b/server/tests/unit/nest/admin.version-notification.test.ts index 81bec036e..71a6a58f3 100644 --- a/server/tests/unit/nest/admin.version-notification.test.ts +++ b/server/tests/unit/nest/admin.version-notification.test.ts @@ -61,7 +61,6 @@ import { makeNotificationsService, makeNotificationPreferencesService } from '.. import { EphemeralTokenService } from '../../../src/nest/auth/ephemeral-token.service'; import { AllowedFileTypesService } from '../../../src/nest/files/allowed-file-types.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const dbs = new DatabaseService(testDb); const realtime = new RealtimeService(); diff --git a/server/tests/unit/nest/assignments.service.test.ts b/server/tests/unit/nest/assignments.service.test.ts index 72a870735..d44a9990f 100644 --- a/server/tests/unit/nest/assignments.service.test.ts +++ b/server/tests/unit/nest/assignments.service.test.ts @@ -56,7 +56,6 @@ import { QueryHelpersService } from '../../../src/nest/query-helpers/query-helpe import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service'; import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const dbs = new DatabaseService(testDb); const realtime = new RealtimeService(); @@ -280,9 +279,9 @@ describe('moveAssignment', () => { const { trip, day, place } = fixture(); const target = createDay(testDb, trip.id); const a = createDayAssignment(testDb, day.id, place.id, { order_index: 7 }); - expect((await (await svc.moveAssignment(a.id, target.id, undefined)).assignment!).order_index).toBe(0); - expect((await (await svc.moveAssignment(a.id, day.id, null)).assignment!).order_index).toBe(0); - expect((await (await svc.moveAssignment(a.id, target.id, 0)).assignment!).order_index).toBe(0); + expect((await svc.moveAssignment(a.id, target.id, undefined)).assignment!.order_index).toBe(0); + expect((await svc.moveAssignment(a.id, day.id, null)).assignment!.order_index).toBe(0); + expect((await svc.moveAssignment(a.id, target.id, 0)).assignment!.order_index).toBe(0); }); }); diff --git a/server/tests/unit/nest/auth.service.test.ts b/server/tests/unit/nest/auth.service.test.ts index 3182bdf9c..9af78419a 100644 --- a/server/tests/unit/nest/auth.service.test.ts +++ b/server/tests/unit/nest/auth.service.test.ts @@ -85,7 +85,6 @@ import { EphemeralTokenService } from '../../../src/nest/auth/ephemeral-token.se import { AllowedFileTypesService } from '../../../src/nest/files/allowed-file-types.service'; import { DEFAULT_ALLOWED_EXTENSIONS } from '../../../src/nest/files/files.constants'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // MailerService is injected since the notifications fold — a stub instead of a // module mock. sendPasswordResetEmail is the only thing auth reaches for. diff --git a/server/tests/unit/nest/budget.service.db.test.ts b/server/tests/unit/nest/budget.service.db.test.ts index 21cc8db72..21360ff3f 100644 --- a/server/tests/unit/nest/budget.service.db.test.ts +++ b/server/tests/unit/nest/budget.service.db.test.ts @@ -73,7 +73,6 @@ import { VacayService } from '../../../src/nest/vacay/vacay.service'; import { QueryHelpersService } from '../../../src/nest/query-helpers/query-helpers.service'; import { notificationsStub } from '../../helpers/notifications'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; diff --git a/server/tests/unit/nest/calendar.service.test.ts b/server/tests/unit/nest/calendar.service.test.ts index d53b066ac..b27a25e16 100644 --- a/server/tests/unit/nest/calendar.service.test.ts +++ b/server/tests/unit/nest/calendar.service.test.ts @@ -58,7 +58,6 @@ import { expectRegisteredProvider } from '../../helpers/module-providers'; import { notificationsStub } from '../../helpers/notifications'; import { accommodationsOver } from '../../helpers/accommodations-service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const dbs = () => new DatabaseService(testDb); @@ -399,8 +398,7 @@ describe('exportICS', () => { insertEp.run(reservation.id, 'from', 0, 'Paris CDG', 'CDG', 49.0, 2.5, 'Not/AZone', '09:00', '2025-06-02'); insertEp.run(reservation.id, 'to', 1, 'New York JFK', 'JFK', 40.6, -73.8, 'garbage', '12:00', '2025-06-02'); - let ics: string; - ics = (await svc.exportICS(trip.id)).ics; + const ics = (await svc.exportICS(trip.id)).ics; // Falls back to a floating local time (no TZID) and never emits a bogus VTIMEZONE. expect(ics).toContain('DTSTART:20250602T090000'); expect(ics).not.toContain('TZID=Not/AZone'); @@ -682,8 +680,7 @@ describe('exportICS', () => { const assignment = createDayAssignment(testDb, day.id, place.id); testDb.prepare('UPDATE day_assignments SET assignment_time=? WHERE id=?').run('09:00', assignment.id); - let ics: string; - ics = (await svc.exportICS(trip.id)).ics; + const ics = (await svc.exportICS(trip.id)).ics; expect(ics).toContain('DTSTART;TZID=Asia/Tokyo:20251345T090000'); expect(ics).toContain('BEGIN:VTIMEZONE\r\nTZID:Asia/Tokyo'); diff --git a/server/tests/unit/nest/collab.service.test.ts b/server/tests/unit/nest/collab.service.test.ts index 7452f7650..5138f5eb4 100644 --- a/server/tests/unit/nest/collab.service.test.ts +++ b/server/tests/unit/nest/collab.service.test.ts @@ -88,7 +88,6 @@ import { notificationsStub } from '../../helpers/notifications'; import { makeStorageFixture } from '../../helpers/storage-fixture'; import { RateLimitService } from '../../../src/nest/common/rate-limit.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const collabFx = makeStorageFixture('files/'); const rateLimit = new RateLimitService(); diff --git a/server/tests/unit/nest/collections.service.test.ts b/server/tests/unit/nest/collections.service.test.ts index 38c0debb1..77c095cc2 100644 --- a/server/tests/unit/nest/collections.service.test.ts +++ b/server/tests/unit/nest/collections.service.test.ts @@ -56,7 +56,6 @@ import { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-pho import { makeStorageFixture } from '../../helpers/storage-fixture'; import { notificationsStub } from '../../helpers/notifications'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const storageFx = makeStorageFixture(''); let svc: CollectionsService; diff --git a/server/tests/unit/nest/dawarich-suggestions.service.test.ts b/server/tests/unit/nest/dawarich-suggestions.service.test.ts index baf51fbe8..605e1d146 100644 --- a/server/tests/unit/nest/dawarich-suggestions.service.test.ts +++ b/server/tests/unit/nest/dawarich-suggestions.service.test.ts @@ -68,7 +68,6 @@ import type { AssignmentsService } from '../../../src/nest/assignments/assignmen import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // ── Collaborator stubs ─────────────────────────────────────────────────────── // diff --git a/server/tests/unit/nest/dawarich.mcp.test.ts b/server/tests/unit/nest/dawarich.mcp.test.ts index f4130ae4b..21ccd2e07 100644 --- a/server/tests/unit/nest/dawarich.mcp.test.ts +++ b/server/tests/unit/nest/dawarich.mcp.test.ts @@ -162,17 +162,17 @@ describe('DawarichMcp surface', () => { it.each(registeredMethods())( 'DAWARICH-MCP-003: %s is gated on the dawarich addon, the same gate the controller gets from @RequireAddon', - (method) => { - const isAddonEnabled = vi.fn().mockReturnValue(false); + async (method) => { + const isAddonEnabled = vi.fn().mockResolvedValue(false); const mcp = makeMcp({ addons: { isAddonEnabled } }); const { when } = toolOptions(method); expect(typeof when).toBe('function'); - expect(when(ctx, mcp)).toBe(false); + await expect(when(ctx, mcp)).resolves.toBe(false); expect(isAddonEnabled).toHaveBeenCalledWith(ADDON_IDS.DAWARICH); - isAddonEnabled.mockReturnValue(true); - expect(when(ctx, mcp)).toBe(true); + isAddonEnabled.mockResolvedValue(true); + await expect(when(ctx, mcp)).resolves.toBe(true); }, ); diff --git a/server/tests/unit/nest/day-notes.service.test.ts b/server/tests/unit/nest/day-notes.service.test.ts index eb62081f7..132086844 100644 --- a/server/tests/unit/nest/day-notes.service.test.ts +++ b/server/tests/unit/nest/day-notes.service.test.ts @@ -52,7 +52,6 @@ import { DayNotesService } from '../../../src/nest/day-notes/day-notes.service'; import type { DayNote } from '../../../src/types'; import { RealtimeService } from '../../../src/nest/realtime/realtime.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; let svc: DayNotesService; beforeAll(async () => { diff --git a/server/tests/unit/nest/days.service.test.ts b/server/tests/unit/nest/days.service.test.ts index e8921dca9..316623b78 100644 --- a/server/tests/unit/nest/days.service.test.ts +++ b/server/tests/unit/nest/days.service.test.ts @@ -65,7 +65,6 @@ import { QueryHelpersService } from '../../../src/nest/query-helpers/query-helpe import { makeAccommodationsService } from '../../helpers/accommodations-service'; import type { Day } from '../../../src/types'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; let svc: DaysService; beforeAll(async () => { diff --git a/server/tests/unit/nest/doc-sync/doc-sync.mcp.test.ts b/server/tests/unit/nest/doc-sync/doc-sync.mcp.test.ts index 23cd5f052..691f1812f 100644 --- a/server/tests/unit/nest/doc-sync/doc-sync.mcp.test.ts +++ b/server/tests/unit/nest/doc-sync/doc-sync.mcp.test.ts @@ -59,7 +59,7 @@ function makeMcp(over: Partial = {}) { retryShelvedItems: vi.fn(), isSwitchedOff: vi.fn((l: LinkRow) => setup.off.includes(l.id)), }; - const addons = { isAddonEnabled: vi.fn(() => setup.addonOn) }; + const addons = { isAddonEnabled: vi.fn(() => Promise.resolve(setup.addonOn)) }; const mcp = new DocSyncMcp( config as unknown as DocSyncConfigService, sync as unknown as DocSyncService, @@ -115,15 +115,15 @@ describe('DocSyncMcp surface', () => { expect([...new Set(fields)].sort()).toEqual(['full', 'tripId']); }); - it.each(registeredMethods())('%s disappears while the documents addon is off', (method) => { + it.each(registeredMethods())('%s disappears while the documents addon is off', async (method) => { const { mcp, addons } = makeMcp({ addonOn: false }); const { when } = toolOptions(method); expect(typeof when).toBe('function'); - expect(when?.(ctx, mcp)).toBe(false); + await expect(when?.(ctx, mcp)).resolves.toBe(false); expect(addons.isAddonEnabled).toHaveBeenCalledWith(ADDON_IDS.DOCUMENTS); - addons.isAddonEnabled.mockReturnValue(true); - expect(when?.(ctx, mcp)).toBe(true); + addons.isAddonEnabled.mockResolvedValue(true); + await expect(when?.(ctx, mcp)).resolves.toBe(true); }); it.each([ diff --git a/server/tests/unit/nest/files/upload-filters.test.ts b/server/tests/unit/nest/files/upload-filters.test.ts index ab3b6141e..129dd74cd 100644 --- a/server/tests/unit/nest/files/upload-filters.test.ts +++ b/server/tests/unit/nest/files/upload-filters.test.ts @@ -80,6 +80,22 @@ describe('filesUploadFileFilter', () => { expect(cb).toHaveBeenCalledTimes(1); expect(cb).toHaveBeenCalledWith(null, true); }); + + it('UPLOAD-FILTER-007: an error escaping the detached IIFE (outside the allow-list try/catch) still answers cb, not an unhandled rejection', async () => { + const allowedTypes = allowedTypesStub('jpg,png'); + const filter = filesUploadFileFilter(allowedTypes)!; + const cb = vi.fn().mockImplementationOnce(() => { throw new Error('cb blew up'); }); + filter(req, fakeFile({ originalname: 'photo.jpg', mimetype: 'image/jpeg' }), cb); + await flush(); + // First call is the filter's own cb(null, true), which throws; the outer + // .catch() answers a second time with the escaped error rather than + // leaving the request hanging or crashing the process. + expect(cb).toHaveBeenCalledTimes(2); + expect(cb).toHaveBeenNthCalledWith(1, null, true); + const secondErr = cb.mock.calls[1][0] as Error; + expect(secondErr).toBeInstanceOf(Error); + expect(secondErr.message).toBe('cb blew up'); + }); }); describe('journeyImageFileFilter', () => { @@ -117,4 +133,17 @@ describe('journeyImageFileFilter', () => { expect(cb).toHaveBeenCalledTimes(1); expect(cb).toHaveBeenCalledWith(null, true); }); + + it('UPLOAD-FILTER-007: an error escaping the detached IIFE (outside the allow-list try/catch) still answers cb, not an unhandled rejection', async () => { + const allowedTypes = allowedTypesStub('jpg,png'); + const filter = journeyImageFileFilter(allowedTypes)!; + const cb = vi.fn().mockImplementationOnce(() => { throw new Error('cb blew up'); }); + filter(req, fakeFile({ originalname: 'photo.jpg', mimetype: 'image/jpeg' }), cb); + await flush(); + expect(cb).toHaveBeenCalledTimes(2); + expect(cb).toHaveBeenNthCalledWith(1, null, true); + const secondErr = cb.mock.calls[1][0] as Error; + expect(secondErr).toBeInstanceOf(Error); + expect(secondErr.message).toBe('cb blew up'); + }); }); diff --git a/server/tests/unit/nest/google-route.test.ts b/server/tests/unit/nest/google-route.test.ts index 632a2c860..66f7cae7c 100644 --- a/server/tests/unit/nest/google-route.test.ts +++ b/server/tests/unit/nest/google-route.test.ts @@ -3,7 +3,6 @@ import Database from 'better-sqlite3'; import { GoogleRouteService } from '../../../src/nest/roadtrip/google-route.service'; import { safeFetchFollow } from '../../../src/utils/ssrfGuard'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; vi.mock('../../../src/utils/ssrfGuard', () => ({ safeFetchFollow: vi.fn() })); diff --git a/server/tests/unit/nest/mcp-shared.service.test.ts b/server/tests/unit/nest/mcp-shared.service.test.ts index d2ea419c3..c33581b53 100644 --- a/server/tests/unit/nest/mcp-shared.service.test.ts +++ b/server/tests/unit/nest/mcp-shared.service.test.ts @@ -26,7 +26,6 @@ import { DatabaseService } from '../../../src/nest/database/database.service'; import { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import { RealtimeService } from '../../../src/nest/realtime/realtime.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const dbs = new DatabaseService(testDb); let svc: McpToolGuardsService; diff --git a/server/tests/unit/nest/oidc.service.test.ts b/server/tests/unit/nest/oidc.service.test.ts index 7b5ad4a8e..b6ddd9cd7 100644 --- a/server/tests/unit/nest/oidc.service.test.ts +++ b/server/tests/unit/nest/oidc.service.test.ts @@ -107,7 +107,6 @@ import { AllowedFileTypesService } from '../../../src/nest/files/allowed-file-ty import { OidcService } from '../../../src/nest/oidc/oidc.service'; import { MailerService } from '../../../src/nest/notifications/mailer/mailer.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // MailerService is injected since the notifications fold — a stub instead of a // module mock. sendPasswordResetEmail is the only thing auth reaches for. diff --git a/server/tests/unit/nest/passkey.service.test.ts b/server/tests/unit/nest/passkey.service.test.ts index 5a2b77ecf..5c68ddd40 100644 --- a/server/tests/unit/nest/passkey.service.test.ts +++ b/server/tests/unit/nest/passkey.service.test.ts @@ -90,7 +90,6 @@ import { EphemeralTokenService } from '../../../src/nest/auth/ephemeral-token.se import { AllowedFileTypesService } from '../../../src/nest/files/allowed-file-types.service'; import { MailerService } from '../../../src/nest/notifications/mailer/mailer.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // MailerService is injected since the notifications fold — a stub instead of a // module mock. sendPasswordResetEmail is the only thing auth reaches for. diff --git a/server/tests/unit/nest/permissions.service.test.ts b/server/tests/unit/nest/permissions.service.test.ts index 5b57a8ac9..cb5fd1c29 100644 --- a/server/tests/unit/nest/permissions.service.test.ts +++ b/server/tests/unit/nest/permissions.service.test.ts @@ -48,7 +48,6 @@ import { invalidatePermissionsCache as invalidateSharedCache, } from '../../../src/nest/permissions/permissions-cache'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; let svc: PermissionsService; beforeAll(async () => { diff --git a/server/tests/unit/nest/places.directions.test.ts b/server/tests/unit/nest/places.directions.test.ts index 2002e36e7..70409ba29 100644 --- a/server/tests/unit/nest/places.directions.test.ts +++ b/server/tests/unit/nest/places.directions.test.ts @@ -55,7 +55,6 @@ import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.s import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository'; import { makeStorageFixture } from '../../helpers/storage-fixture'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const dbs = new DatabaseService(testDb); const photoCacheStub = { removeIfUnreferenced: vi.fn() } as unknown as PlacePhotoCacheService; diff --git a/server/tests/unit/nest/places.service.test.ts b/server/tests/unit/nest/places.service.test.ts index d6330638b..4945d2bff 100644 --- a/server/tests/unit/nest/places.service.test.ts +++ b/server/tests/unit/nest/places.service.test.ts @@ -79,7 +79,6 @@ import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.s import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository'; import { makeStorageFixture } from '../../helpers/storage-fixture'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const GPX_FIXTURE = path.join(__dirname, '../../fixtures/test.gpx'); const KML_FIXTURE = path.join(__dirname, '../../fixtures/test.kml'); diff --git a/server/tests/unit/nest/realtime.gateway.test.ts b/server/tests/unit/nest/realtime.gateway.test.ts index 3b8a5886a..34f974400 100644 --- a/server/tests/unit/nest/realtime.gateway.test.ts +++ b/server/tests/unit/nest/realtime.gateway.test.ts @@ -11,8 +11,16 @@ vi.mock('../../../src/plugin-event-sink', () => ({ emitPluginEvent: vi.fn(), pluginEventMeta: vi.fn(() => ({})), })); +vi.mock('../../../src/nest/audit/audit-log.logger', () => ({ + LOG_LEVEL: 'error', + logInfo: vi.fn(), + logDebug: vi.fn(), + logError: vi.fn(), + logWarn: vi.fn(), +})); import { RealtimeGateway } from '../../../src/nest/realtime/realtime.gateway'; +import { logError } from '../../../src/nest/audit/audit-log.logger'; import { bookPeers, broadcast, @@ -155,6 +163,37 @@ describe('RealtimeGateway handshake', () => { expect(held).not.toHaveProperty('password_version'); expect(held).toMatchObject({ id: 3, email: 'm@x.test' }); }); + + it('WSGW-009: an unexpected handshake error is logged and closes 4001, reason "connection setup failed"', async () => { + // Anything thrown inside the try — here, the user-row lookup blowing up — + // must still answer the callback rather than escape as an uncaught + // exception (recipe R1.5's catch site). + const throwing = { + get: () => { throw new Error('db exploded'); }, + canAccessTrip: (tripId: number) => tripId === 7, + } as unknown as DatabaseService; + const gw = new RealtimeGateway(throwing, tokens, journeys); + const ws = socket(); + + await gw.handleConnection(ws, { url: '/ws?token=x' } as never); + + expect(ws.closedWith).toEqual([4001, 'connection setup failed']); + expect(logError).toHaveBeenCalledWith(expect.stringContaining('db exploded')); + }); + + it('WSGW-009b: a non-Error thrown during the handshake is stringified rather than crashing the catch itself', async () => { + const throwing = { + get: () => { throw 'db string'; }, // non-Error throw, pinning the ternary's String(err) fallback + canAccessTrip: (tripId: number) => tripId === 7, + } as unknown as DatabaseService; + const gw = new RealtimeGateway(throwing, tokens, journeys); + const ws = socket(); + + await gw.handleConnection(ws, { url: '/ws?token=x' } as never); + + expect(ws.closedWith).toEqual([4001, 'connection setup failed']); + expect(logError).toHaveBeenCalledWith(expect.stringContaining('db string')); + }); }); describe('RealtimeGateway rooms', () => { diff --git a/server/tests/unit/nest/reservations.service.test.ts b/server/tests/unit/nest/reservations.service.test.ts index 4eb75c065..c6fb61322 100644 --- a/server/tests/unit/nest/reservations.service.test.ts +++ b/server/tests/unit/nest/reservations.service.test.ts @@ -77,7 +77,6 @@ import { RealtimeService } from '../../../src/nest/realtime/realtime.service'; import { notificationsStub } from '../../helpers/notifications'; import { makeAccommodationsService } from '../../helpers/accommodations-service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; let svc: ReservationsService; beforeAll(async () => { diff --git a/server/tests/unit/nest/share.service.test.ts b/server/tests/unit/nest/share.service.test.ts index 8412a6ade..933b5c101 100644 --- a/server/tests/unit/nest/share.service.test.ts +++ b/server/tests/unit/nest/share.service.test.ts @@ -254,7 +254,7 @@ describe('getSharedTripData', () => { const day = createDay(testDb, trip.id, { date: '2025-06-01' }); const place = createPlace(testDb, trip.id, { name: 'Louvre' }); createDayAssignment(testDb, day.id, place.id, { notes: 'go early' }); - const data = await (await svc.getSharedTripData(token))!; + const data = (await svc.getSharedTripData(token))!; expect(data).not.toBeNull(); expect(data.trip).toEqual(expect.objectContaining({ id: trip.id, title: trip.title })); // Explicit column list — no owner id or internal fields on the trip row. @@ -281,11 +281,11 @@ describe('getSharedTripData', () => { const place = createPlace(testDb, trip.id); testDb.prepare('UPDATE places SET place_time = ?, end_time = ? WHERE id = ?').run('09:00', '10:00', place.id); const a = createDayAssignment(testDb, day.id, place.id); - let entry = (await (await svc.getSharedTripData(token))!).assignments[day.id][0]; + let entry = (await svc.getSharedTripData(token))!.assignments[day.id][0]; expect(entry.place.place_time).toBe('09:00'); expect(entry.place.end_time).toBe('10:00'); testDb.prepare('UPDATE day_assignments SET assignment_time = ?, assignment_end_time = ? WHERE id = ?').run('14:00', '15:00', a.id); - entry = (await (await svc.getSharedTripData(token))!).assignments[day.id][0]; + entry = (await svc.getSharedTripData(token))!.assignments[day.id][0]; expect(entry.place.place_time).toBe('14:00'); expect(entry.place.end_time).toBe('15:00'); }); @@ -301,7 +301,7 @@ describe('getSharedTripData', () => { testDb.prepare( "INSERT INTO day_assignments (day_id, place_id, order_index, created_at) VALUES (?, ?, 0, '2025-01-01T10:00:00')" ).run(day.id, p1.id); - const entries = (await (await svc.getSharedTripData(token))!).assignments[day.id]; + const entries = (await svc.getSharedTripData(token))!.assignments[day.id]; expect(entries.map((e: any) => e.place.name)).toEqual(['First', 'Second']); }); @@ -312,7 +312,7 @@ describe('getSharedTripData', () => { const r2 = testDb.prepare("INSERT INTO reservations (trip_id, title, type) VALUES (?, 'Hotel', 'hotel')").run(trip.id); testDb.prepare('INSERT INTO reservation_day_positions (reservation_id, day_id, position) VALUES (?, ?, 3)') .run(r1.lastInsertRowid, day.id); - const reservations = (await (await svc.getSharedTripData(token))!).reservations; + const reservations = (await svc.getSharedTripData(token))!.reservations; const flight = reservations.find((r: any) => r.id === r1.lastInsertRowid); const hotel = reservations.find((r: any) => r.id === r2.lastInsertRowid); expect(flight.day_positions).toEqual({ [day.id]: 3 }); @@ -323,7 +323,7 @@ describe('getSharedTripData', () => { const { user, trip, token } = await seedSharedTrip(); testDb.prepare("INSERT INTO packing_items (trip_id, name, is_private, owner_id) VALUES (?, 'Common', 0, ?)").run(trip.id, user.id); testDb.prepare("INSERT INTO packing_items (trip_id, name, is_private, owner_id) VALUES (?, 'Secret', 1, ?)").run(trip.id, user.id); - const packing = (await (await svc.getSharedTripData(token))!).packing; + const packing = (await svc.getSharedTripData(token))!.packing; expect(packing.map((p: any) => p.name)).toEqual(['Common']); }); @@ -331,12 +331,12 @@ describe('getSharedTripData', () => { const { user, trip, token } = await seedSharedTrip(); testDb.prepare("INSERT INTO collab_messages (trip_id, user_id, text, deleted) VALUES (?, ?, 'Hello', 0)").run(trip.id, user.id); testDb.prepare("INSERT INTO collab_messages (trip_id, user_id, text, deleted) VALUES (?, ?, 'Gone', 1)").run(trip.id, user.id); - const collab = (await (await svc.getSharedTripData(token))!).collab; + const collab = (await svc.getSharedTripData(token))!.collab; expect(collab).toHaveLength(1); expect(collab[0]).toEqual(expect.objectContaining({ text: 'Hello', username: user.username })); await svc.createOrUpdate(String(trip.id), user.id, { share_collab: false }); - expect((await (await svc.getSharedTripData(token))!).collab).toEqual([]); + expect((await svc.getSharedTripData(token))!.collab).toEqual([]); }); it('SHARE-SVC-019: withholds each section when its flag is off', async () => { @@ -349,7 +349,7 @@ describe('getSharedTripData', () => { testDb.prepare("INSERT INTO reservations (trip_id, title, type) VALUES (?, 'Flight', 'flight')").run(trip.id); testDb.prepare("INSERT INTO packing_items (trip_id, name) VALUES (?, 'Socks')").run(trip.id); testDb.prepare("INSERT INTO budget_items (trip_id, name, category, total_price) VALUES (?, 'Food', 'food', 10)").run(trip.id); - const data = await (await svc.getSharedTripData(token))!; + const data = (await svc.getSharedTripData(token))!; expect(data.days).toEqual([]); expect(data.assignments).toEqual({}); expect(data.dayNotes).toEqual({}); @@ -363,11 +363,11 @@ describe('getSharedTripData', () => { it('SHARE-SVC-020: baseCurrency falls back trip currency → EUR, with the owner default_currency winning (#1361)', async () => { const { user, trip, token } = await seedSharedTrip(); - expect((await (await svc.getSharedTripData(token))!).baseCurrency).toBe('EUR'); + expect((await svc.getSharedTripData(token))!.baseCurrency).toBe('EUR'); testDb.prepare('UPDATE trips SET currency = ? WHERE id = ?').run('USD', trip.id); - expect((await (await svc.getSharedTripData(token))!).baseCurrency).toBe('USD'); + expect((await svc.getSharedTripData(token))!.baseCurrency).toBe('USD'); testDb.prepare("INSERT INTO settings (user_id, key, value) VALUES (?, 'default_currency', ' CHF ')").run(user.id); - expect((await (await svc.getSharedTripData(token))!).baseCurrency).toBe('CHF'); + expect((await svc.getSharedTripData(token))!.baseCurrency).toBe('CHF'); }); it('SHARE-SVC-021: rewrites place-photo proxy URLs to the token-scoped route, passing others through', async () => { @@ -378,7 +378,7 @@ describe('getSharedTripData', () => { testDb.prepare('UPDATE places SET image_url = ? WHERE id = ?').run('/api/maps/place-photo/ChIJabc/bytes', proxied.id); testDb.prepare('UPDATE places SET image_url = ? WHERE id = ?').run('/uploads/pic.jpg', uploaded.id); const byName = Object.fromEntries( - (await (await svc.getSharedTripData(token))!).places.map((p: any) => [p.name, p.image_url]), + (await svc.getSharedTripData(token))!.places.map((p: any) => [p.name, p.image_url]), ); expect(byName['Proxied']).toBe(`/api/shared/${token}/place-photo/ChIJabc/bytes`); expect(byName['Uploaded']).toBe('/uploads/pic.jpg'); @@ -387,11 +387,11 @@ describe('getSharedTripData', () => { it('SHARE-SVC-027: cartoApiKey resolves owner setting → admin instance default → empty (#2054)', async () => { const { user, token } = await seedSharedTrip(); - expect((await (await svc.getSharedTripData(token))!).cartoApiKey).toBe(''); + expect((await svc.getSharedTripData(token))!.cartoApiKey).toBe(''); testDb.prepare("INSERT INTO app_settings (key, value) VALUES ('default_user_setting_carto_api_key', 'instance-key')").run(); - expect((await (await svc.getSharedTripData(token))!).cartoApiKey).toBe('instance-key'); + expect((await svc.getSharedTripData(token))!.cartoApiKey).toBe('instance-key'); testDb.prepare("INSERT INTO settings (user_id, key, value) VALUES (?, 'carto_api_key', ' owner-key ')").run(user.id); - expect((await (await svc.getSharedTripData(token))!).cartoApiKey).toBe('owner-key'); + expect((await svc.getSharedTripData(token))!.cartoApiKey).toBe('owner-key'); }); it('SHARE-SVC-029: staged bookings stay out of the public payload, confirmation number included', async () => { @@ -401,7 +401,7 @@ describe('getSharedTripData', () => { testDb.prepare(`INSERT INTO reservations (trip_id, title, type, status, confirmation_number) VALUES (?, 'Booked Flight', 'flight', 'confirmed', 'OPEN1')`).run(trip.id); - const data = await (await svc.getSharedTripData(token))!; + const data = (await svc.getSharedTripData(token))!; expect((data.reservations as any[]).map((r) => r.title)).toEqual(['Booked Flight']); // SELECT * hands out notes, url and metadata too, so check the whole payload. @@ -415,7 +415,7 @@ describe('getSharedTripData', () => { VALUES (?, ?, 'flight', 'confirmed')`).run(trip.id, `Booking ${i}`); } - expect(((await (await svc.getSharedTripData(token))!).reservations as any[])).toHaveLength(5); + expect(((await svc.getSharedTripData(token))!.reservations as any[])).toHaveLength(5); }); it('SHARE-SVC-031: accommodations backed only by a staged booking are withheld, unlinked ones are kept', async () => { @@ -431,7 +431,7 @@ describe('getSharedTripData', () => { VALUES (?, 'Parked Hotel', 'hotel', 'confirmed', ?, 'staged')`).run(trip.id, String(stagedStay)); const unlinkedStay = stay(); - const rows = (await (await svc.getSharedTripData(token))!).accommodations as any[]; + const rows = (await svc.getSharedTripData(token))!.accommodations as any[]; expect(rows.map((a) => a.id)).toEqual([unlinkedStay]); }); @@ -447,7 +447,7 @@ describe('getSharedTripData redaction (#2320)', () => { VALUES (?, 'Night train', 'train', 'confirmed', 'PNR9XY', 'Bring the tickets', 'https://bahn.example/booking/1', 'kitinerary', 'ext-42', 1, 0)`) .run(trip.id); - const [row] = (await (await svc.getSharedTripData(token))!).reservations as any[]; + const [row] = (await svc.getSharedTripData(token))!.reservations as any[]; expect(Object.keys(row).sort()).toEqual([ 'accommodation_id', 'created_at', 'day_id', 'day_positions', 'end_day_id', 'endpoints', 'id', 'location', @@ -472,7 +472,7 @@ describe('getSharedTripData redaction (#2320)', () => { testDb.prepare(`INSERT INTO reservations (trip_id, title, type, status, metadata) VALUES (?, 'To New York', 'flight', 'confirmed', ?)`).run(trip.id, metadata); - const data = await (await svc.getSharedTripData(token))!; + const data = (await svc.getSharedTripData(token))!; const [row] = data.reservations as any[]; const meta = JSON.parse(row.metadata); @@ -498,7 +498,7 @@ describe('getSharedTripData redaction (#2320)', () => { ins.run(id, 'to', 1, 'Lyon', null, 45.76, 4.84); ins.run(id, 'from', 0, 'Paris', 'PAR', 48.85, 2.35); - const [row] = (await (await svc.getSharedTripData(token))!).reservations as any[]; + const [row] = (await svc.getSharedTripData(token))!.reservations as any[]; expect(row.endpoints.map((e: any) => [e.role, e.name])).toEqual([['from', 'Paris'], ['to', 'Lyon']]); expect(Object.keys(row.endpoints[0]).sort()).toEqual( @@ -516,7 +516,7 @@ describe('getSharedTripData redaction (#2320)', () => { testDb.prepare('UPDATE places SET website = ?, phone = ? WHERE id = ?').run('javascript:alert(2)', '+43 1 234', place.id); createDayAssignment(testDb, day.id, place.id, {}); - const data = await (await svc.getSharedTripData(token))!; + const data = (await svc.getSharedTripData(token))!; const urls = Object.fromEntries((data.reservations as any[]).map((r) => [r.title, r.url])); expect(urls).toEqual({ ok: 'https://example.com/x', js: null, data: null, junk: null, blank: null }); expect(data.assignments[day.id][0].place.website).toBeNull(); @@ -532,7 +532,7 @@ describe('getSharedTripData redaction (#2320)', () => { duration_minutes = 180, website = 'https://louvre.fr', phone = '+33 1', reservation_notes = 'Booked under Ada', google_place_id = 'ChIJ123' WHERE id = ?`).run(place.id); createDayAssignment(testDb, day.id, place.id, { notes: 'go early' }); - const data = await (await svc.getSharedTripData(token))!; + const data = (await svc.getSharedTripData(token))!; const entry = data.assignments[day.id][0]; expect(entry.notes).toBe('go early'); expect(entry.place).toEqual(expect.objectContaining({ @@ -554,7 +554,7 @@ describe('getSharedTripData redaction (#2320)', () => { testDb.prepare(`INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation, notes) VALUES (?, ?, ?, ?, '15:00', '11:00', 'HOTEL-777', 'Ask for a quiet room')`).run(trip.id, place.id, day.id, day.id); - const data = await (await svc.getSharedTripData(token))!; + const data = (await svc.getSharedTripData(token))!; const [stay] = data.accommodations as any[]; expect(stay).toEqual(expect.objectContaining({ check_in: '15:00', check_out: '11:00', notes: 'Ask for a quiet room', place_name: 'Hotel' })); expect(stay).not.toHaveProperty('confirmation'); diff --git a/server/tests/unit/nest/todo.service.test.ts b/server/tests/unit/nest/todo.service.test.ts index 233f67883..1da8187e9 100644 --- a/server/tests/unit/nest/todo.service.test.ts +++ b/server/tests/unit/nest/todo.service.test.ts @@ -49,7 +49,6 @@ import { PermissionsService } from '../../../src/nest/permissions/permissions.se import { TodoService } from '../../../src/nest/todo/todo.service'; import { RealtimeService } from '../../../src/nest/realtime/realtime.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; let svc: TodoService; beforeAll(async () => { diff --git a/server/tests/unit/nest/trek-ws-adapter.test.ts b/server/tests/unit/nest/trek-ws-adapter.test.ts index 83d10210d..a89788ac7 100644 --- a/server/tests/unit/nest/trek-ws-adapter.test.ts +++ b/server/tests/unit/nest/trek-ws-adapter.test.ts @@ -235,7 +235,7 @@ describe('TrekWsAdapter server creation', () => { /** A real http.Server is never listened on; ws only needs it to hang an upgrade handler. */ const httpServer = { on: vi.fn(), once: vi.fn(), removeListener: vi.fn(), emit: vi.fn() } as unknown as HttpServer; - it('WSAD-030: attaches to the http server it was given, on the gateway path', () => { + it('WSAD-030: attaches to the http server it was given, on the gateway path', async () => { const ad = new TrekWsAdapter(httpServer); const server = ad.create(0, { path: '/ws' }) as { options: Record }; try { @@ -244,11 +244,11 @@ describe('TrekWsAdapter server creation', () => { // silently goes nowhere. expect(getServer()).toBe(server); } finally { - void ad.close(server as never); + await ad.close(server as never); } }); - it('WSAD-031: with no allowlist configured, ws gets no verifyClient at all', () => { + it('WSAD-031: with no allowlist configured, ws gets no verifyClient at all', async () => { // Rather than one that always says yes: an always-true hook is a hook that // can be broken into a false later without anyone noticing. wsOrigins.value = null; @@ -257,11 +257,11 @@ describe('TrekWsAdapter server creation', () => { try { expect(typeof server.options.verifyClient).not.toBe('function'); } finally { - void ad.close(server as never); + await ad.close(server as never); } }); - it('WSAD-031b: with an allowlist, a foreign origin is refused 403 before a socket exists', () => { + it('WSAD-031b: with an allowlist, a foreign origin is refused 403 before a socket exists', async () => { wsOrigins.value = ['https://trip.example']; const ad = new TrekWsAdapter(httpServer); const server = ad.create(0, { path: '/ws' }) as { options: { verifyClient?: unknown } }; @@ -278,22 +278,22 @@ describe('TrekWsAdapter server creation', () => { verify({ origin: '' }, (...args) => seen.push(args)); expect(seen).toEqual([[false, 403, 'Origin not allowed'], [true], [true]]); } finally { - void ad.close(server as never); + await ad.close(server as never); } }); - it('WSAD-030b: falls back to /ws when the gateway declares no path', () => { + it('WSAD-030b: falls back to /ws when the gateway declares no path', async () => { wsOrigins.value = null; const ad = new TrekWsAdapter(httpServer); const server = ad.create(0, {}) as { options: Record }; try { expect(server.options.path).toBe('/ws'); } finally { - void ad.close(server as never); + await ad.close(server as never); } }); - it('WSAD-031c: a server-level error is LOGGED, never swallowed', () => { + it('WSAD-031c: a server-level error is LOGGED, never swallowed', async () => { // ws forwards the http server's errors here, so an empty handler eats the // bind failure too and the process survives EADDRINUSE serving nothing. // index.ts owns the fatal decision; this handler owes a log line. @@ -309,7 +309,7 @@ describe('TrekWsAdapter server creation', () => { server.emit('error', 'ECONNRESET'); expect(logError).toHaveBeenCalledWith(expect.stringContaining('ECONNRESET')); } finally { - void ad.close(server as never); + await ad.close(server as never); } }); diff --git a/server/tests/unit/nest/trip-invite.service.test.ts b/server/tests/unit/nest/trip-invite.service.test.ts index 73f9832b9..6a5220952 100644 --- a/server/tests/unit/nest/trip-invite.service.test.ts +++ b/server/tests/unit/nest/trip-invite.service.test.ts @@ -41,7 +41,6 @@ import { PermissionsService } from '../../../src/nest/permissions/permissions.se import { TripMembershipService } from '../../../src/nest/trip-membership/trip-membership.service'; import { TripInviteService } from '../../../src/nest/trip-invite/trip-invite.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // One DatabaseService over the shared in-memory handle, so every collaborator // reads and writes the same rows. diff --git a/server/tests/unit/nest/trip-members.service.test.ts b/server/tests/unit/nest/trip-members.service.test.ts index c83f4f30b..a9e2313e6 100644 --- a/server/tests/unit/nest/trip-members.service.test.ts +++ b/server/tests/unit/nest/trip-members.service.test.ts @@ -66,7 +66,6 @@ import { NotFoundError, ValidationError } from '../../../src/nest/common/domain- import type { User } from '../../../src/types'; import { notificationsStub } from '../../helpers/notifications'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const dbs = () => new DatabaseService(testDb); diff --git a/server/tests/unit/nest/trip-read-model.service.test.ts b/server/tests/unit/nest/trip-read-model.service.test.ts index 2187e5fbb..cb9704317 100644 --- a/server/tests/unit/nest/trip-read-model.service.test.ts +++ b/server/tests/unit/nest/trip-read-model.service.test.ts @@ -79,7 +79,6 @@ import { makeStorageFixture } from '../../helpers/storage-fixture'; import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service'; import { TrekPhotosRepository } from '../../../src/nest/photos/trek-photos.repository'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // Real sibling services over the same in-memory DB — the aggregation runs the // actual SQL of every domain it fans out to, so a shape change downstream shows diff --git a/server/tests/unit/nest/trips.service.test.ts b/server/tests/unit/nest/trips.service.test.ts index 8459070fe..71c4fcd09 100644 --- a/server/tests/unit/nest/trips.service.test.ts +++ b/server/tests/unit/nest/trips.service.test.ts @@ -89,7 +89,6 @@ import path from 'path'; import { notificationsStub } from '../../helpers/notifications'; import { EphemeralTokenService } from '../../../src/nest/auth/ephemeral-token.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // Real sibling services over the same in-memory DB — updateTrip's date-shift // resyncs and the summary/bundle aggregation run their actual SQL. diff --git a/server/tests/unit/nest/user-cleanup.service.test.ts b/server/tests/unit/nest/user-cleanup.service.test.ts index f366cc631..2e7fd20a7 100644 --- a/server/tests/unit/nest/user-cleanup.service.test.ts +++ b/server/tests/unit/nest/user-cleanup.service.test.ts @@ -50,7 +50,6 @@ import { RealtimeService } from '../../../src/nest/realtime/realtime.service'; import { BudgetService } from '../../../src/nest/budget/budget.service'; import { UserCleanupService } from '../../../src/nest/auth/user-cleanup.service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const dbs = new DatabaseService(testDb); @@ -153,7 +152,7 @@ describe('erasePluginUserData', () => { const slim = new (require('better-sqlite3'))(':memory:'); slim.exec('CREATE TABLE users (id INTEGER PRIMARY KEY)'); slim.prepare('INSERT INTO users (id) VALUES (1)').run(); - const slimSvc = new UserCleanupService(new DatabaseService(slim), budget, await createTestUnitOfWork(testDb)); + const slimSvc = new UserCleanupService(new DatabaseService(slim), budget, await createTestUnitOfWork(slim)); await expect(slimSvc.erasePluginUserData(1)).resolves.toBeUndefined(); diff --git a/server/tests/unit/scripts/db-call-graph.test.ts b/server/tests/unit/scripts/db-call-graph.test.ts new file mode 100644 index 000000000..759e3f0fb --- /dev/null +++ b/server/tests/unit/scripts/db-call-graph.test.ts @@ -0,0 +1,100 @@ +/** + * The Phase 1 async-sweep gate (`scripts/db-call-graph.mjs`) had no test of its + * own — CI trusted the tool's own output without ever proving the detector still + * works. This runs the real script as a child process (never through a shell or + * PATH, so nothing but the pinned Node binary decides what runs) against the + * small, hand-documented fixture tree in `scripts/__fixtures__/`, once for each + * flag combination the CI step and the recipe's R5 gate use. + * + * The script's own contract (see its final `process.exit(found ? 1 : 0)`) is to + * exit non-zero when it has findings, and the `--unawaited` run against this + * fixture is SUPPOSED to find the five documented BAD shapes, so `execFileSync` + * throws on it — Node still attaches the child's stdout/status to that error, + * which `run()` below reads back out instead of treating the throw as a test + * failure. + */ +import { describe, it, expect } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; + +const SERVER_ROOT = path.join(__dirname, '../../..'); +const SCRIPT = path.join(SERVER_ROOT, 'scripts/db-call-graph.mjs'); + +interface UnawaitedFinding { + file: string; + class: string; + method: string; + reason: string; +} + +interface UnawaitedReport { + unawaitedCalls: UnawaitedFinding[]; + unawaitedCallCount: number; +} + +interface SyncTxReport { + syncDbMethods: unknown[]; + syncDbMethodCount: number; + transactionSites: unknown[]; + transactionSiteCount: number; +} + +interface ExecError { + status: number | null; + stdout: string; +} + +function run(args: string[]): { status: number | null; report: unknown } { + try { + const stdout = execFileSync(process.execPath, [SCRIPT, ...args], { + cwd: SERVER_ROOT, + encoding: 'utf8', + }); + return { status: 0, report: JSON.parse(stdout) }; + } catch (err) { + // A non-zero exit (the gate has findings) surfaces as a thrown error whose + // stdout/status carry the same JSON report the caller would have seen on + // a clean run. + const { status, stdout } = err as ExecError; + return { status, report: JSON.parse(stdout) }; + } +} + +describe('db-call-graph.mjs', () => { + it('CALLGRAPH-001: --unawaited finds exactly the five documented BAD shapes, one per reason tag, and exits 1', () => { + const { status, report } = run(['--root', 'scripts/__fixtures__', '--unawaited', '--json']); + const { unawaitedCalls, unawaitedCallCount } = report as UnawaitedReport; + + // A gate with findings must fail the CI step it runs in. + expect(status).toBe(1); + expect(unawaitedCallCount).toBe(5); + expect(unawaitedCalls).toHaveLength(5); + expect(unawaitedCalls.every((f) => f.file === 'scripts/__fixtures__/unawaited-sample.ts')).toBe(true); + expect(unawaitedCalls.every((f) => f.class === 'Sample')).toBe(true); + + // The fixture's own doc comment: three GOOD shapes produce nothing, five BAD + // ones each produce exactly one finding with a distinct reason. + const byMethod = Object.fromEntries(unawaitedCalls.map((f) => [f.method, f.reason])); + expect(byMethod).toEqual({ + bad1: 'stored-then-used', + bad2: 'argument', + bad3: 'property', + bad4: 'condition', + bad5: 'statement', + }); + expect(byMethod.good1).toBeUndefined(); + expect(byMethod.good2).toBeUndefined(); + expect(byMethod.good3).toBeUndefined(); + }); + + it('CALLGRAPH-002: --sync --tx finds nothing in the fixture tree (it touches no database) and exits 0', () => { + const { status, report } = run(['--root', 'scripts/__fixtures__', '--sync', '--tx', '--json']); + const { syncDbMethods, syncDbMethodCount, transactionSites, transactionSiteCount } = report as SyncTxReport; + + expect(status).toBe(0); + expect(syncDbMethodCount).toBe(0); + expect(syncDbMethods).toEqual([]); + expect(transactionSiteCount).toBe(0); + expect(transactionSites).toEqual([]); + }); +}); diff --git a/server/tests/unit/services/airtrailImport.test.ts b/server/tests/unit/services/airtrailImport.test.ts index 144b278de..f5bd659d9 100644 --- a/server/tests/unit/services/airtrailImport.test.ts +++ b/server/tests/unit/services/airtrailImport.test.ts @@ -24,7 +24,6 @@ import type { AirtrailService } from '../../../src/nest/integrations/airtrail.se import { notificationsStub } from '../../helpers/notifications'; import { accommodationsOver } from '../../helpers/accommodations-service'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; // The client and the per-user credentials are the only stubs; the reservation // writes go through the real service against the real test DB, as before. They diff --git a/server/tests/unit/services/conflictUpdate.test.ts b/server/tests/unit/services/conflictUpdate.test.ts index 2b807adab..acde47585 100644 --- a/server/tests/unit/services/conflictUpdate.test.ts +++ b/server/tests/unit/services/conflictUpdate.test.ts @@ -55,7 +55,6 @@ import { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.serv import { notificationsStub } from '../../helpers/notifications'; import { makeStorageFixture } from '../../helpers/storage-fixture'; import { createTestUnitOfWork } from '../../helpers/test-uow'; -import { UnitOfWork } from '../../../src/nest/database/unit-of-work'; const dbs = new DatabaseService(testDb); const realtime = new RealtimeService(); diff --git a/server/vitest.config.ts b/server/vitest.config.ts index a34aac4eb..8a7e68fb4 100644 --- a/server/vitest.config.ts +++ b/server/vitest.config.ts @@ -41,6 +41,11 @@ export default defineConfig({ // though every test passes). `istanbul` instruments the source directly, so // coverage is measured independently of the transform pipeline. provider: 'istanbul', + // Serialises report processing: three identical runs otherwise gave 13/6/6 + // threshold errors (files like platform/api-docs.ts flipping between 0% and + // 100%) because the default concurrency processes per-file coverage results + // out of order against istanbul's shared state. + processingConcurrency: 1, // json-summary is what the per-domain ratchet below is derived from: the text // reporter prints one row per DIRECTORY, not a recursive total, so reading the // thresholds off it silently understates any domain with subdirectories. diff --git a/wiki/Real-Time-Collaboration.md b/wiki/Real-Time-Collaboration.md index 6565c2d6f..2743bbc91 100644 --- a/wiki/Real-Time-Collaboration.md +++ b/wiki/Real-Time-Collaboration.md @@ -21,7 +21,7 @@ All changes to a trip — places, day plans, reservations, budget entries, and p **Authentication** uses a short-lived ephemeral token passed as a query parameter on connect. If authentication fails, the server closes the connection with one of these codes: -- **4001** — missing, invalid, or expired token; user not found +- **4001** — missing, invalid, or expired token; user not found; or an unexpected error during connection setup (reason `connection setup failed`) — reconnect; if it persists check the server log - **4403** — site-wide MFA is required but the account does not have MFA enabled An origin rejection happens before the socket exists, so it produces no close code at all — the browser just reports a failed connection. And because clients that send no `Origin` header are exempt, curl and other CLI clients keep connecting while every browser fails, which makes it look like a client bug. If real-time sync stops working behind a reverse proxy, check that `ALLOWED_ORIGINS` lists the exact scheme, host, and port the browser uses: the comparison is a plain string match, so `https://trek.example.com` matches neither `http://trek.example.com` nor `https://trek.example.com:443`. See [Environment-Variables](Environment-Variables) and [Reverse-Proxy](Reverse-Proxy). diff --git a/wiki/Troubleshooting.md b/wiki/Troubleshooting.md index aeedbd7b5..e20b8c762 100644 --- a/wiki/Troubleshooting.md +++ b/wiki/Troubleshooting.md @@ -351,7 +351,7 @@ If `ALLOWED_ORIGINS` is not set, the default is **same-origin only** — cross-o | Code | Reason | |------|--------| -| `4001` | No token, expired/invalid token, or user not found — re-login required | +| `4001` | No token, expired/invalid token, or user not found — re-login required; or an unexpected error during connection setup (reason `connection setup failed`) — reconnect; if it persists check the server log | | `4403` | MFA is required globally but the user has not enabled it | **Fix:**