UNPKG

multiconf

Version:

Work with JSON configs

599 lines (495 loc) 22.2 kB
// 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; }); }); });