mirror of https://github.com/OpenVidu/openvidu.git
231 lines
7.9 KiB
TypeScript
231 lines
7.9 KiB
TypeScript
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { loadConfigFile } from '../../src/config/config-file.js';
|
|
import { LEGACY_VARIABLES, parseConfig, PRE_SCHEMA_VARIABLES, variablesSchema } from '../../src/config/variables.js';
|
|
|
|
const required = { ADMIN_USERNAME: 'admin', ADMIN_PASSWORD: 'secret' };
|
|
|
|
function valid(env: NodeJS.ProcessEnv) {
|
|
const result = parseConfig(env);
|
|
|
|
if (result.kind !== 'config') {
|
|
throw new Error(`expected a configuration, got ${JSON.stringify(result)}`);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function invalid(env: NodeJS.ProcessEnv): string[] {
|
|
const result = parseConfig(env);
|
|
|
|
if (result.kind !== 'invalid') {
|
|
throw new Error(`expected an invalid configuration, got ${result.kind}`);
|
|
}
|
|
|
|
return result.errors;
|
|
}
|
|
|
|
describe('configuration variables', () => {
|
|
it('applies the defaults', () => {
|
|
const { config, warnings } = valid({ ...required });
|
|
expect(warnings).toEqual([]);
|
|
expect(config).toEqual({
|
|
port: 5000,
|
|
logLevel: 'info',
|
|
admin: { username: 'admin', password: 'secret' },
|
|
database: { name: 'openvidu' },
|
|
session: { refreshTokenTtlSeconds: 604_800, cookieSecure: 'auto' },
|
|
http: { trustProxy: ['loopback', 'linklocal', 'uniquelocal'] },
|
|
lists: { totalMaxRangeMs: 7 * 86_400_000 },
|
|
analytics: { cacheTtlSeconds: 60 }
|
|
});
|
|
});
|
|
|
|
it('reads every variable', () => {
|
|
const { config } = valid({
|
|
...required,
|
|
SERVER_PORT: '6000',
|
|
LOG_LEVEL: 'DEBUG',
|
|
DATABASE_URL: 'mongodb://db:27017',
|
|
DATABASE_NAME: 'analytics',
|
|
DATABASE_TLS_CA_FILE: '/certs/ca.pem',
|
|
MONGO_NODES: 'm1,m2',
|
|
MONGO_PORT: '27017',
|
|
MONGO_ADMIN_USERNAME: 'root',
|
|
MONGO_ADMIN_PASSWORD: 'pw',
|
|
MONGO_REPLICA_SET_NAME: 'rs0',
|
|
DASHBOARD_JWT_SECRET: 'x'.repeat(48),
|
|
DASHBOARD_REFRESH_TOKEN_TTL: '43200',
|
|
DASHBOARD_COOKIE_SECURE: 'false'
|
|
});
|
|
expect(config.port).toBe(6000);
|
|
expect(config.logLevel).toBe('debug');
|
|
expect(config.database).toEqual({
|
|
name: 'analytics',
|
|
url: 'mongodb://db:27017',
|
|
tlsCaFile: '/certs/ca.pem',
|
|
nodes: 'm1,m2',
|
|
port: '27017',
|
|
username: 'root',
|
|
password: 'pw',
|
|
replicaSet: 'rs0'
|
|
});
|
|
expect(config.session).toEqual({
|
|
jwtSecret: 'x'.repeat(48),
|
|
refreshTokenTtlSeconds: 43_200,
|
|
cookieSecure: 'false'
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
['ADMIN_USERNAME unset', { ADMIN_PASSWORD: 'secret' }, 'ADMIN_USERNAME is required'],
|
|
['ADMIN_PASSWORD empty', { ADMIN_USERNAME: 'admin', ADMIN_PASSWORD: ' ' }, 'ADMIN_PASSWORD is required'],
|
|
['port not a number', { ...required, SERVER_PORT: 'http' }, 'SERVER_PORT ("http") must be a TCP port'],
|
|
['port out of range', { ...required, SERVER_PORT: '70000' }, 'SERVER_PORT ("70000") must be a TCP port'],
|
|
[
|
|
'TTL not a number',
|
|
{ ...required, DASHBOARD_REFRESH_TOKEN_TTL: 'soon' },
|
|
'DASHBOARD_REFRESH_TOKEN_TTL ("soon") must be a positive integer of seconds'
|
|
],
|
|
[
|
|
'TTL zero',
|
|
{ ...required, DASHBOARD_REFRESH_TOKEN_TTL: '0' },
|
|
'DASHBOARD_REFRESH_TOKEN_TTL ("0") must be a positive integer of seconds'
|
|
],
|
|
[
|
|
'cookie mode',
|
|
{ ...required, DASHBOARD_COOKIE_SECURE: 'yes' },
|
|
'DASHBOARD_COOKIE_SECURE ("yes") must be auto, true or false'
|
|
]
|
|
])('rejects %s', (_name, env, message) => {
|
|
expect(invalid(env)).toContain(message);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
'secret under 32 bytes',
|
|
{ ...required, DASHBOARD_JWT_SECRET: 'short' },
|
|
'DASHBOARD_JWT_SECRET must be at least 32 bytes'
|
|
],
|
|
[
|
|
'trust proxy true',
|
|
{ ...required, DASHBOARD_TRUST_PROXY: 'true' },
|
|
'DASHBOARD_TRUST_PROXY ("true") must not be true: any client could then choose its own address'
|
|
],
|
|
[
|
|
'trust proxy entry',
|
|
{ ...required, DASHBOARD_TRUST_PROXY: 'loopback, proxy.local' },
|
|
'DASHBOARD_TRUST_PROXY ("loopback, proxy.local") must be false, a hop count, or a list of addresses, CIDRs, loopback, linklocal and uniquelocal'
|
|
],
|
|
[
|
|
'trust proxy CIDR',
|
|
{ ...required, DASHBOARD_TRUST_PROXY: '10.0.0.0/33' },
|
|
'DASHBOARD_TRUST_PROXY ("10.0.0.0/33") must be false, a hop count, or a list of addresses, CIDRs, loopback, linklocal and uniquelocal'
|
|
],
|
|
[
|
|
'base path',
|
|
{ ...required, DASHBOARD_BASE_PATH: 'dashboard' },
|
|
'DASHBOARD_BASE_PATH ("dashboard") must be a path starting with /'
|
|
],
|
|
[
|
|
'total range',
|
|
{ ...required, DASHBOARD_LIST_TOTAL_MAX_RANGE: '1w' },
|
|
'DASHBOARD_LIST_TOTAL_MAX_RANGE ("1w") must be a duration such as 30m, 24h or 7d'
|
|
],
|
|
[
|
|
'analytics cache TTL',
|
|
{ ...required, DASHBOARD_ANALYTICS_CACHE_TTL: '1.5' },
|
|
'DASHBOARD_ANALYTICS_CACHE_TTL ("1.5") must be a non-negative integer of seconds'
|
|
]
|
|
])('rejects %s', (_name, env, message) => {
|
|
expect(invalid(env)).toContain(message);
|
|
});
|
|
|
|
it('reads the proxy, base path and total range settings', () => {
|
|
const read = (env: NodeJS.ProcessEnv) => valid({ ...required, ...env }).config.http;
|
|
|
|
expect(read({ DASHBOARD_TRUST_PROXY: 'false' }).trustProxy).toBe(false);
|
|
expect(read({ DASHBOARD_TRUST_PROXY: '2' }).trustProxy).toBe(2);
|
|
expect(read({ DASHBOARD_TRUST_PROXY: 'loopback, 10.0.0.0/8, fd00::/8, 192.168.1.10' }).trustProxy).toEqual([
|
|
'loopback',
|
|
'10.0.0.0/8',
|
|
'fd00::/8',
|
|
'192.168.1.10'
|
|
]);
|
|
expect(read({ DASHBOARD_BASE_PATH: '/admin' }).basePath).toBe('/admin/');
|
|
expect(valid({ ...required, DASHBOARD_LIST_TOTAL_MAX_RANGE: '24h' }).config.lists.totalMaxRangeMs).toBe(
|
|
86_400_000
|
|
);
|
|
expect(valid({ ...required, DASHBOARD_JWT_SECRET: 'x'.repeat(32) }).config.session.jwtSecret).toHaveLength(32);
|
|
expect(valid(required).config.analytics.cacheTtlSeconds).toBe(60);
|
|
expect(valid({ ...required, DASHBOARD_ANALYTICS_CACHE_TTL: '0' }).config.analytics.cacheTtlSeconds).toBe(0);
|
|
});
|
|
|
|
it('reports every invalid variable and never echoes secrets', () => {
|
|
const errors = invalid({
|
|
ADMIN_USERNAME: 'admin',
|
|
ADMIN_PASSWORD: '',
|
|
SERVER_PORT: 'x',
|
|
DASHBOARD_JWT_SECRET: ''
|
|
});
|
|
expect(errors).toHaveLength(2);
|
|
expect(errors.join('\n')).not.toContain('""');
|
|
});
|
|
|
|
it('stops before validating anything when MongoDB is disabled', () => {
|
|
expect(parseConfig({ MONGO_ENABLED: 'false' })).toEqual({ kind: 'mongo-disabled' });
|
|
expect(parseConfig({ MONGO_ENABLED: 'no' })).toEqual({ kind: 'mongo-disabled' });
|
|
expect(parseConfig({ ...required, MONGO_ENABLED: 'TRUE' }).kind).toBe('config');
|
|
});
|
|
|
|
it('falls back to info on an unknown log level', () => {
|
|
const { config, warnings } = valid({ ...required, LOG_LEVEL: 'loud' });
|
|
expect(config.logLevel).toBe('info');
|
|
expect(warnings).toEqual(['LOG_LEVEL "loud" is not a log level; using info']);
|
|
});
|
|
|
|
it('names every variable after the legacy list or the DASHBOARD_ and DATABASE_ prefixes', () => {
|
|
const names = [...Object.keys(variablesSchema.shape), ...PRE_SCHEMA_VARIABLES];
|
|
const legacy: readonly string[] = LEGACY_VARIABLES;
|
|
|
|
for (const name of names) {
|
|
expect(legacy.includes(name) || name.startsWith('DASHBOARD_') || name.startsWith('DATABASE_'), name).toBe(
|
|
true
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('configuration file', () => {
|
|
function file(content: string): string {
|
|
const path = join(mkdtempSync(join(tmpdir(), 'dashboard-config-')), 'dashboard.env');
|
|
writeFileSync(path, content);
|
|
|
|
return path;
|
|
}
|
|
|
|
it('loads values the environment does not set', () => {
|
|
const env: NodeJS.ProcessEnv = { DASHBOARD_CONFIG_PATH: file('ADMIN_PASSWORD=secret\nSERVER_PORT=7000\n') };
|
|
expect(loadConfigFile(env)).toMatchObject({ kind: 'loaded', applied: ['ADMIN_PASSWORD', 'SERVER_PORT'] });
|
|
expect(env.ADMIN_PASSWORD).toBe('secret');
|
|
});
|
|
|
|
it('keeps the environment over the file', () => {
|
|
const env: NodeJS.ProcessEnv = { DASHBOARD_CONFIG_PATH: file('SERVER_PORT=7000\n'), SERVER_PORT: '5000' };
|
|
loadConfigFile(env);
|
|
expect(env.SERVER_PORT).toBe('5000');
|
|
});
|
|
|
|
it('reports a missing file', () => {
|
|
expect(loadConfigFile({ DASHBOARD_CONFIG_PATH: '/nonexistent/dashboard.env' })).toEqual({
|
|
kind: 'missing',
|
|
path: '/nonexistent/dashboard.env'
|
|
});
|
|
expect(loadConfigFile({})).toEqual({ kind: 'none' });
|
|
});
|
|
});
|