fix(server): phase 1 review follow-ups — awaited returns, logged upload-filter rejections, covered handshake/notice branches, call-graph gate in CI, dead test imports

This commit is contained in:
jubnl
2026-09-22 07:54:09 +02:00
parent f202227bbc
commit ae2af05cad
107 changed files with 324 additions and 209 deletions
+3
View File
@@ -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
+2 -2
View File
@@ -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;
@@ -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) {
+1 -1
View File
@@ -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<boolean> {
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;
+5 -1
View File
@@ -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)}`);
}
}
}
@@ -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)}`);
});
+6 -2
View File
@@ -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)));
});
};
}
@@ -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)));
});
};
}
+3 -1
View File
@@ -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<void> {
// TrekWsAdapter.bindClientConnect fires this from a plain 'connection'
@@ -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)) {
@@ -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);
}
+1 -1
View File
@@ -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';
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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 = {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
@@ -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)', () => {
-2
View File
@@ -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 {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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';
/**
-2
View File
@@ -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)', () => {
-2
View File
@@ -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. */
-2
View File
@@ -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';
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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';
@@ -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 {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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 {
@@ -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' };
@@ -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 = {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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. */
@@ -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)', () => {
-2
View File
@@ -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(() => {
@@ -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';
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
@@ -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)', () => {
@@ -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 = {
-2
View File
@@ -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 {
-2
View File
@@ -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 {
@@ -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;
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
-2
View File
@@ -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)', () => {
@@ -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.
@@ -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,
-1
View File
@@ -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
@@ -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 () => {
@@ -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 });
@@ -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 {
@@ -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();
});
});
@@ -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.
@@ -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);
@@ -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);
@@ -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
@@ -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.
@@ -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
@@ -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<ReturnType<typeof makeAccommodationsService>>;
@@ -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<SettingsService>);
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' });
});
});
@@ -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();
@@ -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();
@@ -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);
});
});
@@ -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.
@@ -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';
@@ -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');
@@ -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();
@@ -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;
@@ -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 ───────────────────────────────────────────────────────
//
+5 -5
View File
@@ -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);
},
);
@@ -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 () => {
@@ -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 () => {
@@ -59,7 +59,7 @@ function makeMcp(over: Partial<Setup> = {}) {
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([
@@ -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');
});
});
@@ -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() }));
@@ -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;
@@ -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.
@@ -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.
@@ -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 () => {
@@ -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;
@@ -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');
@@ -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', () => {
@@ -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 () => {
+25 -25
View File
@@ -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');
@@ -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 () => {
+10 -10
View File
@@ -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<string, unknown> };
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<string, unknown> };
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);
}
});
@@ -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.
@@ -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);
@@ -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
@@ -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.

Some files were not shown because too many files have changed in this diff Show More