import { promises as fs } from 'fs'; import { createConfig } from './index'; // Test JSON data const jsonDefaults = { setting: 'defaultvalue', another: { setting: 'avalue', }, }; const jsonLocals = { setting: 'localvalue', another: { more: 'stuff', }, even: { deeper: { key: 'sodeep', }, }, }; describe('Config', () => { it('should only have values from config.defaults.json', async () => { await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); const config = createConfig(); await fs.unlink('config.defaults.json'); expect(config.setting).toBe('defaultvalue'); expect(config.another.setting).toBe('avalue'); }); it('should only have values from config.json', async () => { await fs.writeFile('config.json', JSON.stringify(jsonLocals)); const config = createConfig(); await fs.unlink('config.json'); expect(config.setting).toBe('localvalue'); expect(config.another.more).toBe('stuff'); }); it('should have both values with preference to config.json', async () => { await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); await fs.writeFile('config.json', JSON.stringify(jsonLocals)); const config = createConfig(); await fs.unlink('config.json'); await fs.unlink('config.defaults.json'); expect(config.setting).toBe('localvalue'); expect(config.another.more).toBe('stuff'); expect(config.another.setting).toBe('avalue'); }); it('should respect a given basePath', async () => { // Create the tmp directory only if it doesn't exist await fs.mkdir('tmp', { recursive: true }); await fs.writeFile('tmp/config.defaults.json', JSON.stringify(jsonDefaults)); await fs.writeFile('tmp/config.json', JSON.stringify(jsonLocals)); const config = createConfig('tmp'); await fs.unlink('tmp/config.json'); await fs.unlink('tmp/config.defaults.json'); // Remove the tmp directory after the test await fs.rmdir('tmp', { recursive: true }); expect(config.setting).toBe('localvalue'); expect(config.another.more).toBe('stuff'); expect(config.another.setting).toBe('avalue'); }); it('should have all values with preference to env', async () => { await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); await fs.writeFile('config.json', JSON.stringify(jsonLocals)); process.env['SETTING'] = 'overwritten-by-env'; process.env['ANOTHER_MORE'] = 'false'; process.env['EVEN_DEEPER_KEY'] = 'true'; const config = createConfig(); await fs.unlink('config.json'); await fs.unlink('config.defaults.json'); delete process.env['SETTING']; delete process.env['ANOTHER_MORE']; delete process.env['EVEN_DEEPER_KEY']; expect(config.setting).toBe('overwritten-by-env'); expect(config.another.more).toBe(false); expect(config.even.deeper.key).toBe(true); expect(config.another.setting).toBe('avalue'); }); it('should have all values with preference to env and prefix', async () => { await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); await fs.writeFile('config.json', JSON.stringify(jsonLocals)); process.env['P_SETTING'] = 'overwritten-by-env'; process.env['P_ANOTHER_MORE'] = 'false'; process.env['P_EVEN_DEEPER_KEY'] = 'true'; const config = createConfig(undefined, 'p'); await fs.unlink('config.json'); await fs.unlink('config.defaults.json'); delete process.env['P_SETTING']; delete process.env['P_ANOTHER_MORE']; delete process.env['P_EVEN_DEEPER_KEY']; expect(config.setting).toBe('overwritten-by-env'); expect(config.another.more).toBe(false); expect(config.even.deeper.key).toBe(true); expect(config.another.setting).toBe('avalue'); }); it('should read in a file when given', async () => { jsonLocals.setting = 'file:file.txt'; await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); await fs.writeFile('config.json', JSON.stringify(jsonLocals)); await fs.writeFile('file.txt', 'value-from-file'); const config = createConfig(); await fs.unlink('config.json'); await fs.unlink('config.defaults.json'); await fs.unlink('file.txt'); jsonLocals.setting = 'localvalue'; expect(config.setting).toBe('value-from-file'); }); it('should show a message and keep the setting when file given but not found', async () => { jsonLocals.setting = 'file:file.txt'; await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); await fs.writeFile('config.json', JSON.stringify(jsonLocals)); const config = createConfig(); await fs.unlink('config.json'); await fs.unlink('config.defaults.json'); jsonLocals.setting = 'localvalue'; expect(config.setting).toBe('file:file.txt'); }); it('should _reload if asked', async () => { await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); await fs.writeFile('config.json', JSON.stringify(jsonLocals)); const config = createConfig(); expect(config.setting).toBe('localvalue'); jsonLocals.setting = 'reloaded-value'; await fs.writeFile('config.json', JSON.stringify(jsonLocals)); config._reload(); await fs.unlink('config.json'); await fs.unlink('config.defaults.json'); jsonLocals.setting = 'localvalue'; expect(config.setting).toBe('reloaded-value'); }); it('should redact passwords if using _show', async () => { jsonLocals.even.deeper.key = 'password123'; await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); await fs.writeFile('config.json', JSON.stringify(jsonLocals)); const config = createConfig(); await fs.unlink('config.json'); await fs.unlink('config.defaults.json'); jsonLocals.even.deeper.key = 'sodeep'; expect(config._show().even.deeper.key).toBe('REDACTED'); }); it('should handle missing config.defaults.json gracefully', async () => { // Ensure config.defaults.json does not exist await fs.unlink('config.defaults.json').catch(() => {}); // Ignore errors if the file doesn't exist const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); const config = createConfig(); // Ensure no crash and empty config expect(config.setting).toBeUndefined(); // Check if the missing file message was logged expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining(`Defaults file missing`) ); consoleSpy.mockRestore(); }); it('should handle missing config.json gracefully', async () => { // Create config.defaults.json but ensure config.json does not exist await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); await fs.unlink('config.json').catch(() => {}); const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); const config = createConfig(); await fs.unlink('config.defaults.json'); // Expect log message for the missing file expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('local file missing') ); // Defaults should still be loaded expect(config.setting).toBe('defaultvalue'); expect(config.another.setting).toBe('avalue'); consoleSpy.mockRestore(); }); it('should handle invalid JSON content in config.defaults.json gracefully', async () => { // Write invalid JSON to config.defaults.json await fs.writeFile('config.defaults.json', '{ invalid json }'); await fs.writeFile('config.json', JSON.stringify(jsonLocals)); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); const config = createConfig(); // Expect a log message for invalid JSON expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('Error reading defaults file') ); // Clean up files await fs.unlink('config.defaults.json'); await fs.unlink('config.json'); // Expect config.json to still load successfully expect(config.setting).toBe('localvalue'); expect(config.another.more).toBe('stuff'); consoleSpy.mockRestore(); }); it('should handle invalid JSON content in config.json gracefully', async () => { // Write invalid JSON to config.json await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); await fs.writeFile('config.json', '{ invalid json }'); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); const config = createConfig(); // Expect a log message for invalid JSON expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('Error reading local config file') ); // Clean up files await fs.unlink('config.defaults.json'); await fs.unlink('config.json'); // Expect defaults to still load successfully expect(config.setting).toBe('defaultvalue'); expect(config.another.setting).toBe('avalue'); consoleSpy.mockRestore(); }); it('should serialize only configData using toJSON()', async () => { // Create test configuration files await fs.writeFile('config.defaults.json', JSON.stringify(jsonDefaults)); await fs.writeFile('config.json', JSON.stringify(jsonLocals)); const config = createConfig(); // Serialize the config object to JSON const serializedConfig = JSON.stringify(config); // Clean up test files await fs.unlink('config.defaults.json'); await fs.unlink('config.json'); // Parse the serialized JSON const parsedConfig = JSON.parse(serializedConfig); // Check that all expected configData properties are present expect(parsedConfig.setting).toBe('localvalue'); expect(parsedConfig.another.setting).toBe('avalue'); // From defaults expect(parsedConfig.another.more).toBe('stuff'); // From local expect(parsedConfig.even.deeper.key).toBe('sodeep'); // From local // Ensure internal properties are not present expect(parsedConfig).not.toHaveProperty('configDefaults'); expect(parsedConfig).not.toHaveProperty('configLocal'); expect(parsedConfig).not.toHaveProperty('envPrefix'); }); it('should log an error if a file referenced by "file:" is missing', async () => { // Write test configuration file await fs.writeFile( 'config.json', JSON.stringify({ setting: 'file:nonexistent-file.txt', }) ); // Spy on console.error const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); const config = createConfig(); // Clean up test files await fs.unlink('config.json'); // Ensure the original "file:" reference remains unchanged expect(config.setting).toBe('file:nonexistent-file.txt'); // Ensure error is logged for the missing file expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('An unknown error occurred') ); consoleSpy.mockRestore(); }); });