mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
ba1504d6e9
Eight registerAs tokens existed while every consumer read the same values through readEnv() instead, so the boot-stable layer the repo rules describe was unused and nothing said which path owned a variable. FORCE_HTTPS, for instance, was frozen by the middleware through the http token and read live by the cookie code at the same time. A variable is now owned by exactly one side. TRUST_PROXY and HSTS_INCLUDE_SUBDOMAINS belong to httpConfig, which buildApp() already injects into the pre-init middleware, and TREK_PLACE_PHOTO_DIR to the new storageConfig, which the storage registry now injects instead of reading it through RuntimeEnvService. Their derive functions moved to boot-derive.ts, which deriveAll() never calls. The six tokens nothing injected are gone: their values are needed before the container exists (config.ts, the database, src/mcp, the SSRF guard) and stay on readEnv(). config-ownership.test.ts records the keys each side reads through a Proxy and fails on a variable read through both, on a token nothing injects and on a variable the schema does not validate. env-reference.test.ts holds server/.env.example to the schema, which had drifted by 34 variables (SMTP, WebAuthn, plugins, MCP session tuning, TREK_MANAGED and others); they are documented now. jwt-verify.ts no longer cites a middleware file that is gone.
103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { Test } from '@nestjs/testing';
|
|
import { ConfigService, ConfigType } from '@nestjs/config';
|
|
|
|
import { AppConfigModule } from '../../../src/nest/app-config/app-config.module';
|
|
import { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.service';
|
|
import { httpConfig, storageConfig, BOOT_STABLE_TOKENS } from '../../../src/nest/app-config/tokens';
|
|
import { DataPathsService } from '../../../src/nest/app-config/data-paths.service';
|
|
|
|
describe('RuntimeEnvService', () => {
|
|
const service = new RuntimeEnvService();
|
|
let prevDemo: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
prevDemo = process.env.DEMO_MODE;
|
|
});
|
|
afterEach(() => {
|
|
if (prevDemo === undefined) delete process.env.DEMO_MODE;
|
|
else process.env.DEMO_MODE = prevDemo;
|
|
});
|
|
|
|
it('reads live — a mid-lifetime env mutation is visible on the next call', () => {
|
|
process.env.DEMO_MODE = 'true';
|
|
expect(service.isDemoMode()).toBe(true);
|
|
process.env.DEMO_MODE = 'off';
|
|
expect(service.isDemoMode()).toBe(false);
|
|
delete process.env.DEMO_MODE;
|
|
expect(service.isDemoMode()).toBe(false);
|
|
});
|
|
|
|
it('env() exposes the full live-derived namespaces', () => {
|
|
process.env.DEMO_MODE = 'yes';
|
|
expect(service.env().demo.enabled).toBe(true);
|
|
expect(service.env().app.port).toBeTypeOf('number');
|
|
});
|
|
});
|
|
|
|
describe('boot-stable tokens', () => {
|
|
it('factories re-derive from the current env on each invocation', () => {
|
|
const prev = process.env.HSTS_INCLUDE_SUBDOMAINS;
|
|
try {
|
|
process.env.HSTS_INCLUDE_SUBDOMAINS = 'on';
|
|
expect(httpConfig().hstsIncludeSubdomains).toBe(true);
|
|
process.env.HSTS_INCLUDE_SUBDOMAINS = 'off';
|
|
expect(httpConfig().hstsIncludeSubdomains).toBe(false);
|
|
} finally {
|
|
if (prev === undefined) delete process.env.HSTS_INCLUDE_SUBDOMAINS;
|
|
else process.env.HSTS_INCLUDE_SUBDOMAINS = prev;
|
|
}
|
|
});
|
|
|
|
it('every token carries a namespaced KEY for @Inject', () => {
|
|
for (const token of BOOT_STABLE_TOKENS) {
|
|
expect(token.KEY).toMatch(/^CONFIGURATION\(.+\)$/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('AppConfigModule', () => {
|
|
it('provides ConfigService, RuntimeEnvService, DataPathsService and the loaded namespaces', async () => {
|
|
const prev = process.env.TREK_PLACE_PHOTO_DIR;
|
|
process.env.TREK_PLACE_PHOTO_DIR = '/srv/place-photos';
|
|
const moduleRef = await Test.createTestingModule({ imports: [AppConfigModule] }).compile();
|
|
if (prev === undefined) delete process.env.TREK_PLACE_PHOTO_DIR;
|
|
else process.env.TREK_PLACE_PHOTO_DIR = prev;
|
|
try {
|
|
const config = moduleRef.get(ConfigService);
|
|
const runtime = moduleRef.get(RuntimeEnvService);
|
|
const storage = moduleRef.get<ConfigType<typeof storageConfig>>(storageConfig.KEY);
|
|
expect(runtime).toBeInstanceOf(RuntimeEnvService);
|
|
expect(moduleRef.get(DataPathsService)).toBeInstanceOf(DataPathsService);
|
|
// A snapshot taken when the module was built, not a live read.
|
|
expect(storage.placePhotoDir).toBe('/srv/place-photos');
|
|
// Plain get() falls through to live process.env (cache: false).
|
|
expect(config.get('NODE_ENV')).toBe('test');
|
|
} finally {
|
|
await moduleRef.close();
|
|
}
|
|
});
|
|
|
|
// Guards `ignoreEnvFile: true`: a marker var present only in server/.env must
|
|
// stay invisible. Skipped locally when a real server/.env exists (we won't
|
|
// clobber it); CI has none, so the regression is always covered there.
|
|
const envPath = path.resolve(process.cwd(), '.env');
|
|
it.skipIf(fs.existsSync(envPath))('never reads a .env file (dotenv stays index.ts-only)', async () => {
|
|
fs.writeFileSync(envPath, 'TREK_TEST_ENVFILE_MARKER=leaked\n');
|
|
try {
|
|
const moduleRef = await Test.createTestingModule({ imports: [AppConfigModule] }).compile();
|
|
try {
|
|
const config = moduleRef.get(ConfigService);
|
|
expect(config.get('TREK_TEST_ENVFILE_MARKER')).toBeUndefined();
|
|
expect(process.env.TREK_TEST_ENVFILE_MARKER).toBeUndefined();
|
|
} finally {
|
|
await moduleRef.close();
|
|
}
|
|
} finally {
|
|
fs.rmSync(envPath, { force: true });
|
|
}
|
|
});
|
|
});
|