multiconf
Version:
Work with JSON configs
599 lines (495 loc) • 22.2 kB
JavaScript
// Import.
const assert = require('node:assert');
const test = require('node:test');
const Conf = require('../index');
// Constants.
const TEST_CONF_FILES_DIRECTORY = './tests/data';
const TEST_CONF_FILES_DIRECTORY_2 = './tests/data2';
const TEST_ENVIRONMENT_CONFIG_PREFIX = 'CONFIG_';
// Init conf.
const conf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
// Main tests.
test.describe('Check config objects.', () => {
// Test if only user-defined config exist.
test.it('Should contain key "1".', () => {
assert.strictEqual('1', conf.test1.key);
});
// Check if default and user-defined configs exists.
test.it('Should contain key "2".', () => {
assert.strictEqual('2', conf.test2.key);
});
// Check if only default config exist.
test.it('Should contain key "3-default".', () => {
assert.strictEqual('3-default', conf.test3.key);
});
// Check environment config.
test.it('Should contain key 44.', () => {
assert.strictEqual(44, conf.test4.key);
});
});
test.describe('Check default config objects.', () => {
const defaultConf = Conf.getDefault(TEST_CONF_FILES_DIRECTORY);
// Test if only user-defined config exist.
test.it('Should not contain test1.', () => {
assert.strictEqual(undefined, defaultConf.test1);
});
// Check if default and user-defined configs exists.
test.it('Should contain default key "2".', () => {
assert.strictEqual('2-default', defaultConf.test2.key);
});
// Check if only default config exist.
test.it('Should contain key "3-default".', () => {
assert.strictEqual('3-default', defaultConf.test3.key);
});
// Check environment config.
test.it('Should not contain test4.', () => {
assert.strictEqual(undefined, defaultConf.test4);
});
});
// Environment variable config tests
test.describe('Check environment variable configurations.', () => {
// Set environment variables for testing
// Note: CONFIG_TEST4 is already set in the npm script
// This test uses the already configured ENV var
test.it('Should load environment variable with numeric value', () => {
assert.strictEqual(44, conf.test4.key);
});
// Test case-insensitivity of config keys
test.it('Should provide case-insensitive access to environment variables', () => {
assert.strictEqual(44, conf.TEST4.key);
});
// Test with complex objects from environment variables
test.describe('Complex object handling from environment', () => {
// Create test conf with complex object env var
process.env.CONFIG_COMPLEX = '{"nested": {"value": 123}, "array": [1, 2, 3]}';
const complexConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
test.it('Should parse nested objects correctly', () => {
assert.strictEqual(123, complexConf.complex.nested.value);
});
test.it('Should parse arrays correctly', () => {
assert.deepStrictEqual([1, 2, 3], complexConf.complex.array);
});
test.it('Should maintain original case of the env var after prefix removal', () => {
assert.strictEqual(true, 'COMPLEX' in complexConf);
});
// Clean up env var
delete process.env.CONFIG_COMPLEX;
});
// Test prefix detection
test.describe('Environment variable prefix behavior', () => {
// Set environment variables with different prefixes
process.env.DIFFERENT_PREFIX_TEST = '{"value": "wrong"}';
process.env.CONFIG_PREFIX_TEST = '{"value": "correct"}';
const prefixConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
test.it('Should only include environment variables with correct prefix', () => {
assert.strictEqual('correct', prefixConf.PREFIX_TEST.value);
assert.strictEqual(undefined, prefixConf.DIFFERENT_PREFIX_TEST);
});
// Clean up env vars
delete process.env.DIFFERENT_PREFIX_TEST;
delete process.env.CONFIG_PREFIX_TEST;
});
// Test various data types via environment variables
test.describe('Data type handling from environment variables', () => {
process.env.CONFIG_BOOLEAN = 'true';
process.env.CONFIG_NUMBER = '42';
process.env.CONFIG_STRING = '"test string"';
process.env.CONFIG_NULL = 'null';
const typesConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
test.it('Should parse boolean values correctly', () => {
assert.strictEqual(true, typesConf.boolean);
});
test.it('Should parse number values correctly', () => {
assert.strictEqual(42, typesConf.number);
});
test.it('Should parse string values correctly', () => {
assert.strictEqual('test string', typesConf.string);
});
test.it('Should parse null values correctly', () => {
assert.strictEqual(null, typesConf.null);
});
// Clean up env vars
delete process.env.CONFIG_BOOLEAN;
delete process.env.CONFIG_NUMBER;
delete process.env.CONFIG_STRING;
delete process.env.CONFIG_NULL;
});
// Test error handling for invalid JSON
test.describe('Error handling for invalid environment variables', () => {
test.it('Should throw an error for invalid JSON in environment variable', () => {
process.env.CONFIG_INVALID = '{not valid json}';
assert.throws(
() => Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX),
{
name: 'Error',
message: /Can\'t parse env config "CONFIG_INVALID"\./,
},
);
// Clean up
delete process.env.CONFIG_INVALID;
});
});
});
test.describe('Check multi-directory config objects.', () => {
const multiConf = Conf.get([TEST_CONF_FILES_DIRECTORY, TEST_CONF_FILES_DIRECTORY_2], TEST_ENVIRONMENT_CONFIG_PREFIX);
const multiConfFromDelimitedString = Conf.get(`${TEST_CONF_FILES_DIRECTORY};${TEST_CONF_FILES_DIRECTORY_2}`, TEST_ENVIRONMENT_CONFIG_PREFIX);
const multiConfFromMixedArray = Conf.get([
`${TEST_CONF_FILES_DIRECTORY};${TEST_CONF_FILES_DIRECTORY_2}`,
TEST_CONF_FILES_DIRECTORY,
], TEST_ENVIRONMENT_CONFIG_PREFIX);
// Later directory overrides earlier.
test.it('Should contain overridden key "1-override".', () => {
assert.strictEqual('1-override', multiConf.test1.key);
});
// Semicolon-delimited string should scan both directories in order.
test.it('Should support semicolon-delimited config directories.', () => {
assert.strictEqual('1-override', multiConfFromDelimitedString.test1.key);
assert.strictEqual('5', multiConfFromDelimitedString.test5.key);
});
// Mixed array entries should flatten semicolon-delimited segments and preserve sequence.
test.it('Should support mixed arrays of plain and semicolon-delimited directories.', () => {
assert.strictEqual('1', multiConfFromMixedArray.test1.key);
assert.strictEqual('5', multiConfFromMixedArray.test5.key);
});
// Custom delimiter should be applied when normalizing directory strings.
test.it('Should support custom config directory delimiters.', () => {
try {
const multiConfFromCustomDelimiter = Conf
.setDirDelimiter('|')
.get(`${TEST_CONF_FILES_DIRECTORY}|${TEST_CONF_FILES_DIRECTORY_2}`, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual('1-override', multiConfFromCustomDelimiter.test1.key);
assert.strictEqual('5', multiConfFromCustomDelimiter.test5.key);
} finally {
Conf.setDirDelimiter(';');
}
});
// Key only in first directory is preserved.
test.it('Should contain key "2" from first directory.', () => {
assert.strictEqual('2', multiConf.test2.key);
});
// New key only in second directory is present.
test.it('Should contain key "5" from second directory.', () => {
assert.strictEqual('5', multiConf.test5.key);
});
// Deep merge: top-level key only in first directory's nested object is preserved.
test.it('Should deep-merge nested objects (a from dir1 preserved).', () => {
assert.strictEqual(1, multiConf.testnested.a);
});
// Deep merge: nested key only in first directory is preserved.
test.it('Should deep-merge nested objects (b.x from dir1 preserved).', () => {
assert.strictEqual(10, multiConf.testnested.b.x);
});
// Deep merge: nested key overridden by second directory.
test.it('Should deep-merge nested objects (b.y overridden by dir2).', () => {
assert.strictEqual(99, multiConf.testnested.b.y);
});
// Deep merge: new nested key from second directory.
test.it('Should deep-merge nested objects (b.z added by dir2).', () => {
assert.strictEqual(30, multiConf.testnested.b.z);
});
// Deep merge: new top-level key from second directory.
test.it('Should deep-merge nested objects (c added by dir2).', () => {
assert.strictEqual(3, multiConf.testnested.c);
});
});
test.describe('Check multi-directory default config objects.', () => {
const multiDefaultConf = Conf.getDefault([TEST_CONF_FILES_DIRECTORY, TEST_CONF_FILES_DIRECTORY_2]);
const multiDefaultConfFromDelimitedString = Conf.getDefault(`${TEST_CONF_FILES_DIRECTORY};${TEST_CONF_FILES_DIRECTORY_2}`);
const multiDefaultConfFromMixedArray = Conf.getDefault([
`${TEST_CONF_FILES_DIRECTORY};${TEST_CONF_FILES_DIRECTORY_2}`,
TEST_CONF_FILES_DIRECTORY,
]);
// User-defined files are not loaded.
test.it('Should not contain test1.', () => {
assert.strictEqual(undefined, multiDefaultConf.test1);
});
// Default from first directory is present.
test.it('Should contain default key "2" from first directory.', () => {
assert.strictEqual('2-default', multiDefaultConf.test2.key);
});
// Semicolon-delimited string should scan both directories in order.
test.it('Should support semicolon-delimited default config directories.', () => {
assert.strictEqual('3-override', multiDefaultConfFromDelimitedString.test3.key);
assert.strictEqual('default2', multiDefaultConfFromDelimitedString.testdefault2.key);
});
// Mixed array entries should flatten semicolon-delimited segments and preserve sequence.
test.it('Should support mixed arrays of default config directories.', () => {
assert.strictEqual('3-default', multiDefaultConfFromMixedArray.test3.key);
assert.strictEqual('default2', multiDefaultConfFromMixedArray.testdefault2.key);
});
// Default overridden by second directory.
test.it('Should contain overridden default key "3-override" from second directory.', () => {
assert.strictEqual('3-override', multiDefaultConf.test3.key);
});
// New default only in second directory.
test.it('Should contain default key "default2" from second directory.', () => {
assert.strictEqual('default2', multiDefaultConf.testdefault2.key);
});
});
test.describe('Check template rendering in config objects.', () => {
test.it('Should allow chaining custom setters.', () => {
process.env.CHAINED_ALLOWED_VAR = 'chained value';
process.env.CONFIG_CHAINED = `
{
"key": "<%= env.CHAINED_ALLOWED_VAR %>"
}
`;
try {
const chainedConf = Conf
.setTemplateRendering(true)
.setAllowedEnvVars(['CHAINED_ALLOWED_VAR'])
.setDirDelimiter('|')
.get(`${TEST_CONF_FILES_DIRECTORY}|${TEST_CONF_FILES_DIRECTORY_2}`, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(chainedConf.chained.key, 'chained value');
assert.strictEqual(chainedConf.test5.key, '5');
} finally {
Conf.setTemplateRendering(false);
Conf.setAllowedEnvVars(null);
Conf.setDirDelimiter(';');
delete process.env.CHAINED_ALLOWED_VAR;
delete process.env.CONFIG_CHAINED;
}
});
test.it('Should render environment variable in config value.', () => {
Conf.setTemplateRendering(true);
process.env.TEST_ENV_VAR1 = 'test value';
process.env.TEST_ENV_VAR2 = 'another test value';
process.env.CONFIG_TEMPLATING_1 = `
{
"key": "<%= env.TEST_ENV_VAR1 %>",
"nested": {
"innerKey": "<%= env.TEST_ENV_VAR2 %>"
}
}
`;
const templatingConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(templatingConf.templating_1.key, process.env.TEST_ENV_VAR1);
assert.strictEqual(templatingConf.templating_1.nested.innerKey, process.env.TEST_ENV_VAR2);
// Clean up environment variables
delete process.env.TEST_ENV_VAR1;
delete process.env.TEST_ENV_VAR2;
delete process.env.CONFIG_TEMPLATING_1;
});
test.it('Should render config values referencing other config values.', () => {
Conf.setTemplateRendering(true);
process.env.CONFIG_TEMPLATING_2 = `
{
"key1": "value1",
"key2": "<%= config.key1 %> and more"
}
`;
const templatingConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(templatingConf.templating_2.key1, 'value1');
assert.strictEqual(templatingConf.templating_2.key2, 'value1 and more');
// Clean up environment variable
delete process.env.CONFIG_TEMPLATING_2;
});
test.it('Should render to_base64 and from_base64 functions in config values.', () => {
Conf.setTemplateRendering(true);
process.env.CONFIG_TEMPLATING_3 = `
{
"encoded": "<%= to_base64('Hello, World!') %>",
"decoded": "<%= from_base64('SGVsbG8sIFdvcmxkIQ==') %>"
}
`;
const templatingConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(templatingConf.templating_3.encoded, Buffer.from('Hello, World!').toString('base64'));
assert.strictEqual(templatingConf.templating_3.decoded, 'Hello, World!');
// Clean up environment variable
delete process.env.CONFIG_TEMPLATING_3;
});
test.describe('Environment variable whitelisting', () => {
test.it('Should allow all environment variables when allowedEnvVars is null (default).', () => {
Conf.setTemplateRendering(true);
Conf.setAllowedEnvVars(null);
process.env.WHITELISTED_VAR = 'allowed';
process.env.UNRESTRICTED_VAR = 'also allowed';
process.env.CONFIG_WHITELIST_1 = `
{
"whitelisted": "<%= env.WHITELISTED_VAR %>",
"unrestricted": "<%= env.UNRESTRICTED_VAR %>"
}
`;
const whitelistConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(whitelistConf.whitelist_1.whitelisted, 'allowed');
assert.strictEqual(whitelistConf.whitelist_1.unrestricted, 'also allowed');
// Clean up
delete process.env.WHITELISTED_VAR;
delete process.env.UNRESTRICTED_VAR;
delete process.env.CONFIG_WHITELIST_1;
});
test.it('Should restrict environment variables to whitelist when allowedEnvVars is set.', () => {
Conf.setTemplateRendering(true);
Conf.setAllowedEnvVars(['ALLOWED_VAR_1', 'ALLOWED_VAR_2']);
process.env.ALLOWED_VAR_1 = 'whitelisted1';
process.env.ALLOWED_VAR_2 = 'whitelisted2';
process.env.BLOCKED_VAR = 'this should not be accessible';
process.env.CONFIG_WHITELIST_2 = `
{
"allowed1": "<%= env.ALLOWED_VAR_1 %>",
"allowed2": "<%= env.ALLOWED_VAR_2 %>",
"blocked": "<%= env.BLOCKED_VAR || 'undefined' %>"
}
`;
const whitelistConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(whitelistConf.whitelist_2.allowed1, 'whitelisted1');
assert.strictEqual(whitelistConf.whitelist_2.allowed2, 'whitelisted2');
assert.strictEqual(whitelistConf.whitelist_2.blocked, 'undefined');
// Clean up
delete process.env.ALLOWED_VAR_1;
delete process.env.ALLOWED_VAR_2;
delete process.env.BLOCKED_VAR;
delete process.env.CONFIG_WHITELIST_2;
});
test.it('Should only include whitelisted vars in template context.', () => {
Conf.setTemplateRendering(true);
Conf.setAllowedEnvVars(['SAFE_VAR']);
process.env.SAFE_VAR = 'safe value';
process.env.UNSAFE_VAR = 'unsafe value';
process.env.CONFIG_WHITELIST_3 = `
{
"keys": "<%= Object.keys(env).join(',') %>"
}
`;
const whitelistConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
// Only SAFE_VAR should be in the keys
assert.strictEqual(whitelistConf.whitelist_3.keys, 'SAFE_VAR');
// Clean up
delete process.env.SAFE_VAR;
delete process.env.UNSAFE_VAR;
delete process.env.CONFIG_WHITELIST_3;
});
test.it('Should handle empty whitelist array (no vars accessible).', () => {
Conf.setTemplateRendering(true);
Conf.setAllowedEnvVars([]);
process.env.SOME_VAR = 'value';
process.env.CONFIG_WHITELIST_4 = `
{
"count": "<%= Object.keys(env).length %>"
}
`;
const whitelistConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(whitelistConf.whitelist_4.count, '0');
// Clean up
delete process.env.SOME_VAR;
delete process.env.CONFIG_WHITELIST_4;
});
test.it('Should reset whitelist when setAllowedEnvVars(null) is called.', () => {
Conf.setTemplateRendering(true);
Conf.setAllowedEnvVars(['LIMITED_VAR']);
process.env.LIMITED_VAR = 'limited';
process.env.EXTRA_VAR = 'extra';
// First config with whitelist
process.env.CONFIG_WHITELIST_5A = `
{
"limited": "<%= env.LIMITED_VAR || 'missing' %>",
"extra": "<%= env.EXTRA_VAR || 'missing' %>"
}
`;
const limitedConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(limitedConf.whitelist_5a.limited, 'limited');
assert.strictEqual(limitedConf.whitelist_5a.extra, 'missing');
// Reset whitelist
Conf.setAllowedEnvVars(null);
// Second config without whitelist
process.env.CONFIG_WHITELIST_5B = `
{
"limited": "<%= env.LIMITED_VAR || 'missing' %>",
"extra": "<%= env.EXTRA_VAR || 'missing' %>"
}
`;
const unlimitedConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(unlimitedConf.whitelist_5b.limited, 'limited');
assert.strictEqual(unlimitedConf.whitelist_5b.extra, 'extra');
// Clean up
delete process.env.LIMITED_VAR;
delete process.env.EXTRA_VAR;
delete process.env.CONFIG_WHITELIST_5A;
delete process.env.CONFIG_WHITELIST_5B;
});
test.it('Should match environment variables using regex patterns in whitelist.', () => {
Conf.setTemplateRendering(true);
Conf.setAllowedEnvVars([/^SAFE_/, 'EXACT_VAR']);
process.env.SAFE_VAR1 = 'safe1';
process.env.SAFE_VAR2 = 'safe2';
process.env.EXACT_VAR = 'exact';
process.env.BLOCKED_VAR = 'blocked';
process.env.CONFIG_REGEX_1 = `
{
"safe1": "<%= env.SAFE_VAR1 || 'missing' %>",
"safe2": "<%= env.SAFE_VAR2 || 'missing' %>",
"exact": "<%= env.EXACT_VAR || 'missing' %>",
"blocked": "<%= env.BLOCKED_VAR || 'missing' %>"
}
`;
const regexConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(regexConf.regex_1.safe1, 'safe1');
assert.strictEqual(regexConf.regex_1.safe2, 'safe2');
assert.strictEqual(regexConf.regex_1.exact, 'exact');
assert.strictEqual(regexConf.regex_1.blocked, 'missing');
// Clean up
delete process.env.SAFE_VAR1;
delete process.env.SAFE_VAR2;
delete process.env.EXACT_VAR;
delete process.env.BLOCKED_VAR;
delete process.env.CONFIG_REGEX_1;
});
test.it('Should support complex regex patterns for environment variable matching.', () => {
Conf.setTemplateRendering(true);
Conf.setAllowedEnvVars([/^(DB|API)_/, /^LOG_LEVEL$/]);
process.env.DB_HOST = 'localhost';
process.env.DB_PORT = '5432';
process.env.API_KEY = 'secret';
process.env.LOG_LEVEL = 'debug';
process.env.OTHER_VAR = 'other';
process.env.CONFIG_REGEX_2 = `
{
"db_host": "<%= env.DB_HOST || 'missing' %>",
"api_key": "<%= env.API_KEY || 'missing' %>",
"log": "<%= env.LOG_LEVEL || 'missing' %>",
"other": "<%= env.OTHER_VAR || 'missing' %>"
}
`;
const regexConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
assert.strictEqual(regexConf.regex_2.db_host, 'localhost');
assert.strictEqual(regexConf.regex_2.api_key, 'secret');
assert.strictEqual(regexConf.regex_2.log, 'debug');
assert.strictEqual(regexConf.regex_2.other, 'missing');
// Clean up
delete process.env.DB_HOST;
delete process.env.DB_PORT;
delete process.env.API_KEY;
delete process.env.LOG_LEVEL;
delete process.env.OTHER_VAR;
delete process.env.CONFIG_REGEX_2;
});
test.it('Should handle mixed regex and string patterns in whitelist.', () => {
Conf.setTemplateRendering(true);
Conf.setAllowedEnvVars([/^PATTERN_/, 'EXACT_MATCH_1', 'EXACT_MATCH_2']);
process.env.PATTERN_A = 'patternA';
process.env.PATTERN_B = 'patternB';
process.env.EXACT_MATCH_1 = 'exact1';
process.env.EXACT_MATCH_2 = 'exact2';
process.env.OTHER = 'other';
process.env.CONFIG_REGEX_3 = `
{
"keys": "<%= Object.keys(env).sort().join(',') %>"
}
`;
const regexConf = Conf.get(TEST_CONF_FILES_DIRECTORY, TEST_ENVIRONMENT_CONFIG_PREFIX);
const keys = regexConf.regex_3.keys.split(',');
assert.strictEqual(keys.includes('PATTERN_A'), true);
assert.strictEqual(keys.includes('PATTERN_B'), true);
assert.strictEqual(keys.includes('EXACT_MATCH_1'), true);
assert.strictEqual(keys.includes('EXACT_MATCH_2'), true);
assert.strictEqual(keys.includes('OTHER'), false);
// Clean up
delete process.env.PATTERN_A;
delete process.env.PATTERN_B;
delete process.env.EXACT_MATCH_1;
delete process.env.EXACT_MATCH_2;
delete process.env.OTHER;
delete process.env.CONFIG_REGEX_3;
});
});
});