Skip to content

Commit c859efe

Browse files
author
Ovidiu Barabula
committed
feat(core): improve error handling for config manager and add tests
1 parent fa445ca commit c859efe

File tree

2 files changed

+179
-30
lines changed

2 files changed

+179
-30
lines changed

src/config-manager/index.spec.ts

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
import { assert, expect } from 'chai';
22
import 'mocha';
33
import * as path from 'path';
4-
import ConfigManager, { IConfigManager } from './index';
4+
import { stdout } from 'test-console';
5+
import Logger from '../util/logger';
6+
import ConfigManager, { ERRORS, IConfigManager } from './index';
57
import { Config, ConfigReader } from './package-json-config-reader';
68

79
describe('ConfigManager', () => {
8-
const testDir = '/tmp/tests/';
9-
const configFile = 'package.json';
1010
let configManager: IConfigManager;
1111

12-
13-
beforeEach(async () => configManager = await ConfigManager('frontvue'));
12+
beforeEach(async function () {
13+
this.timeout(12000);
14+
configManager = await ConfigManager('frontvue');
15+
});
1416

1517

1618
it('instantiates', async () => {
17-
expect(configManager).to.be.an('object').to.have.all.keys('get', 'has', 'remove', 'set');
19+
expect(configManager).to.be.an('object')
20+
.to.contain.keys('get', 'has', 'remove', 'set');
21+
});
22+
23+
24+
it('instantiates with default namespace', async () => {
25+
expect(await ConfigManager()).to.be.an('object')
26+
.to.contain.keys('get', 'has', 'remove', 'set');
1827
});
1928

2029

@@ -36,9 +45,15 @@ describe('ConfigManager', () => {
3645
},
3746
};
3847
};
39-
4048
const customConfigManager = await ConfigManager('frontvue', customReader);
41-
expect(customConfigManager).to.be.an('object').to.have.all.keys('get', 'has', 'remove', 'set');
49+
expect(customConfigManager).to.be.an('object')
50+
.to.contain.keys('get', 'has', 'remove', 'set');
51+
});
52+
53+
54+
it('instantiates with custom logger', async () => {
55+
const customLogger = Logger('frontvue')('customConfigManager');
56+
const configManagerWithLogger = await ConfigManager('frontvue', undefined, customLogger);
4257
});
4358

4459

@@ -89,4 +104,89 @@ describe('ConfigManager', () => {
89104
const removed = await configManager.remove('key2', 'key3');
90105
expect(await configManager.get()).to.eql({});
91106
});
107+
108+
109+
it('console logs even if errorHandler did not receive an error', () => {
110+
const inspect = stdout.inspect();
111+
configManager.errorHandler();
112+
inspect.restore();
113+
expect(inspect.output.length).to.gt(0);
114+
});
115+
116+
117+
describe('Inaccessible configuration', () => {
118+
// Stub for customReader with errors
119+
const badCustomReaderFactory = (
120+
options: {
121+
badDestroy?: boolean,
122+
badFetch?: boolean,
123+
badUpdate?: boolean,
124+
} = {},
125+
) => {
126+
const {
127+
badDestroy = false,
128+
badFetch = false,
129+
badUpdate = false,
130+
} = options;
131+
132+
// Custom reader with very nasty errors
133+
return function (namespace: string): ConfigReader {
134+
return {
135+
destroy(): Promise<Config|Error> {
136+
if (badDestroy) {
137+
return Promise.reject(new Error('customReader> Some nasty destroy() error here'));
138+
}
139+
return Promise.resolve({});
140+
},
141+
fetch(): Promise<Config|Error> {
142+
if (badFetch) {
143+
return Promise.reject(new Error('customReader> Some nasty fetch() error here'));
144+
}
145+
return Promise.resolve({});
146+
},
147+
update(object: object): Promise<boolean|Error> {
148+
if (badUpdate) {
149+
return Promise.reject(new Error('customReader> Some nasty update() error here'));
150+
}
151+
return Promise.resolve(true);
152+
},
153+
};
154+
};
155+
};
156+
157+
158+
it('returns rejected promise when configReader fails to instantiate', async () => {
159+
const customReader = () => {
160+
return new Promise((resolve, reject) => {
161+
reject(new Error('customReader> Some nasty init error here'));
162+
});
163+
};
164+
165+
return expect(ConfigManager('frontvue', customReader))
166+
.to.be.rejectedWith(Error);
167+
});
168+
169+
170+
it('returns a rejected promise when configuration cannot be fetched', async () => {
171+
const customReader = badCustomReaderFactory({ badFetch: true });
172+
return expect(ConfigManager('frontvue', customReader))
173+
.to.be.rejectedWith(Error);
174+
});
175+
176+
177+
it('returns a rejected promise when .set() fails', async () => {
178+
const customReader = badCustomReaderFactory({ badUpdate: true });
179+
const badConfigManager = await ConfigManager('frontvue', customReader);
180+
return expect(badConfigManager.set('key', 'value'))
181+
.to.be.rejectedWith(Error);
182+
});
183+
184+
185+
it('returns a rejected promise when .remove() fails', async () => {
186+
const customReader = badCustomReaderFactory({ badUpdate: true });
187+
const badConfigManager = await ConfigManager('frontvue', customReader);
188+
return expect(badConfigManager.remove('key'))
189+
.to.be.rejectedWith(Error);
190+
});
191+
});
92192
});

src/config-manager/index.ts

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* @since 0.1.0
66
*/
77

8+
import Logger, { ILogger } from '../util/logger';
89
import PackageJsonConfigReader, {
910
Config,
1011
ConfigReader,
@@ -15,10 +16,14 @@ import PackageJsonConfigReader, {
1516
export interface IConfigManager {
1617
has(key: string): Promise<boolean>;
1718
get(key?: string): Promise<Config | any>;
18-
set(option: Config | string, value?: any): Promise<boolean>;
19-
remove(...options: string[]): Promise<boolean>;
19+
set(option: Config | string, value?: any): Promise<boolean|Error>;
20+
remove(...options: string[]): Promise<boolean|Error>;
21+
errorHandler?(error?: Error): Error;
2022
}
2123

24+
export const ERRORS = {
25+
CONFIG_FETCH_FAILED: 'There was an error while accessing the configuration:',
26+
};
2227

2328
/**
2429
* Configuration manager constructor
@@ -27,22 +32,47 @@ export interface IConfigManager {
2732
* @param customReader Custom ConfigReader
2833
*/
2934
async function ConfigManager(
30-
namespace: string,
35+
namespace: string = 'frontvue',
3136
customReader?: ConfigReaderConstructor,
37+
logger: ILogger = Logger('frontvue')('ConfigManager'),
3238
): Promise<IConfigManager> {
3339
let configReader: ConfigReader;
3440

35-
// Check for custom config reader
36-
if (customReader && typeof customReader === 'function') {
37-
// Initialize custom config reader
38-
configReader = customReader(namespace);
39-
} else {
40-
// If not, stick with the default config reader
41-
configReader = await PackageJsonConfigReader(namespace);
41+
// Instantiate configReader
42+
try {
43+
// Check for custom config reader
44+
if (customReader && typeof customReader === 'function') {
45+
// Initialize custom config reader
46+
// Wrap customReader with a promise to handle this asynchronous
47+
configReader = await Promise.resolve(customReader(namespace));
48+
} else {
49+
// If not, stick with the default config reader
50+
configReader = await PackageJsonConfigReader(namespace);
51+
}
52+
} catch (error) {
53+
return Promise.reject(errorHandler(error));
54+
}
55+
56+
57+
// Catch any errors when fetching configuration object
58+
let config: Config;
59+
try {
60+
// Get the configuration contents
61+
config = await configReader.fetch();
62+
} catch (error) {
63+
return Promise.reject(errorHandler(error));
4264
}
4365

44-
// Get the configuration contents
45-
let config: Config = await configReader.fetch();
66+
67+
/**
68+
* Error handler function
69+
* @param error Caught error
70+
*/
71+
function errorHandler(error?: Error): Error {
72+
const errorMessage = error ? `\n ${error.message}` : '';
73+
logger.fatal(`${ERRORS.CONFIG_FETCH_FAILED} ${errorMessage}`);
74+
return new Error(ERRORS.CONFIG_FETCH_FAILED);
75+
}
4676

4777

4878
/**
@@ -75,7 +105,7 @@ async function ConfigManager(
75105
* @param option Configuration object or object key
76106
* @param value New value to be set if option is an object key ("string")
77107
*/
78-
async function set(option: Config | string, value?: any): Promise<boolean> {
108+
async function set(option: Config | string, value?: any): Promise<boolean|Error> {
79109
// If we're passing in a "key" and a "value"
80110
if (typeof option === 'string' && typeof value !== 'undefined') {
81111
config[option] = value;
@@ -86,29 +116,48 @@ async function ConfigManager(
86116
return false;
87117
}
88118

89-
const saved: boolean = await configReader.update(config);
90-
return saved;
119+
try {
120+
const saved: boolean | Error = await configReader.update(config);
121+
return saved;
122+
} catch (error) {
123+
return Promise.reject(errorHandler(error));
124+
}
125+
91126
}
92127

93128

94129
/**
95130
* Removes one or more options from config
96131
* @param option Option name or array of option names
97132
*/
98-
async function remove(...options: string[]): Promise<boolean> {
133+
async function remove(...options: string[]): Promise<boolean|Error> {
99134
options.forEach(item => delete config[item]);
100-
const saved: boolean = await configReader.update(config);
101-
return saved;
135+
try {
136+
const saved: boolean | Error = await configReader.update(config);
137+
return saved;
138+
} catch (error) {
139+
return Promise.reject(errorHandler(error));
140+
}
102141
}
103142

104-
105-
// Returning config manager public API
106-
return Object.freeze({
143+
// Creating the public API object
144+
let publicApi: IConfigManager = {
107145
get,
108146
has,
109147
remove,
110148
set,
111-
});
149+
};
150+
151+
// Adding private methods to public API in test environment
152+
/* test:start */
153+
publicApi = {...publicApi,
154+
errorHandler,
155+
};
156+
/* test:end */
157+
158+
159+
// Returning config manager public API
160+
return Object.freeze(publicApi);
112161
}
113162

114163
export default ConfigManager;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy