672 lines
27 KiB
TypeScript
672 lines
27 KiB
TypeScript
/**
|
|
* app.config.test.ts
|
|
*
|
|
* Comprehensive unit tests for loadConfig() in app.config.ts.
|
|
*
|
|
* Strategy:
|
|
* - Each test saves the current process.env, mutates it, calls loadConfig(),
|
|
* then restores the original env. This avoids vi.resetModules() and makes
|
|
* every test fully isolated.
|
|
*
|
|
* Coverage targets:
|
|
* ✅ All 11 config sections and every field inside them
|
|
* ✅ Default values (no env vars set)
|
|
* ✅ Env-var overrides (custom values respected)
|
|
* ✅ Numeric parsing (PORT, POSTGRES_PORT, MAX_FILE_SIZE, limits …)
|
|
* ✅ Optional / undefined fields (s3Bucket, accessApiKey, vercelToken …)
|
|
* ✅ Regression: PORT default fixed from 3000 → 7130
|
|
* ✅ Regression: storage.appKey defaults to 'local', not 'default-app-key'
|
|
* ✅ PARENT_APP_KEY whitespace trimming
|
|
* ✅ MAX_FILE_SIZE absent → undefined (not 0 or NaN)
|
|
* ✅ Deployment byte limits default to 100 MiB
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { loadConfig } from '../../src/infra/config/app.config';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper — save / restore process.env around each test
|
|
// ---------------------------------------------------------------------------
|
|
let savedEnv: NodeJS.ProcessEnv;
|
|
|
|
beforeEach(() => {
|
|
savedEnv = { ...process.env };
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = savedEnv;
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Utility to wipe a set of keys so defaults kick in cleanly
|
|
// ---------------------------------------------------------------------------
|
|
function unsetEnvKeys(...keys: string[]) {
|
|
for (const k of keys) {
|
|
delete process.env[k];
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: app
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.app', () => {
|
|
it('uses defaults when no env vars are set', () => {
|
|
unsetEnvKeys('PORT', 'JWT_SECRET', 'ACCESS_API_KEY', 'LOG_LEVEL');
|
|
const c = loadConfig();
|
|
|
|
expect(c.app.port).toBe(7130);
|
|
expect(c.app.jwtSecret).toBe('');
|
|
expect(c.app.apiKey).toBe('your_api_key');
|
|
expect(c.app.logLevel).toBe('info');
|
|
});
|
|
|
|
it('overrides all app fields from env', () => {
|
|
process.env.PORT = '9000';
|
|
process.env.JWT_SECRET = 'super-secret-jwt';
|
|
process.env.ACCESS_API_KEY = 'ik_test1234';
|
|
process.env.LOG_LEVEL = 'debug';
|
|
const c = loadConfig();
|
|
|
|
expect(c.app.port).toBe(9000);
|
|
expect(c.app.jwtSecret).toBe('super-secret-jwt');
|
|
expect(c.app.apiKey).toBe('ik_test1234');
|
|
expect(c.app.logLevel).toBe('debug');
|
|
});
|
|
|
|
// ── PORT regression ──────────────────────────────────────────────────────
|
|
it('REGRESSION: PORT default is 7130 (was incorrectly 3000)', () => {
|
|
unsetEnvKeys('PORT');
|
|
expect(loadConfig().app.port).toBe(7130);
|
|
expect(loadConfig().app.port).not.toBe(3000);
|
|
});
|
|
|
|
it('parses PORT as an integer', () => {
|
|
process.env.PORT = '8080';
|
|
expect(loadConfig().app.port).toBe(8080);
|
|
expect(typeof loadConfig().app.port).toBe('number');
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: telemetry
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.telemetry', () => {
|
|
it('enables anonymous telemetry by default', () => {
|
|
unsetEnvKeys(
|
|
'INSFORGE_TELEMETRY_DISABLED',
|
|
'INSFORGE_TELEMETRY_DEBUG',
|
|
'INSFORGE_TELEMETRY_ENDPOINT',
|
|
'INSFORGE_TELEMETRY_POSTHOG_API_KEY',
|
|
'INSFORGE_TELEMETRY_INSTALLATION_ID_PATH',
|
|
'INSFORGE_TELEMETRY_HEARTBEAT_INTERVAL_MS',
|
|
'INSFORGE_TELEMETRY_REQUEST_TIMEOUT_MS',
|
|
'LOGS_DIR'
|
|
);
|
|
const c = loadConfig();
|
|
|
|
expect(c.telemetry).toEqual({ disabled: false });
|
|
});
|
|
|
|
it('only exposes the telemetry disabled env var', () => {
|
|
process.env.INSFORGE_TELEMETRY_DISABLED = 'true';
|
|
process.env.INSFORGE_TELEMETRY_DEBUG = '1';
|
|
process.env.INSFORGE_TELEMETRY_ENDPOINT = 'https://events.example.com/v1';
|
|
process.env.INSFORGE_TELEMETRY_POSTHOG_API_KEY = 'phc_custom';
|
|
process.env.INSFORGE_TELEMETRY_INSTALLATION_ID_PATH = '/tmp/insforge-id';
|
|
process.env.INSFORGE_TELEMETRY_HEARTBEAT_INTERVAL_MS = '60000';
|
|
process.env.INSFORGE_TELEMETRY_REQUEST_TIMEOUT_MS = '500';
|
|
const c = loadConfig();
|
|
|
|
expect(c.telemetry).toEqual({ disabled: true });
|
|
});
|
|
|
|
it('disables telemetry with the documented opt-out value', () => {
|
|
process.env.INSFORGE_TELEMETRY_DISABLED = '1';
|
|
const c = loadConfig();
|
|
|
|
expect(c.telemetry).toEqual({ disabled: true });
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: cloud
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.cloud', () => {
|
|
it('uses defaults when no env vars are set', () => {
|
|
unsetEnvKeys(
|
|
'AWS_S3_BUCKET',
|
|
'AWS_INSTANCE_PROFILE_NAME',
|
|
'CLOUD_API_HOST',
|
|
'PROJECT_ID',
|
|
'APP_KEY',
|
|
'AWS_CLOUDFRONT_URL',
|
|
'AWS_CLOUDFRONT_KEY_PAIR_ID',
|
|
'AWS_CLOUDFRONT_PRIVATE_KEY'
|
|
);
|
|
const c = loadConfig();
|
|
|
|
expect(c.cloud.apiHost).toBe('https://api.insforge.dev');
|
|
expect(c.cloud.projectId).toBeUndefined();
|
|
expect(c.cloud.cloudFrontUrl).toBeUndefined();
|
|
expect(c.cloud.cloudFrontKeyPairId).toBeUndefined();
|
|
expect(c.cloud.cloudFrontPrivateKey).toBeUndefined();
|
|
});
|
|
|
|
it('overrides cloud fields from env', () => {
|
|
process.env.CLOUD_API_HOST = 'https://custom.api.dev';
|
|
process.env.PROJECT_ID = 'proj-abc123';
|
|
process.env.AWS_CLOUDFRONT_URL = 'https://xyz.cloudfront.net';
|
|
process.env.AWS_CLOUDFRONT_KEY_PAIR_ID = 'K1234567890';
|
|
process.env.AWS_CLOUDFRONT_PRIVATE_KEY = 'mock-private-key-data';
|
|
const c = loadConfig();
|
|
|
|
expect(c.cloud.apiHost).toBe('https://custom.api.dev');
|
|
expect(c.cloud.projectId).toBe('proj-abc123');
|
|
expect(c.cloud.cloudFrontUrl).toBe('https://xyz.cloudfront.net');
|
|
expect(c.cloud.cloudFrontKeyPairId).toBe('K1234567890');
|
|
expect(c.cloud.cloudFrontPrivateKey).toBe('mock-private-key-data');
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: denoSubhosting
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.denoSubhosting', () => {
|
|
it('uses empty-string defaults when tokens are absent', () => {
|
|
unsetEnvKeys('DENO_DEPLOY_TOKEN', 'DENO_DEPLOY_ORG_ID', 'FUNCTIONS_DOMAIN');
|
|
const c = loadConfig();
|
|
|
|
expect(c.denoSubhosting.token).toBe('');
|
|
expect(c.denoSubhosting.organizationId).toBe('');
|
|
expect(c.denoSubhosting.domain).toBe('function2.insforge.app');
|
|
});
|
|
|
|
it('reads DENO_DEPLOY_TOKEN and DENO_DEPLOY_ORG_ID', () => {
|
|
process.env.DENO_DEPLOY_TOKEN = 'dsh_token_test';
|
|
process.env.DENO_DEPLOY_ORG_ID = 'org-abc';
|
|
const c = loadConfig();
|
|
|
|
expect(c.denoSubhosting.token).toBe('dsh_token_test');
|
|
expect(c.denoSubhosting.organizationId).toBe('org-abc');
|
|
});
|
|
|
|
it('domain defaults to the v2 proxy domain and is overridable via FUNCTIONS_DOMAIN', () => {
|
|
unsetEnvKeys('FUNCTIONS_DOMAIN');
|
|
expect(loadConfig().denoSubhosting.domain).toBe('function2.insforge.app');
|
|
|
|
process.env.FUNCTIONS_DOMAIN = 'custom.functions.example.com';
|
|
expect(loadConfig().denoSubhosting.domain).toBe('custom.functions.example.com');
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: fly
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.fly', () => {
|
|
it('uses empty-string defaults when tokens are absent', () => {
|
|
unsetEnvKeys('FLY_API_TOKEN', 'FLY_ORG', 'COMPUTE_DOMAIN');
|
|
const c = loadConfig();
|
|
|
|
expect(c.fly.apiToken).toBe('');
|
|
expect(c.fly.org).toBe('');
|
|
expect(c.fly.domain).toBe('');
|
|
});
|
|
|
|
it('reads Fly env vars correctly', () => {
|
|
process.env.FLY_API_TOKEN = 'fly-secret-token';
|
|
process.env.FLY_ORG = 'my-org';
|
|
process.env.COMPUTE_DOMAIN = 'compute.example.com';
|
|
const c = loadConfig();
|
|
|
|
expect(c.fly.apiToken).toBe('fly-secret-token');
|
|
expect(c.fly.org).toBe('my-org');
|
|
expect(c.fly.domain).toBe('compute.example.com');
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: server
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.server', () => {
|
|
it('uses defaults when no env vars are set', () => {
|
|
unsetEnvKeys(
|
|
'MAX_JSON_BODY_SIZE',
|
|
'MAX_URLENCODED_BODY_SIZE',
|
|
'MAX_FILE_SIZE',
|
|
'MAX_FILES_PER_FIELD',
|
|
'LOGS_DIR'
|
|
);
|
|
const c = loadConfig();
|
|
|
|
expect(c.server.maxJsonBodySize).toBe('100mb');
|
|
expect(c.server.maxUrlencodedBodySize).toBe('10mb');
|
|
expect(c.server.maxFileSize).toBeUndefined();
|
|
expect(c.server.maxFilesPerField).toBe(10);
|
|
expect(typeof c.server.logsDir).toBe('string');
|
|
expect(c.server.logsDir.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('overrides body size limits', () => {
|
|
process.env.MAX_JSON_BODY_SIZE = '50mb';
|
|
process.env.MAX_URLENCODED_BODY_SIZE = '5mb';
|
|
const c = loadConfig();
|
|
|
|
expect(c.server.maxJsonBodySize).toBe('50mb');
|
|
expect(c.server.maxUrlencodedBodySize).toBe('5mb');
|
|
});
|
|
|
|
it('parses MAX_FILE_SIZE as number when set', () => {
|
|
process.env.MAX_FILE_SIZE = '52428800'; // 50 MiB
|
|
const c = loadConfig();
|
|
|
|
expect(c.server.maxFileSize).toBe(52428800);
|
|
expect(typeof c.server.maxFileSize).toBe('number');
|
|
});
|
|
|
|
it('MAX_FILE_SIZE is undefined when not set (not 0 or NaN)', () => {
|
|
unsetEnvKeys('MAX_FILE_SIZE');
|
|
const c = loadConfig();
|
|
|
|
expect(c.server.maxFileSize).toBeUndefined();
|
|
expect(c.server.maxFileSize).not.toBe(0);
|
|
expect(c.server.maxFileSize).not.toBeNaN();
|
|
});
|
|
|
|
it('parses MAX_FILES_PER_FIELD as number', () => {
|
|
process.env.MAX_FILES_PER_FIELD = '25';
|
|
expect(loadConfig().server.maxFilesPerField).toBe(25);
|
|
});
|
|
|
|
it('reads LOGS_DIR when set', () => {
|
|
process.env.LOGS_DIR = '/var/log/insforge';
|
|
expect(loadConfig().server.logsDir).toBe('/var/log/insforge');
|
|
});
|
|
|
|
it('defaults trustProxy to 2 hops', () => {
|
|
unsetEnvKeys('TRUST_PROXY');
|
|
expect(loadConfig().server.trustProxy).toBe(2);
|
|
});
|
|
|
|
it('reads trustProxy as boolean true', () => {
|
|
process.env.TRUST_PROXY = 'true';
|
|
expect(loadConfig().server.trustProxy).toBe(true);
|
|
});
|
|
|
|
it('reads trustProxy as number', () => {
|
|
process.env.TRUST_PROXY = '3';
|
|
expect(loadConfig().server.trustProxy).toBe(3);
|
|
});
|
|
|
|
it('reads trustProxy as Express subnet string', () => {
|
|
process.env.TRUST_PROXY = 'loopback, 10.0.0.0/8';
|
|
expect(loadConfig().server.trustProxy).toBe('loopback, 10.0.0.0/8');
|
|
});
|
|
|
|
it('robustly falls back to defaults for 0 or negative limits', () => {
|
|
process.env.MAX_FILES_PER_FIELD = '0';
|
|
process.env.MAX_FILE_SIZE = '0';
|
|
const c = loadConfig();
|
|
expect(c.server.maxFilesPerField).toBe(10);
|
|
expect(c.server.maxFileSize).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: database
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.database', () => {
|
|
it('uses defaults when no env vars are set', () => {
|
|
unsetEnvKeys(
|
|
'POSTGRES_HOST',
|
|
'POSTGRES_PORT',
|
|
'POSTGRES_DB',
|
|
'POSTGRES_USER',
|
|
'POSTGRES_PASSWORD',
|
|
'DATABASE_DIR',
|
|
'POSTGREST_BASE_URL'
|
|
);
|
|
const c = loadConfig();
|
|
|
|
expect(c.database.host).toBe('localhost');
|
|
expect(c.database.port).toBe(5432);
|
|
expect(c.database.name).toBe('insforge');
|
|
expect(c.database.user).toBe('postgres');
|
|
expect(c.database.password).toBe('postgres');
|
|
expect(c.database.postgrestBaseUrl).toBe('http://localhost:5430');
|
|
expect(typeof c.database.dir).toBe('string');
|
|
});
|
|
|
|
it('overrides all database fields', () => {
|
|
process.env.POSTGRES_HOST = 'db.internal';
|
|
process.env.POSTGRES_PORT = '5433';
|
|
process.env.POSTGRES_DB = 'myapp';
|
|
process.env.POSTGRES_USER = 'dbuser';
|
|
process.env.POSTGRES_PASSWORD = 'securepass';
|
|
process.env.POSTGREST_BASE_URL = 'http://postgrest:3000';
|
|
const c = loadConfig();
|
|
|
|
expect(c.database.host).toBe('db.internal');
|
|
expect(c.database.port).toBe(5433);
|
|
expect(c.database.name).toBe('myapp');
|
|
expect(c.database.user).toBe('dbuser');
|
|
expect(c.database.password).toBe('securepass');
|
|
expect(c.database.postgrestBaseUrl).toBe('http://postgrest:3000');
|
|
});
|
|
|
|
it('parses POSTGRES_PORT as integer', () => {
|
|
process.env.POSTGRES_PORT = '5435';
|
|
expect(loadConfig().database.port).toBe(5435);
|
|
expect(typeof loadConfig().database.port).toBe('number');
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: auth
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.auth', () => {
|
|
it('uses defaults when no env vars are set', () => {
|
|
unsetEnvKeys(
|
|
'ROOT_ADMIN_USERNAME',
|
|
'ROOT_ADMIN_PASSWORD',
|
|
'ADMIN_EMAIL',
|
|
'ADMIN_PASSWORD',
|
|
'ACCESS_API_KEY'
|
|
);
|
|
const c = loadConfig();
|
|
|
|
expect(c.auth.rootAdminUsername).toBe('');
|
|
expect(c.auth.rootAdminPassword).toBe('');
|
|
expect(c.auth.accessApiKey).toBeUndefined();
|
|
});
|
|
|
|
it('overrides root admin credentials', () => {
|
|
process.env.ROOT_ADMIN_USERNAME = 'root-admin';
|
|
process.env.ROOT_ADMIN_PASSWORD = 'ultrasecure!99';
|
|
const c = loadConfig();
|
|
|
|
expect(c.auth.rootAdminUsername).toBe('root-admin');
|
|
expect(c.auth.rootAdminPassword).toBe('ultrasecure!99');
|
|
});
|
|
|
|
it('keeps ADMIN_EMAIL and ADMIN_PASSWORD as legacy fallbacks', () => {
|
|
unsetEnvKeys('ROOT_ADMIN_USERNAME', 'ROOT_ADMIN_PASSWORD');
|
|
process.env.ADMIN_EMAIL = 'superadmin@company.com';
|
|
process.env.ADMIN_PASSWORD = 'ultrasecure!99';
|
|
const c = loadConfig();
|
|
|
|
expect(c.auth.rootAdminUsername).toBe('superadmin@company.com');
|
|
expect(c.auth.rootAdminPassword).toBe('ultrasecure!99');
|
|
});
|
|
|
|
it('accessApiKey is set when ACCESS_API_KEY env var is present', () => {
|
|
process.env.ACCESS_API_KEY = 'ik_abc123def456';
|
|
expect(loadConfig().auth.accessApiKey).toBe('ik_abc123def456');
|
|
});
|
|
|
|
it('accessApiKey is undefined when ACCESS_API_KEY is not set', () => {
|
|
unsetEnvKeys('ACCESS_API_KEY');
|
|
expect(loadConfig().auth.accessApiKey).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: storage
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.storage', () => {
|
|
it('uses defaults when no env vars are set', () => {
|
|
unsetEnvKeys(
|
|
'AWS_S3_BUCKET',
|
|
'APP_KEY',
|
|
'PARENT_APP_KEY',
|
|
'AWS_REGION',
|
|
'STORAGE_DIR',
|
|
'S3_ACCESS_KEY_ID',
|
|
'S3_SECRET_ACCESS_KEY',
|
|
'AWS_ACCESS_KEY_ID',
|
|
'AWS_SECRET_ACCESS_KEY',
|
|
'S3_ENDPOINT_URL',
|
|
'AWS_CONFIG_BUCKET',
|
|
'AWS_CONFIG_REGION'
|
|
);
|
|
const c = loadConfig();
|
|
|
|
expect(c.storage.s3Bucket).toBeUndefined();
|
|
expect(c.storage.appKey).toBe('local');
|
|
expect(c.storage.parentAppKey).toBeUndefined();
|
|
expect(c.storage.awsRegion).toBe('us-east-2');
|
|
expect(c.storage.s3AccessKeyId).toBeUndefined();
|
|
expect(c.storage.s3SecretAccessKey).toBeUndefined();
|
|
expect(c.storage.awsAccessKeyId).toBeUndefined();
|
|
expect(c.storage.awsSecretAccessKey).toBeUndefined();
|
|
expect(c.storage.s3EndpointUrl).toBeUndefined();
|
|
expect(c.storage.awsConfigBucket).toBe('insforge-config');
|
|
expect(c.storage.awsConfigRegion).toBe('us-east-2');
|
|
expect(typeof c.storage.storageDir).toBe('string');
|
|
});
|
|
|
|
// ── storage.appKey regression ─────────────────────────────────────────────
|
|
it('REGRESSION: storage.appKey defaults to "local" (not "default-app-key")', () => {
|
|
unsetEnvKeys('APP_KEY');
|
|
expect(loadConfig().storage.appKey).toBe('local');
|
|
expect(loadConfig().storage.appKey).not.toBe('default-app-key');
|
|
});
|
|
|
|
it('overrides S3 bucket and region', () => {
|
|
process.env.AWS_S3_BUCKET = 'my-production-bucket';
|
|
process.env.AWS_REGION = 'eu-west-1';
|
|
const c = loadConfig();
|
|
|
|
expect(c.storage.s3Bucket).toBe('my-production-bucket');
|
|
expect(c.storage.awsRegion).toBe('eu-west-1');
|
|
});
|
|
|
|
it('sets s3Bucket to undefined when AWS_S3_BUCKET is not set', () => {
|
|
unsetEnvKeys('AWS_S3_BUCKET');
|
|
expect(loadConfig().storage.s3Bucket).toBeUndefined();
|
|
});
|
|
|
|
it('reads S3-specific credentials when set', () => {
|
|
process.env.S3_ACCESS_KEY_ID = 's3-key-id';
|
|
process.env.S3_SECRET_ACCESS_KEY = 's3-secret';
|
|
const c = loadConfig();
|
|
|
|
expect(c.storage.s3AccessKeyId).toBe('s3-key-id');
|
|
expect(c.storage.s3SecretAccessKey).toBe('s3-secret');
|
|
});
|
|
|
|
it('reads AWS credentials when set', () => {
|
|
process.env.AWS_ACCESS_KEY_ID = 'test-aws-access-key-id';
|
|
process.env.AWS_SECRET_ACCESS_KEY = 'test-aws-secret-access-key';
|
|
const c = loadConfig();
|
|
|
|
expect(c.storage.awsAccessKeyId).toBe('test-aws-access-key-id');
|
|
expect(c.storage.awsSecretAccessKey).toBe('test-aws-secret-access-key');
|
|
});
|
|
|
|
it('reads S3_ENDPOINT_URL when set', () => {
|
|
process.env.S3_ENDPOINT_URL = 'https://s3.wasabisys.com';
|
|
expect(loadConfig().storage.s3EndpointUrl).toBe('https://s3.wasabisys.com');
|
|
});
|
|
|
|
it('S3_ENDPOINT_URL is undefined when not set', () => {
|
|
unsetEnvKeys('S3_ENDPOINT_URL');
|
|
expect(loadConfig().storage.s3EndpointUrl).toBeUndefined();
|
|
});
|
|
|
|
it('reads PARENT_APP_KEY and trims whitespace', () => {
|
|
process.env.PARENT_APP_KEY = ' parent-key ';
|
|
expect(loadConfig().storage.parentAppKey).toBe('parent-key');
|
|
});
|
|
|
|
it('PARENT_APP_KEY is undefined when set to only whitespace', () => {
|
|
process.env.PARENT_APP_KEY = ' ';
|
|
expect(loadConfig().storage.parentAppKey).toBeUndefined();
|
|
});
|
|
|
|
it('PARENT_APP_KEY is undefined when not set', () => {
|
|
unsetEnvKeys('PARENT_APP_KEY');
|
|
expect(loadConfig().storage.parentAppKey).toBeUndefined();
|
|
});
|
|
|
|
it('reads STORAGE_DIR from env', () => {
|
|
process.env.STORAGE_DIR = '/data/insforge-storage';
|
|
expect(loadConfig().storage.storageDir).toBe('/data/insforge-storage');
|
|
});
|
|
|
|
it('reads config bucket overrides', () => {
|
|
process.env.AWS_CONFIG_BUCKET = 'my-config-bucket';
|
|
process.env.AWS_CONFIG_REGION = 'ap-southeast-1';
|
|
const c = loadConfig();
|
|
|
|
expect(c.storage.awsConfigBucket).toBe('my-config-bucket');
|
|
expect(c.storage.awsConfigRegion).toBe('ap-southeast-1');
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: functions
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.functions', () => {
|
|
it('uses default Deno runtime URL', () => {
|
|
unsetEnvKeys('DENO_RUNTIME_URL');
|
|
expect(loadConfig().functions.denoRuntimeUrl).toBe('http://localhost:7133');
|
|
});
|
|
|
|
it('overrides Deno runtime URL', () => {
|
|
process.env.DENO_RUNTIME_URL = 'http://deno-runtime:7133';
|
|
expect(loadConfig().functions.denoRuntimeUrl).toBe('http://deno-runtime:7133');
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: deployments
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.deployments', () => {
|
|
const ONE_HUNDRED_MIB = 100 * 1024 * 1024;
|
|
|
|
it('optional Vercel fields are undefined when not set', () => {
|
|
unsetEnvKeys('VERCEL_TOKEN', 'VERCEL_TEAM_ID', 'VERCEL_PROJECT_ID');
|
|
const c = loadConfig();
|
|
|
|
expect(c.deployments.vercelToken).toBeUndefined();
|
|
expect(c.deployments.vercelTeamId).toBeUndefined();
|
|
expect(c.deployments.vercelProjectId).toBeUndefined();
|
|
});
|
|
|
|
it('reads Vercel credentials from env', () => {
|
|
process.env.VERCEL_TOKEN = 'vcel_token_abc';
|
|
process.env.VERCEL_TEAM_ID = 'team_xyz';
|
|
process.env.VERCEL_PROJECT_ID = 'prj_123';
|
|
const c = loadConfig();
|
|
|
|
expect(c.deployments.vercelToken).toBe('vcel_token_abc');
|
|
expect(c.deployments.vercelTeamId).toBe('team_xyz');
|
|
expect(c.deployments.vercelProjectId).toBe('prj_123');
|
|
});
|
|
|
|
it('deployment size limits use 100 MiB defaults', () => {
|
|
unsetEnvKeys('MAX_DEPLOYMENT_FILES', 'MAX_DEPLOYMENT_TOTAL_BYTES', 'MAX_DEPLOYMENT_FILE_BYTES');
|
|
const c = loadConfig();
|
|
|
|
expect(c.deployments.maxDeploymentFiles).toBe(5000);
|
|
expect(c.deployments.maxDeploymentTotalBytes).toBe(ONE_HUNDRED_MIB);
|
|
expect(c.deployments.maxDeploymentFileBytes).toBe(ONE_HUNDRED_MIB);
|
|
});
|
|
|
|
it('deployment size limits are parsed as integers from env', () => {
|
|
process.env.MAX_DEPLOYMENT_FILES = '1000';
|
|
process.env.MAX_DEPLOYMENT_TOTAL_BYTES = '52428800';
|
|
process.env.MAX_DEPLOYMENT_FILE_BYTES = '10485760';
|
|
const c = loadConfig();
|
|
|
|
expect(c.deployments.maxDeploymentFiles).toBe(1000);
|
|
expect(c.deployments.maxDeploymentTotalBytes).toBe(52428800);
|
|
expect(c.deployments.maxDeploymentFileBytes).toBe(10485760);
|
|
expect(typeof c.deployments.maxDeploymentFiles).toBe('number');
|
|
expect(typeof c.deployments.maxDeploymentTotalBytes).toBe('number');
|
|
expect(typeof c.deployments.maxDeploymentFileBytes).toBe('number');
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Section: ai
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('config.ai', () => {
|
|
it('openrouterApiKey is undefined when not set', () => {
|
|
unsetEnvKeys('OPENROUTER_API_KEY');
|
|
expect(loadConfig().ai.openrouterApiKey).toBeUndefined();
|
|
});
|
|
|
|
it('reads OPENROUTER_API_KEY when set', () => {
|
|
process.env.OPENROUTER_API_KEY = 'sk-or-test-key-12345';
|
|
expect(loadConfig().ai.openrouterApiKey).toBe('sk-or-test-key-12345');
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// loadConfig() structural integrity
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
describe('loadConfig() structural integrity', () => {
|
|
it('returns a complete object with all 11 top-level sections', () => {
|
|
const c = loadConfig();
|
|
|
|
expect(c).toHaveProperty('app');
|
|
expect(c).toHaveProperty('cloud');
|
|
expect(c).toHaveProperty('denoSubhosting');
|
|
expect(c).toHaveProperty('fly');
|
|
expect(c).toHaveProperty('server');
|
|
expect(c).toHaveProperty('database');
|
|
expect(c).toHaveProperty('auth');
|
|
expect(c).toHaveProperty('storage');
|
|
expect(c).toHaveProperty('functions');
|
|
expect(c).toHaveProperty('deployments');
|
|
expect(c).toHaveProperty('ai');
|
|
});
|
|
|
|
it('returns a new object each time (not a cached singleton)', () => {
|
|
process.env.PORT = '8001';
|
|
const c1 = loadConfig();
|
|
process.env.PORT = '8002';
|
|
const c2 = loadConfig();
|
|
|
|
expect(c1.app.port).toBe(8001);
|
|
expect(c2.app.port).toBe(8002);
|
|
expect(c1).not.toBe(c2);
|
|
});
|
|
|
|
it('all numeric fields are actual numbers (not strings)', () => {
|
|
const c = loadConfig();
|
|
|
|
expect(typeof c.app.port).toBe('number');
|
|
expect(typeof c.database.port).toBe('number');
|
|
expect(typeof c.server.maxFilesPerField).toBe('number');
|
|
expect(typeof c.deployments.maxDeploymentFiles).toBe('number');
|
|
expect(typeof c.deployments.maxDeploymentTotalBytes).toBe('number');
|
|
expect(typeof c.deployments.maxDeploymentFileBytes).toBe('number');
|
|
});
|
|
|
|
it('no numeric field is NaN', () => {
|
|
const c = loadConfig();
|
|
|
|
expect(c.app.port).not.toBeNaN();
|
|
expect(c.database.port).not.toBeNaN();
|
|
expect(c.server.maxFilesPerField).not.toBeNaN();
|
|
expect(c.deployments.maxDeploymentFiles).not.toBeNaN();
|
|
expect(c.deployments.maxDeploymentTotalBytes).not.toBeNaN();
|
|
expect(c.deployments.maxDeploymentFileBytes).not.toBeNaN();
|
|
});
|
|
});
|