mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
404 lines
19 KiB
TypeScript
404 lines
19 KiB
TypeScript
import { deriveHttpBoot, deriveKitinerary, deriveStorage, deriveTransit } from '../../../src/app-config/boot-derive';
|
|
import {
|
|
deriveApp,
|
|
deriveHttp,
|
|
deriveSession,
|
|
deriveDemo,
|
|
deriveMaps,
|
|
deriveOidc,
|
|
deriveSmtp,
|
|
deriveMcp,
|
|
derivePlugins,
|
|
deriveIntegrations,
|
|
deriveBackup,
|
|
deriveDb,
|
|
deriveFiles,
|
|
deriveNet,
|
|
derivePaths,
|
|
derivePush,
|
|
deriveAll,
|
|
} from '../../../src/app-config/derive';
|
|
import { OVERPASS_TIMEOUT_DEFAULT_MS } from '../../../src/nest/maps/maps.helpers';
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
// These tests PIN the exact legacy coercions each derived field replaced.
|
|
// If one fails after an edit, the edit changed runtime behavior — fix the
|
|
// edit, not the test (parity is law).
|
|
|
|
describe('deriveApp', () => {
|
|
it('NODE_ENV: loose helpers lowercase, isTest stays case-sensitive (db/database.ts)', () => {
|
|
expect(deriveApp({ NODE_ENV: 'PRODUCTION' }).isProduction).toBe(true);
|
|
expect(deriveApp({ NODE_ENV: 'Development' }).isDevelopment).toBe(true);
|
|
expect(deriveApp({ NODE_ENV: 'test' }).isTest).toBe(true);
|
|
expect(deriveApp({ NODE_ENV: 'TEST' }).isTest).toBe(false); // strict on purpose
|
|
expect(deriveApp({}).isProduction).toBe(false);
|
|
});
|
|
|
|
it('PORT: Number(x) || 3001 (index.ts)', () => {
|
|
expect(deriveApp({ PORT: '8080' }).port).toBe(8080);
|
|
expect(deriveApp({ PORT: 'abc' }).port).toBe(3001);
|
|
expect(deriveApp({}).port).toBe(3001);
|
|
});
|
|
|
|
it('APP_VERSION / APP_URL stay raw — per-site fallbacks live at the call sites', () => {
|
|
expect(deriveApp({}).appVersion).toBeUndefined();
|
|
expect(deriveApp({ APP_URL: 'https://x.example/' }).appUrl).toBe('https://x.example/');
|
|
});
|
|
|
|
it('DEFAULT_LANGUAGE resolves: case-insensitive, canonical code, en fallback', () => {
|
|
expect(deriveApp({ DEFAULT_LANGUAGE: 'EN' }).defaultLanguage).toBe('en');
|
|
expect(deriveApp({ DEFAULT_LANGUAGE: 'de' }).defaultLanguage).toBe('de');
|
|
expect(deriveApp({ DEFAULT_LANGUAGE: 'klingon' }).defaultLanguage).toBe('en');
|
|
expect(deriveApp({}).defaultLanguage).toBe('en');
|
|
// Mixed-case canonical codes resolve to their canonical form (legacy could
|
|
// never match zh-TW and silently served English).
|
|
expect(deriveApp({ DEFAULT_LANGUAGE: 'zh-TW' }).defaultLanguage).toBe('zh-TW');
|
|
expect(deriveApp({ DEFAULT_LANGUAGE: 'zh-tw' }).defaultLanguage).toBe('zh-TW');
|
|
});
|
|
});
|
|
|
|
describe('deriveHttp', () => {
|
|
it('ALLOWED_ORIGINS: CORS variant filters empties, websocket variant keeps them', () => {
|
|
const env = { ALLOWED_ORIGINS: ' https://a.example ,, https://b.example ' };
|
|
expect(deriveHttp(env).corsOrigins).toEqual(['https://a.example', 'https://b.example']);
|
|
expect(deriveHttp(env).wsOrigins).toEqual(['https://a.example', '', 'https://b.example']);
|
|
expect(deriveHttp({}).corsOrigins).toBeNull();
|
|
expect(deriveHttp({}).wsOrigins).toBeNull();
|
|
});
|
|
|
|
it('TRUST_PROXY: numeric hop count wins, 1 only as fallback (globalMiddleware, httpConfig token)', () => {
|
|
expect(deriveHttpBoot({ TRUST_PROXY: '2' }).trustProxy).toBe(2);
|
|
// 0 is a valid hop count ("trust no proxy"), not a missing value.
|
|
expect(deriveHttpBoot({ TRUST_PROXY: '0' }).trustProxy).toBe(0);
|
|
expect(deriveHttpBoot({ TRUST_PROXY: 'abc' }).trustProxy).toBe(1);
|
|
expect(deriveHttpBoot({}).trustProxy).toBe(1);
|
|
});
|
|
|
|
it("HTTP_KEEP_ALIVE_TIMEOUT_MS: 95 s unless set (above nginx's 60 s and Traefik's 90 s idle timeout)", () => {
|
|
expect(deriveHttpBoot({}).keepAliveTimeoutMs).toBe(95_000);
|
|
expect(deriveHttpBoot({ HTTP_KEEP_ALIVE_TIMEOUT_MS: '120000' }).keepAliveTimeoutMs).toBe(120_000);
|
|
});
|
|
|
|
it('boolean switches accept the whole boolean-like family (unified semantics)', () => {
|
|
expect(deriveHttp({ FORCE_HTTPS: 'TRUE' }).forceHttps).toBe(true);
|
|
expect(deriveHttp({ FORCE_HTTPS: 'on' }).forceHttps).toBe(true);
|
|
expect(deriveHttpBoot({ HSTS_INCLUDE_SUBDOMAINS: 'TRUE' }).hstsIncludeSubdomains).toBe(true);
|
|
expect(deriveHttpBoot({ HSTS_INCLUDE_SUBDOMAINS: '1' }).hstsIncludeSubdomains).toBe(true);
|
|
expect(deriveHttpBoot({ HSTS_INCLUDE_SUBDOMAINS: 'off' }).hstsIncludeSubdomains).toBe(false);
|
|
});
|
|
|
|
it('COOKIE_SECURE is tri-state: explicit falsy disables, anything else auto-detects', () => {
|
|
expect(deriveHttp({ COOKIE_SECURE: 'false' }).cookieSecureDisabled).toBe(true);
|
|
expect(deriveHttp({ COOKIE_SECURE: 'off' }).cookieSecureDisabled).toBe(true);
|
|
expect(deriveHttp({ COOKIE_SECURE: 'true' }).cookieSecureDisabled).toBe(false);
|
|
expect(deriveHttp({}).cookieSecureDisabled).toBe(false);
|
|
});
|
|
|
|
it('TREK_API_DOCS_ENABLED: trimmed, boolean-like (api-docs.kill-switch)', () => {
|
|
expect(deriveHttp({ TREK_API_DOCS_ENABLED: ' TRUE ' }).apiDocsEnabled).toBe(true);
|
|
expect(deriveHttp({ TREK_API_DOCS_ENABLED: '1' }).apiDocsEnabled).toBe(true);
|
|
expect(deriveHttp({ TREK_API_DOCS_ENABLED: 'false' }).apiDocsEnabled).toBe(false);
|
|
expect(deriveHttp({}).apiDocsEnabled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('deriveSession', () => {
|
|
it('parses durations and falls back on invalid input (src/config.ts semantics)', () => {
|
|
const ok = deriveSession({ SESSION_DURATION: '12h', SESSION_DURATION_REMEMBER: '90d' });
|
|
expect(ok.duration).toBe('12h');
|
|
expect(ok.durationMs).toBe(12 * 3_600_000);
|
|
expect(ok.durationSeconds).toBe(12 * 3600);
|
|
expect(ok.durationRememberMs).toBe(90 * 86_400_000);
|
|
|
|
const bad = deriveSession({ SESSION_DURATION: 'bogus' });
|
|
expect(bad.duration).toBe('24h');
|
|
expect(bad.durationMs).toBe(24 * 3_600_000);
|
|
expect(deriveSession({}).durationRemember).toBe('30d');
|
|
});
|
|
|
|
it('IDEMPOTENCY_TTL_SECONDS: positive number or 30 days (scheduler.ts)', () => {
|
|
expect(deriveSession({ IDEMPOTENCY_TTL_SECONDS: '60' }).idempotencyTtlSeconds).toBe(60);
|
|
expect(deriveSession({ IDEMPOTENCY_TTL_SECONDS: '0' }).idempotencyTtlSeconds).toBe(30 * 24 * 60 * 60);
|
|
expect(deriveSession({}).idempotencyTtlSeconds).toBe(30 * 24 * 60 * 60);
|
|
});
|
|
});
|
|
|
|
describe('deriveDemo', () => {
|
|
it('DEMO_MODE coerces the whole boolean-like family to one bool', () => {
|
|
expect(deriveDemo({ DEMO_MODE: 'TRUE' }).enabled).toBe(true);
|
|
expect(deriveDemo({ DEMO_MODE: 'yes' }).enabled).toBe(true);
|
|
expect(deriveDemo({ DEMO_MODE: 'off' }).enabled).toBe(false);
|
|
expect(deriveDemo({ DEMO_MODE: 'maybe' }).enabled).toBe(false); // out-of-family → default (validation rejects it at boot)
|
|
expect(deriveDemo({}).enabled).toBe(false);
|
|
});
|
|
|
|
it('DEMO_ADMIN_EMAIL stays raw — demo-seed and demo-reset apply different defaults', () => {
|
|
expect(deriveDemo({}).adminEmailRaw).toBeUndefined();
|
|
expect(deriveDemo({}).adminUser).toBe('admin');
|
|
expect(deriveDemo({}).adminPass).toBe('admin12345');
|
|
});
|
|
|
|
it('DEMO_ADMIN_PASS: adminPassSet tells the default apart from an explicit value', () => {
|
|
expect(deriveDemo({}).adminPassSet).toBe(false);
|
|
expect(deriveDemo({ DEMO_ADMIN_PASS: '' }).adminPassSet).toBe(false);
|
|
expect(deriveDemo({ DEMO_ADMIN_PASS: 'hunter22' }).adminPassSet).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('deriveOidc', () => {
|
|
it('scope and adminClaim carry their single-site defaults', () => {
|
|
expect(deriveOidc({}).scope).toBe('openid email profile');
|
|
expect(deriveOidc({}).adminClaim).toBe('groups');
|
|
expect(deriveOidc({ OIDC_ADMIN_CLAIM: 'roles' }).adminClaim).toBe('roles');
|
|
});
|
|
|
|
it('usernameClaim stays unset unless a claim is named (#1677)', () => {
|
|
expect(deriveOidc({}).usernameClaim).toBeUndefined();
|
|
expect(deriveOidc({ OIDC_USERNAME_CLAIM: ' ' }).usernameClaim).toBeUndefined();
|
|
expect(deriveOidc({ OIDC_USERNAME_CLAIM: ' preferred_username ' }).usernameClaim).toBe('preferred_username');
|
|
});
|
|
|
|
it('OIDC_ONLY coerces the boolean-like family', () => {
|
|
expect(deriveOidc({ OIDC_ONLY: 'True' }).only).toBe(true);
|
|
expect(deriveOidc({ OIDC_ONLY: '1' }).only).toBe(true);
|
|
expect(deriveOidc({ OIDC_ONLY: 'no' }).only).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('deriveSmtp', () => {
|
|
it('SMTP_SKIP_TLS_VERIFY coerces the boolean-like family', () => {
|
|
expect(deriveSmtp({ SMTP_SKIP_TLS_VERIFY: 'true' }).skipTlsVerify).toBe(true);
|
|
expect(deriveSmtp({ SMTP_SKIP_TLS_VERIFY: 'TRUE' }).skipTlsVerify).toBe(true);
|
|
expect(deriveSmtp({ SMTP_SKIP_TLS_VERIFY: 'off' }).skipTlsVerify).toBe(false);
|
|
expect(deriveSmtp({}).skipTlsVerify).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('deriveMcp', () => {
|
|
it('pins the mcp/index.ts module-const parsing', () => {
|
|
expect(deriveMcp({}).sessionTtlMs).toBe(3_600_000);
|
|
expect(deriveMcp({ MCP_MAX_SESSION_PER_USER: '5' }).maxSessionsPerUser).toBe(5);
|
|
expect(deriveMcp({ MCP_MAX_SESSION_PER_USER: '0' }).maxSessionsPerUser).toBe(20);
|
|
expect(deriveMcp({ MCP_SSE_KEEPALIVE: '0' }).sseKeepaliveMs).toBe(0);
|
|
expect(deriveMcp({ MCP_RATE_LIMIT: '100' }).rateLimitMax).toBe(100);
|
|
expect(deriveMcp({}).rateLimitMax).toBe(300);
|
|
});
|
|
});
|
|
|
|
describe('derivePlugins', () => {
|
|
it('kill-switch is default-on; only an explicit falsy value disables', () => {
|
|
expect(derivePlugins({}).enabled).toBe(true);
|
|
expect(derivePlugins({ TREK_PLUGINS_ENABLED: 'anything' }).enabled).toBe(true); // out-of-family → default-on
|
|
for (const v of ['false', '0', 'off', 'no', ' OFF ']) {
|
|
expect(derivePlugins({ TREK_PLUGINS_ENABLED: v }).enabled).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('devLink and permissions jail coerce the boolean-like family; jail is default-on', () => {
|
|
expect(derivePlugins({ TREK_PLUGINS_DEV_LINK: '1' }).devLink).toBe(true);
|
|
expect(derivePlugins({ TREK_PLUGINS_DEV_LINK: 'true' }).devLink).toBe(true);
|
|
expect(derivePlugins({ TREK_PLUGINS_DEV_LINK: 'off' }).devLink).toBe(false);
|
|
expect(derivePlugins({ TREK_PLUGIN_PERMISSIONS: 'OFF' }).permissionsOff).toBe(true);
|
|
expect(derivePlugins({ TREK_PLUGIN_PERMISSIONS: 'false' }).permissionsOff).toBe(true);
|
|
expect(derivePlugins({ TREK_PLUGIN_PERMISSIONS: 'on' }).permissionsOff).toBe(false);
|
|
expect(derivePlugins({}).permissionsOff).toBe(false);
|
|
});
|
|
|
|
it('ignoreTrekRange is off unless explicitly truthy — the version gate must never bypass by accident', () => {
|
|
expect(derivePlugins({}).ignoreTrekRange).toBe(false);
|
|
expect(derivePlugins({ TREK_PLUGINS_IGNORE_TREK_RANGE: '' }).ignoreTrekRange).toBe(false);
|
|
expect(derivePlugins({ TREK_PLUGINS_IGNORE_TREK_RANGE: 'off' }).ignoreTrekRange).toBe(false);
|
|
expect(derivePlugins({ TREK_PLUGINS_IGNORE_TREK_RANGE: '1' }).ignoreTrekRange).toBe(true);
|
|
expect(derivePlugins({ TREK_PLUGINS_IGNORE_TREK_RANGE: 'TRUE' }).ignoreTrekRange).toBe(true);
|
|
});
|
|
|
|
it('allowPrivateEgress is a bool (supervisor normalizes it to the literal "on" for the child)', () => {
|
|
expect(derivePlugins({ TREK_PLUGIN_ALLOW_PRIVATE_EGRESS: 'on' }).allowPrivateEgress).toBe(true);
|
|
expect(derivePlugins({ TREK_PLUGIN_ALLOW_PRIVATE_EGRESS: 'true' }).allowPrivateEgress).toBe(true);
|
|
expect(derivePlugins({ TREK_PLUGIN_ALLOW_PRIVATE_EGRESS: 'off' }).allowPrivateEgress).toBe(false);
|
|
expect(derivePlugins({}).allowPrivateEgress).toBe(false);
|
|
});
|
|
|
|
it('rate limits use Number(x) || default (rate-limit.ts / supervisor)', () => {
|
|
expect(derivePlugins({}).rpcBurst).toBe(60);
|
|
expect(derivePlugins({ TREK_PLUGIN_RPC_PER_SEC: '5' }).rpcPerSec).toBe(5);
|
|
expect(derivePlugins({ TREK_PLUGIN_MAX_RSS_MB: 'abc' }).maxRssMb).toBe(300);
|
|
});
|
|
});
|
|
|
|
describe('deriveIntegrations', () => {
|
|
it('pins unsplash trim, nominatim trim + default, overpass timeout', () => {
|
|
expect(deriveIntegrations({ UNSPLASH_ACCESS_KEY: ' key ' }).unsplashAccessKey).toBe('key');
|
|
expect(deriveIntegrations({}).nominatimUrl).toBe('https://nominatim.openstreetmap.org');
|
|
// Padded and whitespace-only both come back from a compose file or a ConfigMap,
|
|
// and the schema already called the second one unset.
|
|
expect(deriveIntegrations({ NOMINATIM_URL: ' http://nominatim:8080/geo// ' }).nominatimUrl).toBe(
|
|
'http://nominatim:8080/geo',
|
|
);
|
|
expect(deriveIntegrations({ NOMINATIM_URL: ' ' }).nominatimUrl).toBe('https://nominatim.openstreetmap.org');
|
|
// Against the constant, not a literal: the client budget is derived from the timeout
|
|
// the query itself carries, and pinning the number here is how the two drifted apart.
|
|
expect(deriveIntegrations({}).overpassTimeoutMs).toBe(OVERPASS_TIMEOUT_DEFAULT_MS);
|
|
expect(deriveIntegrations({ OVERPASS_TIMEOUT_MS: '-1' }).overpassTimeoutMs).toBe(OVERPASS_TIMEOUT_DEFAULT_MS);
|
|
});
|
|
|
|
it('floors the LLM ceiling to a whole number — undici rejects a fractional headersTimeout', () => {
|
|
expect(deriveIntegrations({}).llmTimeoutMs).toBe(900_000);
|
|
expect(deriveIntegrations({ LLM_TIMEOUT_MS: '-1' }).llmTimeoutMs).toBe(900_000);
|
|
expect(deriveIntegrations({ LLM_TIMEOUT_MS: '60000.5' }).llmTimeoutMs).toBe(60_000);
|
|
});
|
|
});
|
|
|
|
describe('deriveTransit (transitConfig token)', () => {
|
|
it('TRANSIT_API_URL: trailing slashes stripped, Transitous unless set', () => {
|
|
expect(deriveTransit({}).apiBase).toBe('https://api.transitous.org');
|
|
expect(deriveTransit({ TRANSIT_API_URL: 'https://t.example//' }).apiBase).toBe('https://t.example');
|
|
});
|
|
});
|
|
|
|
describe('deriveKitinerary (kitineraryConfig token)', () => {
|
|
const sep = process.platform === 'win32' ? ';' : ':';
|
|
|
|
it('KITINERARY_EXTRACTOR_PATH passes through as given, unset stays unset', () => {
|
|
expect(deriveKitinerary({}).extractorPath).toBeUndefined();
|
|
expect(deriveKitinerary({ KITINERARY_EXTRACTOR_PATH: '/opt/ki' }).extractorPath).toBe('/opt/ki');
|
|
});
|
|
|
|
it('splits PATH (or the Windows Path) on the platform delimiter and drops blank entries', () => {
|
|
expect(deriveKitinerary({ PATH: ['/a', ' /b ', '', '/c'].join(sep) }).searchPath).toEqual(['/a', '/b', '/c']);
|
|
expect(deriveKitinerary({ Path: ['/w1', '/w2'].join(sep) }).searchPath).toEqual(['/w1', '/w2']);
|
|
expect(deriveKitinerary({}).searchPath).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('deriveFiles (#1364)', () => {
|
|
it('defaults the upload limit to 50 MB and takes FILE_UPLOAD_LIMIT_MB', () => {
|
|
expect(deriveFiles({}).uploadLimitMb).toBe(50);
|
|
expect(deriveFiles({ FILE_UPLOAD_LIMIT_MB: '200' }).uploadLimitMb).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('deriveBackup', () => {
|
|
it('pins the backupService limit parsing (positive number or default)', () => {
|
|
expect(deriveBackup({}).uploadLimitMb).toBe(500);
|
|
expect(deriveBackup({ BACKUP_UPLOAD_LIMIT_MB: ' 750 ' }).uploadLimitMb).toBe(750);
|
|
expect(deriveBackup({ BACKUP_UPLOAD_LIMIT_MB: '-1' }).uploadLimitMb).toBe(500);
|
|
expect(deriveBackup({}).maxDecompressedMb).toBe(5 * 1024);
|
|
expect(deriveBackup({ ENCRYPTION_KEY: 'x' }).encryptionKeyFromEnv).toBe(true);
|
|
});
|
|
|
|
it('reads RESTORE_FROM_BACKUP as a trimmed path, and blank as unset (#1089)', () => {
|
|
expect(deriveBackup({}).restoreFromBackup).toBeNull();
|
|
expect(deriveBackup({ RESTORE_FROM_BACKUP: ' ' }).restoreFromBackup).toBeNull();
|
|
expect(deriveBackup({ RESTORE_FROM_BACKUP: ' /app/data/b.zip ' }).restoreFromBackup).toBe('/app/data/b.zip');
|
|
});
|
|
});
|
|
|
|
describe('deriveDb: the pre-migration snapshot', () => {
|
|
it('leaves the switch undefined when unset, so the caller can default it per environment', () => {
|
|
expect(deriveDb({}).preMigrateSnapshot).toBeUndefined();
|
|
expect(deriveDb({ TREK_DB_PRE_MIGRATE_SNAPSHOT: 'off' }).preMigrateSnapshot).toBe(false);
|
|
expect(deriveDb({ TREK_DB_PRE_MIGRATE_SNAPSHOT: 'TRUE' }).preMigrateSnapshot).toBe(true);
|
|
});
|
|
|
|
it('keeps three copies unless told otherwise, at most a hundred', () => {
|
|
expect(deriveDb({}).preMigrateSnapshotKeep).toBe(3);
|
|
expect(deriveDb({ TREK_DB_PRE_MIGRATE_SNAPSHOT_KEEP: '5' }).preMigrateSnapshotKeep).toBe(5);
|
|
expect(deriveDb({ TREK_DB_PRE_MIGRATE_SNAPSHOT_KEEP: '0' }).preMigrateSnapshotKeep).toBe(3);
|
|
expect(deriveDb({ TREK_DB_PRE_MIGRATE_SNAPSHOT_KEEP: '500' }).preMigrateSnapshotKeep).toBe(100);
|
|
});
|
|
});
|
|
|
|
describe('deriveNet', () => {
|
|
it('ALLOW_INTERNAL_NETWORK coerces the boolean-like family (ssrfGuard)', () => {
|
|
expect(deriveNet({ ALLOW_INTERNAL_NETWORK: 'True' }).allowInternalNetwork).toBe(true);
|
|
expect(deriveNet({ ALLOW_INTERNAL_NETWORK: '1' }).allowInternalNetwork).toBe(true);
|
|
expect(deriveNet({}).allowInternalNetwork).toBe(false);
|
|
});
|
|
|
|
it('ALLOW_LINK_LOCAL_IPS keeps only the addresses that may be used', () => {
|
|
expect(deriveNet({ ALLOW_LINK_LOCAL_IPS: '169.254.1.2,169.254.169.254,bogus' }).allowLinkLocalIps).toEqual([
|
|
'169.254.1.2',
|
|
]);
|
|
expect(deriveNet({}).allowLinkLocalIps).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('derivePaths', () => {
|
|
it('passes the path vars through raw — defaulting stays at the consumer', () => {
|
|
expect(derivePaths({ TREK_WIKI_DIR: '/w' }).wikiDir).toBe('/w');
|
|
expect(deriveStorage({ TREK_PLACE_PHOTO_DIR: '/p' }).placePhotoDir).toBe('/p');
|
|
expect(derivePaths({}).wikiDir).toBeUndefined();
|
|
expect(deriveStorage({}).placePhotoDir).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('deriveAll', () => {
|
|
it('assembles every namespace', () => {
|
|
const env = deriveAll({ PORT: '4000', DEMO_MODE: 'true' });
|
|
expect(env.app.port).toBe(4000);
|
|
expect(env.demo.enabled).toBe(true);
|
|
for (const ns of [
|
|
'app',
|
|
'http',
|
|
'session',
|
|
'demo',
|
|
'adminBootstrap',
|
|
'oidc',
|
|
'smtp',
|
|
'mcp',
|
|
'plugins',
|
|
'webauthn',
|
|
'integrations',
|
|
'backup',
|
|
'db',
|
|
'paths',
|
|
'net',
|
|
'push',
|
|
] as const) {
|
|
expect(env[ns]).toBeDefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('deriveMaps', () => {
|
|
it('TREK_PLACES_ENABLED accepts the same false family as every other switch', () => {
|
|
// The schema admits the whole boolean-like family for this variable, so a
|
|
// value it lets through must also count: an operator who writes 0 or off,
|
|
// as the .env.example header invites, gets the index switched off rather
|
|
// than a silent no-op.
|
|
expect(deriveMaps({ TREK_PLACES_ENABLED: 'false' } as never).trekPlacesEnabled).toBe(false);
|
|
for (const value of ['FALSE', '0', 'off', 'no', ' No ']) {
|
|
expect(deriveMaps({ TREK_PLACES_ENABLED: value } as never).trekPlacesEnabled, value).toBe(false);
|
|
}
|
|
for (const value of ['true', 'TRUE', '1', 'on', 'yes']) {
|
|
expect(deriveMaps({ TREK_PLACES_ENABLED: value } as never).trekPlacesEnabled, value).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('TREK_PLACES_ENABLED fails open when unset, blank or outside the family', () => {
|
|
// A typo must not silently drop a whole install back to Nominatim, whose
|
|
// usage policy forbids what TREK was doing with it. Outside the family the
|
|
// schema aborts boot anyway; this covers the paths that skip validation.
|
|
expect(deriveMaps({} as never).trekPlacesEnabled).toBe(true);
|
|
for (const value of ['', ' ', 'maybe', 'fasle']) {
|
|
expect(deriveMaps({ TREK_PLACES_ENABLED: value } as never).trekPlacesEnabled, value).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('derivePush', () => {
|
|
it('passes the VAPID_* values through trimmed, blank counting as unset', () => {
|
|
expect(derivePush({})).toEqual({ vapidPublicKey: undefined, vapidPrivateKey: undefined, vapidSubject: undefined });
|
|
expect(
|
|
derivePush({ VAPID_PUBLIC_KEY: ' BPub ', VAPID_PRIVATE_KEY: 'priv', VAPID_SUBJECT: ' mailto:ops@example.com ' }),
|
|
).toEqual({ vapidPublicKey: 'BPub', vapidPrivateKey: 'priv', vapidSubject: 'mailto:ops@example.com' });
|
|
expect(derivePush({ VAPID_PUBLIC_KEY: ' ', VAPID_SUBJECT: '' })).toEqual({
|
|
vapidPublicKey: undefined,
|
|
vapidPrivateKey: undefined,
|
|
vapidSubject: undefined,
|
|
});
|
|
});
|
|
});
|