Files
TREK/server/tests/setup.ts
T
Maurice ba7d55f999 Finish the contract ratchet, the upload bridge and the client leftovers
BODY_CONTRACT_ALLOW_LIST is empty. The last seventeen handlers type their
body with a createZodDto class, so the fail-closed boot gate has nothing left
to forgive; it still refuses a boot for a new handler that reads @Body()
without one.

Several of those contracts describe rather than constrain, and that is the
whole care in this change. The pipe runs before the handler, so a schema
stricter than the handler moves a user-visible answer:

- TagsController.create answers { error: 'Tag name is required' }, pinned
  byte-for-byte in two tests, and the pipe formats as `field: message`. The
  DTO leaves name optional and the handler keeps the check that owns the
  message. Same shape for the booking-import confirm body, which additionally
  gains per-item validation the handler never did.
- PluginsController.activate and install answer 503 when plugins are disabled,
  and check that BEFORE looking at the body. A schema that rejected first
  would turn that into a 400 for an operator whose server has plugins off.
- setEgressHosts reads a non-array `hosts` as "clear the list", which is the
  documented reset. Typing it as string[] would have removed a working admin
  action rather than tightened anything.
- PluginRoutesController.route answers { route: null } at 200 for everything
  it dislikes, so the client falls back to straight lines. A 400 there would
  turn a graceful degradation into a visible error.

files.bridge is gone. The multer options are built by
MulterModule.registerAsync from an AllowedFileTypesService leaf, so the
fileFilter reads the operator's extension list from the container instead of
from a second FilesService built outside it. Nest injects
MULTER_MODULE_OPTIONS with @Optional(), so a token that fails to resolve is
not a boot error: it silently falls back to defaults, with no filter and no
size cap. FILE-020 already proved that for trip files by uploading an .svg
over HTTP and expecting 400; journey uploads had no such case, and now do.
VIDEO_UPLOAD is untouched, including the filename callback that forces .jpg
regardless of what the client sends, which guards the public share proxy.

PluginFrame gets an onError. A frame whose document fails to load left a
blank element at full height, which reads as a hung app rather than as a
plugin that is gone.

typecheck:tests runs in CI now, with the test tree at zero errors. tz-lookup
is left as it is: the build puts it in a lazy helpers chunk, not the entry
chunk, and the remaining split would trade a synchronous render for an async
one to move 172 kB that is already off the critical path.
2026-08-10 19:54:46 +02:00

46 lines
2.7 KiB
TypeScript

// Global test setup — runs before every test file.
// Environment variables must be set before any module import so that
// config.ts, database.ts, etc. pick them up at import time. (Importing from
// 'vitest' itself is safe: it is externalized and pulls in no app modules.)
import { afterEach } from 'vitest';
// The one app module imported here, and it is deliberately a static import even
// though import declarations are hoisted above the process.env writes below.
// The chain is nominatim.client -> maps.helpers -> app-config, and its only
// load-time read of the environment is the UA constant's getAppUrl(), which
// looks at APP_URL, ALLOWED_ORIGINS and PORT. This file sets none of those, so
// nothing in the chain observes a variable before it is written; config.ts and
// database.ts are not on it.
//
// It also has to be static. A setup file is imported for its side effects, not
// awaited as a hook, so a deferred `import()` would still be resolving while the
// test file registers its own mocks: booking-import's kitinerary suite replaces
// app-config with a factory that has no getAppUrl, and the late import then
// resolves maps.helpers against that mock and throws.
import { setGeoThrottleInterval } from '../src/nest/geo/nominatim.client';
// Fixed encryption key (64 hex chars = 32 bytes) for at-rest crypto in tests
process.env.ENCRYPTION_KEY = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2';
process.env.NODE_ENV = 'test';
process.env.COOKIE_SECURE = 'false';
process.env.LOG_LEVEL = 'error'; // suppress info/debug logs in test output
// Several services fire notification sends as unawaited dynamic-import chains
// (`import('…/notificationService').then(({ send }) => send(…).catch(…))`).
// Give those chains one macrotask turn to settle after every test, while the
// suite's DB and the worker environment are still alive — otherwise the last
// test in a file can leave the chain pending into worker teardown ("Cannot
// load ... after the environment was torn down"). Under the default "stack"
// hook order this afterEach runs after each test's own afterEach and before
// the suite's afterAll, i.e. before any afterAll closes its test DB.
// setImmediate is captured up front because some suites install fake timers.
const realSetImmediate = globalThis.setImmediate;
afterEach(async () => {
await new Promise((resolve) => realSetImmediate(resolve));
});
// Nominatim's rate limit is a property of the real service, not of the code
// under test. Nineteen maps cases stub fetch and drive those paths back to back;
// under the real 1.1s interval they would spend nineteen seconds asleep. The
// cases that are actually about the throttle set it back for themselves.
setGeoThrottleInterval(0);