Skip to content
Snippets Groups Projects
Commit bf629189 authored by Sigmund, Dominik's avatar Sigmund, Dominik
Browse files

first doing on typescript

parent f798690c
No related tags found
1 merge request!9Draft: Resolve "Rewrite in TypeScript"
Pipeline #129525 passed
This commit is part of merge request !9. Comments created here will be created in the context of that merge request.
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();
});
});
\ No newline at end of file
import * as fs from 'fs';
import * as path from 'path';
type ConfigObject = Record<string, any>;
export class Config {
[key: string]: any; // Index signature to allow dynamic properties
private basePath: string | undefined;
private envPrefix: string | undefined;
private configDefaults: string;
private configLocal: string;
private configData: ConfigObject;
constructor(basePath: string | undefined = undefined, envPrefix: string | undefined = undefined) {
this.basePath = basePath;
this.envPrefix = envPrefix ? envPrefix.toUpperCase() + "_" : "";
this.configDefaults = this.basePath
? path.join(this.basePath, 'config.defaults.json')
: path.join(path.dirname(require.main?.filename || ''), 'config.defaults.json');
this.configLocal = this.basePath
? path.join(this.basePath, 'config.json')
: path.join(path.dirname(require.main?.filename || ''), 'config.json');
this.configData = {};
this._reload();
}
private objectDeepKeys(obj: ConfigObject): string[] {
return Object.keys(obj)
.filter((key) => obj[key] instanceof Object)
.map((key) => this.objectDeepKeys(obj[key]).map((k) => `${key}.${k}`))
.reduce((x, y) => x.concat(y), Object.keys(obj));
}
private set(obj: ConfigObject, path: string, value: any): void {
const keys = path.split('.');
let schema = obj;
keys.slice(0, -1).forEach((key) => {
if (!schema[key]) schema[key] = {};
schema = schema[key];
});
schema[keys[keys.length - 1]] = value;
}
private get(obj: ConfigObject, path: string): any {
const keys = path.split('.');
let schema = obj;
keys.slice(0, -1).forEach((key) => {
if (!schema[key]) schema[key] = {};
schema = schema[key];
});
return schema[keys[keys.length - 1]];
}
private merge(target: ConfigObject, source: ConfigObject): ConfigObject {
for (const key in source) {
if (
Object.prototype.hasOwnProperty.call(source, key) &&
typeof source[key] === 'object' &&
!Array.isArray(source[key])
) {
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {};
}
this.merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
public _reload(): void {
// Handle config.defaults.json
try {
if (fs.existsSync(this.configDefaults)) {
const defaults = JSON.parse(fs.readFileSync(this.configDefaults, 'utf8'));
this.configData = this.merge(this.configData, defaults);
} else {
console.log(`Defaults file missing: ${this.configDefaults}`);
}
} catch (error) {
console.error(`Error reading defaults file: ${this.configDefaults}`);
}
// Handle config.json
try {
if (fs.existsSync(this.configLocal)) {
const local = JSON.parse(fs.readFileSync(this.configLocal, 'utf8'));
this.configData = this.merge(this.configData, local);
} else {
console.log(`local file missing: ${this.configLocal}`);
}
} catch (error) {
console.error(`Error reading local config file: ${this.configLocal}`);
}
// Apply environment variables
const keys = this.objectDeepKeys(this.configData);
for (const key of keys) {
const envKey = this.envPrefix + key.toUpperCase().replace(/\./g, '_');
const envValue = process.env[envKey];
if (envValue !== undefined) {
const parsedValue =
envValue === 'true' ? true : envValue === 'false' ? false : envValue;
this.set(this.configData, key, parsedValue); // Update configData
}
}
// Handle "file:" values
for (const key of keys) {
const value = this.get(this.configData, key)?.toString();
if (value?.startsWith('file:')) {
try {
const fileValue = fs.readFileSync(value.replace('file:', ''), 'utf8');
this.set(this.configData, key, fileValue);
} catch (error) {
console.error(`An unknown error occurred:${error}`);
}
}
}
// Assign the configData properties to the instance
Object.assign(this, this.configData);
}
public _show(): ConfigObject {
const redactedConfig = JSON.parse(JSON.stringify(this.configData));
const keys = this.objectDeepKeys(redactedConfig);
for (const key of keys) {
if (
/password|secret|token|key|apikey|apitoken|apisecret|user|username/i.test(key)
) {
this.set(redactedConfig, key, 'REDACTED');
}
}
return redactedConfig;
}
public toJSON(): Record<string, any> {
// Return only the properties of configData for JSON serialization
return { ...this.configData };
}
}
// Factory function for usage compatibility
export function createConfig(basePath?: string, envPrefix?: string): Config {
return new Config(basePath, envPrefix);
}
export default Config;
\ No newline at end of file
module.exports = {
preset: 'ts-jest', // Use ts-jest to handle TypeScript files
testEnvironment: 'node', // Set the test environment to Node.js
moduleFileExtensions: ['ts', 'js'], // Support both TypeScript and JavaScript files
testMatch: ['**/*.test.ts'], // Match test files ending with .test.ts
collectCoverage: true,
coverageReporters: [
"json",
"lcov",
"text",
"clover",
"html"
],
coverageDirectory: "docs/coverage"
};
\ No newline at end of file
Source diff could not be displayed: it is too large. Options to address this: view the blob.
{ {
"name": "@libs/config", "name": "@libs/config",
"version": "1.13.6", "version": "1.14.0",
"description": "Simple Config with ENV Support", "description": "Simple Config with ENV Support",
"main": "index.js", "main": "index.ts",
"scripts": { "scripts": {
"test": "jest", "test": "jest --config jest.config.js",
"test:mutation": "stryker run" "test:mutation": "stryker run"
}, },
"keywords": [ "keywords": [
...@@ -13,28 +13,17 @@ ...@@ -13,28 +13,17 @@
], ],
"author": "Dominik Sigmund <dominik.sigmund@br.de>", "author": "Dominik Sigmund <dominik.sigmund@br.de>",
"license": "ISC", "license": "ISC",
"dependencies": {
"lodash.merge": "^4.6.2"
},
"devDependencies": { "devDependencies": {
"@stryker-mutator/core": "^8.2.6", "@stryker-mutator/core": "^8.2.6",
"@stryker-mutator/javascript-mutator": "^4.0.0", "@stryker-mutator/javascript-mutator": "^4.0.0",
"@stryker-mutator/jest-runner": "^8.2.6", "@stryker-mutator/jest-runner": "^8.2.6",
"@stryker-mutator/typescript": "^4.0.0", "@stryker-mutator/typescript": "^4.0.0",
"jest": "^29.7.0" "@types/jest": "^29.5.14",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.3"
}, },
"publishConfig": { "publishConfig": {
"@libs:registry": "https://gitlab.ard.de/api/v4/projects/919/packages/npm/" "@libs:registry": "https://gitlab.ard.de/api/v4/projects/919/packages/npm/"
},
"jest": {
"collectCoverage": true,
"coverageReporters": [
"json",
"lcov",
"text",
"clover",
"html"
],
"coverageDirectory": "docs/coverage"
} }
} }
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"resolveJsonModule": true
},
"include": ["examples/**/*.ts", "index.ts", "examples/**/*.json"],
"exclude": ["node_modules"]
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment