mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
80183bc986
POST /api/routing/valhalla/route, /trace_attributes and /height ask the Valhalla at TREK_VALHALLA_URL (default https://valhalla.liketrek.com) for a signed-in user. The body is held to the shared contract (64 KB, at most 20 locations), every question counts against 120 a minute per user, and the installation key goes along only to a host TREK_SERVICES_HOSTS lists. Valhalla's answer is passed on as it came, refusals included. The road trip router asks the same relay when the routing choice lands on the TREK services, so the MCP road trip, city walk and hike tools share its limits, and hands a question it cannot answer to FOSSGIS.
465 lines
22 KiB
TypeScript
465 lines
22 KiB
TypeScript
import {
|
|
deriveHttpBoot,
|
|
deriveKitinerary,
|
|
deriveServices,
|
|
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('says whether the operator set a Nominatim of their own, which keeps search off Photon', () => {
|
|
expect(deriveIntegrations({}).nominatimOwn).toBe(false);
|
|
expect(deriveIntegrations({ NOMINATIM_URL: ' ' }).nominatimOwn).toBe(false);
|
|
expect(deriveIntegrations({ NOMINATIM_URL: 'http://nominatim:8080' }).nominatimOwn).toBe(true);
|
|
// The public address spelled out is still a choice the operator made.
|
|
expect(deriveIntegrations({ NOMINATIM_URL: 'https://nominatim.openstreetmap.org' }).nominatimOwn).toBe(true);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
it('leaves the LLM answer cap to each client unless one is set (#2677)', () => {
|
|
expect(deriveIntegrations({}).llmMaxOutputTokens).toBeUndefined();
|
|
expect(deriveIntegrations({ LLM_MAX_OUTPUT_TOKENS: '' }).llmMaxOutputTokens).toBeUndefined();
|
|
expect(deriveIntegrations({ LLM_MAX_OUTPUT_TOKENS: '16384' }).llmMaxOutputTokens).toBe(16384);
|
|
});
|
|
});
|
|
|
|
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('deriveServices (servicesConfig token)', () => {
|
|
it('registers by itself unless TREK_SERVICES says off, and any value but auto is off', () => {
|
|
expect(deriveServices({}).mode).toBe('auto');
|
|
expect(deriveServices({ TREK_SERVICES: ' AUTO ' }).mode).toBe('auto');
|
|
expect(deriveServices({ TREK_SERVICES: 'off' }).mode).toBe('off');
|
|
expect(deriveServices({ TREK_SERVICES: 'of' }).mode).toBe('off');
|
|
});
|
|
|
|
it('takes a key from TREK_SERVICES_KEY, trimmed, and no key for a blank one', () => {
|
|
expect(deriveServices({ TREK_SERVICES_KEY: ' tk1.k.p.s ' }).key).toBe('tk1.k.p.s');
|
|
expect(deriveServices({ TREK_SERVICES_KEY: ' ' }).key).toBeNull();
|
|
expect(deriveServices({}).key).toBeNull();
|
|
});
|
|
|
|
it('sends the key to the three liketrek services unless TREK_SERVICES_HOSTS names others', () => {
|
|
const all = ['places.liketrek.com', 'photon.liketrek.com', 'valhalla.liketrek.com'];
|
|
expect(deriveServices({}).hosts).toEqual(all);
|
|
expect(deriveServices({ TREK_SERVICES_HOSTS: 'Places.LikeTrek.com, , photon.liketrek.com' }).hosts).toEqual([
|
|
'places.liketrek.com',
|
|
'photon.liketrek.com',
|
|
]);
|
|
expect(deriveServices({ TREK_SERVICES_HOSTS: ' , ' }).hosts).toEqual(all);
|
|
});
|
|
|
|
it('asks photon.liketrek.com unless TREK_PHOTON_URL names another copy, trimmed and without trailing slashes', () => {
|
|
expect(deriveServices({}).photonUrl).toBe('https://photon.liketrek.com');
|
|
expect(deriveServices({ TREK_PHOTON_URL: ' ' }).photonUrl).toBe('https://photon.liketrek.com');
|
|
expect(deriveServices({ TREK_PHOTON_URL: ' https://geo.example.org/photon// ' }).photonUrl).toBe(
|
|
'https://geo.example.org/photon',
|
|
);
|
|
});
|
|
|
|
it('relays routing to valhalla.liketrek.com unless TREK_VALHALLA_URL names another, without its trailing slash', () => {
|
|
expect(deriveServices({}).valhallaUrl).toBe('https://valhalla.liketrek.com');
|
|
expect(deriveServices({ TREK_VALHALLA_URL: ' http://valhalla.lan:8002/ ' }).valhallaUrl).toBe(
|
|
'http://valhalla.lan:8002',
|
|
);
|
|
expect(deriveServices({ TREK_VALHALLA_URL: ' ' }).valhallaUrl).toBe('https://valhalla.liketrek.com');
|
|
});
|
|
});
|
|
|
|
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,
|
|
});
|
|
});
|
|
});
|