Skip to content

Commit fa445ca

Browse files
author
Ovidiu Barabula
committed
feat(core): improve error handling for package.json config reader and add more tests
1 parent 33a1400 commit fa445ca

File tree

2 files changed

+203
-61
lines changed

2 files changed

+203
-61
lines changed
Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,80 @@
1+
import * as chai from 'chai';
2+
import * as chaiAspromised from 'chai-as-promised';
3+
chai.use(chaiAspromised);
4+
15
import { assert, expect } from 'chai';
26
import * as fs from 'fs';
37
import 'mocha';
48
import * as mockfs from 'mock-fs';
59
import * as path from 'path';
10+
import Logger from '../util/logger';
611
import PackageJsonConfigReader, { ERRORS } from './package-json-config-reader';
712

813
describe('PackageJsonConfigReader', () => {
914
const testDir = '/tmp/tests/';
1015
const priorConfiguredFile = 'priorConfiguredFile.json';
1116
const noConfigFile = 'noConfigFile.json';
1217
const noFrontvueConfigFile = 'noFrontVueConfigFile.json';
18+
const nonExistingFile = 'nonExistingConfigFile.json';
19+
const readonlyConfigFile = 'readonlyConfigFile.json';
1320

21+
// Restore FileSystem
22+
before(mockfs.restore);
23+
// Mock FileSystem with dummy files
1424
beforeEach(() => {
25+
mockfs.restore();
26+
const noConfigContent = '{}';
27+
const noFrontvueConfigContent = `{ "config": { "somePlugin": {} } }`;
28+
const withConfigContent = `{ "config": { "frontvue": { "key": "value" } } }`;
1529
mockfs({
1630
[testDir]: {
17-
[noConfigFile]: `{}`,
18-
[noFrontvueConfigFile]: `{
19-
"config": {
20-
"somePlugin": {}
21-
}
22-
}`,
23-
[priorConfiguredFile]: `{
24-
"config": {
25-
"frontvue": {
26-
"key": "value"
27-
}
28-
}
29-
}`,
31+
[noConfigFile]: noConfigContent,
32+
[noFrontvueConfigFile]: noFrontvueConfigContent,
33+
[priorConfiguredFile]: withConfigContent,
34+
[readonlyConfigFile]: mockfs.file({ content: withConfigContent, mode: 0o440 }),
3035
},
3136
});
3237
});
38+
// Clean up
39+
afterEach(mockfs.restore);
40+
after(async () => {
41+
return PackageJsonConfigReader('frontvue')
42+
.then(configReader => configReader.destroy());
43+
});
3344

34-
35-
it('throws if no namespace is passed', async () => {
36-
await PackageJsonConfigReader(undefined).catch((error: Error) => {
37-
expect(error.message, ERRORS.NO_NAMESPACE);
38-
});
45+
it('instantiates with no namespace', () => {
46+
// Reading actual package.json from current folder
47+
mockfs.restore();
48+
return expect(PackageJsonConfigReader())
49+
.to.eventually.have.all.keys('destroy', 'fetch', 'update');
3950
});
4051

4152

42-
it('throws if namespace is not a string', async () => {
43-
await PackageJsonConfigReader(1).catch((error: Error) => {
44-
expect(error.message, ERRORS.NO_NAMESPACE);
45-
});
53+
it('throws if namespace is not a string', () => {
54+
// Reading actual package.json from current folder
55+
mockfs.restore();
56+
return expect(PackageJsonConfigReader(1))
57+
.to.be.rejectedWith(ERRORS.INVALID_NAMESPACE);
4658
});
4759

4860

49-
it('instantiates with no custom filepath', async () => {
61+
it('instantiates with no custom filepath', () => {
62+
// Reading actual package.json from current folder
5063
mockfs.restore();
51-
expect(await PackageJsonConfigReader('frontvue'))
52-
.to.be.an('object')
53-
.to.have.all.keys('destroy', 'fetch', 'update');
64+
return expect(PackageJsonConfigReader('frontvue'))
65+
.to.eventually.be.an('object')
66+
.to.eventually.have.all.keys('destroy', 'fetch', 'update');
5467
});
5568

5669

5770
it('instantiates without any config object', async () => {
5871
const filepath = path.join(testDir, noConfigFile);
5972
const configReader = await PackageJsonConfigReader('frontvue', filepath);
6073
const config = await configReader.fetch();
61-
const fileContents = await fs.readFile(filepath, { encoding: 'utf-8' }, (error, data) => {
74+
const fileContents = fs.readFile(filepath, { encoding: 'utf-8' }, (error, data) => {
6275
expect(JSON.parse(data)).to.eql({config: { frontvue: {} }});
6376
});
64-
});
77+
}).timeout(12000);
6578

6679

6780
it('instantiates without Frontvue config object', async () => {
@@ -71,21 +84,21 @@ describe('PackageJsonConfigReader', () => {
7184
const fileContents = await fs.readFile(filepath, { encoding: 'utf-8' }, (error, data) => {
7285
expect(JSON.parse(data)).to.eql({config: { somePlugin: {}, frontvue: {} }});
7386
});
74-
});
87+
}).timeout(12000);
7588

7689

7790
it('instantiates with prior Frontvue config object', async () => {
7891
const filepath = path.join(testDir, priorConfiguredFile);
7992
const configReader = await PackageJsonConfigReader('frontvue', filepath);
80-
expect(await configReader.fetch()).to.eql({ key: 'value' });
93+
return expect(configReader.fetch()).to.eventually.eql({ key: 'value' });
8194
});
8295

8396

8497
it('updates config object', async () => {
8598
const filepath = path.join(testDir, noConfigFile);
8699
const configReader = await PackageJsonConfigReader('frontvue', filepath);
87100
const updated = await configReader.update({ key: 'value' });
88-
expect(await configReader.fetch()).to.eql({ key: 'value' });
101+
return expect(configReader.fetch()).to.eventually.eql({ key: 'value' });
89102
});
90103

91104

@@ -97,5 +110,55 @@ describe('PackageJsonConfigReader', () => {
97110
});
98111

99112

100-
afterEach(mockfs.restore);
113+
it('accepts custom logger', () => {
114+
const filepath = path.join(testDir, priorConfiguredFile);
115+
return expect(
116+
PackageJsonConfigReader('frontvue', filepath, Logger('frontvue')('customLogger')),
117+
).to.eventually.not.throw;
118+
}).timeout(12000);
119+
120+
121+
it('rejects promise if filepath config file can\'t be read', () => {
122+
return expect(PackageJsonConfigReader('frontvue', path.join(testDir, nonExistingFile)))
123+
.to.be.rejectedWith(RegExp(ERRORS.RW_ERROR));
124+
}).timeout(12000);
125+
126+
127+
it('rejects promise if refetching config from file fails', async () => {
128+
const filepath = path.join(testDir, priorConfiguredFile);
129+
const configReader = await PackageJsonConfigReader('frontvue', filepath);
130+
// We destroy the mocked fs and replace the file with something that can't be read
131+
mockfs.restore();
132+
mockfs({
133+
[testDir]: {
134+
[priorConfiguredFile]: '',
135+
},
136+
});
137+
return expect(configReader.fetch()).to.be.rejectedWith(RegExp(ERRORS.RW_ERROR));
138+
}).timeout(12000);
139+
140+
141+
it('rejects promise if destroy fails after initial config file fetch', async () => {
142+
const filepath = path.join(testDir, priorConfiguredFile);
143+
const configReader = await PackageJsonConfigReader('frontvue', filepath);
144+
// We destroy the mocked fs and recreate the file with something that can't be read
145+
mockfs.restore();
146+
mockfs({
147+
[testDir]: {
148+
[priorConfiguredFile]: mockfs.file({
149+
content: 'File content',
150+
mode: 0o330,
151+
}),
152+
},
153+
});
154+
155+
return expect(configReader.destroy()).to.be.rejectedWith(RegExp(ERRORS.RW_ERROR));
156+
});
157+
158+
159+
it('rejects promise if trying to destroy from readonly file', async () => {
160+
const filepath = path.join(testDir, readonlyConfigFile);
161+
const configReader = await PackageJsonConfigReader('frontvue', filepath);
162+
return expect(configReader.destroy()).to.be.rejectedWith(Error);
163+
}).timeout(12000);
101164
});

src/config-manager/package-json-config-reader.ts

Lines changed: 108 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,110 @@
66
*/
77

88
import FileReader from '../util/file-reader';
9-
import { hasNested } from '../util/utility-functions';
9+
import Logger, { ILogger } from '../util/logger';
10+
import { hasNested, retry } from '../util/utility-functions';
1011

1112
export interface Config {
1213
[key: string]: any;
1314
}
1415

1516
export interface ConfigReader {
16-
destroy(): Promise<Config>;
17-
fetch(): Promise<Config>;
18-
update(config: Config): Promise<boolean>;
17+
destroy(): Promise<Config|Error>;
18+
fetch(): Promise<Config|Error>;
19+
update(config: Config): Promise<boolean|Error>;
1920
}
2021

2122
export type ConfigReaderConstructor = (namespace: string, filepath?: string) => ConfigReader;
2223

2324
export const ERRORS = {
24-
NO_NAMESPACE: 'PackageJsonConfigReader requires parameter 1 to be string',
25+
INVALID_NAMESPACE: 'PackageJsonConfigReader() requires parameter 1 to be string',
26+
RW_ERROR: 'PackageJsonConfigReader> An error occured while reading/writing the file',
2527
};
2628

2729

2830
/**
2931
* Factory function for package.json configuration reader
3032
* @param namespace Configuration key in package.json 'config' object
33+
* @param filepath Custom file path to store/get configuration
34+
* @param logger Instance of a logger that implements ILogger interface
3135
*/
32-
async function PackageJsonConfigReader(namespace: string, filepath?: string): Promise<ConfigReader> {
33-
if (typeof namespace === 'undefined') {
34-
throw new Error(ERRORS.NO_NAMESPACE);
36+
async function PackageJsonConfigReader(
37+
namespace: string = 'frontvue',
38+
filepath: string = './package.json',
39+
logger: ILogger = Logger('frontvue')('packageJsonConfigReader'),
40+
): Promise<ConfigReader> {
41+
if (typeof namespace !== 'string') {
42+
throw new Error(ERRORS.INVALID_NAMESPACE);
3543
}
3644

37-
if (typeof namespace !== 'string') {
38-
throw new Error(ERRORS.NO_NAMESPACE);
45+
// Get config file contents and catch any errors
46+
const configFile = FileReader(filepath);
47+
let packageJson: Config;
48+
try {
49+
packageJson = await readConfigFile();
50+
} catch (error) {
51+
return Promise.reject(errorHandler(error));
3952
}
4053

41-
const configFile = FileReader(filepath || './package.json');
42-
const packageJson = await configFile.read();
54+
55+
/**
56+
* Read configuration file
57+
*/
58+
async function readConfigFile(): Promise<Config|Error> {
59+
// Catch any errors when retrieving file contents
60+
try {
61+
return await retry(configFile.read, { logChannel: 'packageJsonConfigReader' });
62+
} catch (error) {
63+
return Promise.reject(errorHandler(error));
64+
}
65+
}
66+
67+
68+
/**
69+
* Refetch config file contents
70+
*/
71+
async function refetchConfigFileContents(): Promise<void|Error> {
72+
try {
73+
packageJson = await readConfigFile();
74+
return undefined;
75+
} catch (error) {
76+
return Promise.reject(errorHandler());
77+
}
78+
}
79+
80+
81+
/**
82+
* Write config object to configuration file
83+
* @param config Config object
84+
*/
85+
async function writeConfigFile(config: Config): Promise<boolean|Error> {
86+
// retry() requires a function and we can't pass configFile.write without the config object
87+
const writeConfigToFile = async () => await configFile.write(config);
88+
89+
// Update the file
90+
try {
91+
return await retry(writeConfigToFile, { logChannel: 'packageJsonConfigReader' });
92+
} catch (error) {
93+
return Promise.reject(errorHandler());
94+
}
95+
}
96+
97+
98+
/**
99+
* Handler for failed promise
100+
* @param error Error object
101+
*/
102+
function errorHandler(error?: Error): Error {
103+
const errorMessage = error ? `\n ${error.message}` : '';
104+
logger.fatal(`${ERRORS.RW_ERROR} ${errorMessage}`);
105+
return new Error(ERRORS.RW_ERROR);
106+
}
43107

44108

45109
/**
46110
* Initialize empty configuration if not found
47111
*/
48-
async function initializeEmptyConfig(): Promise<boolean> {
112+
async function initializeEmptyConfig(): Promise<boolean|Error> {
49113
if (hasNested(packageJson, 'config')) {
50114
packageJson.config[namespace] = {};
51115
} else {
@@ -54,43 +118,58 @@ async function PackageJsonConfigReader(namespace: string, filepath?: string): Pr
54118
};
55119
}
56120

57-
const written = await configFile.write(packageJson);
58-
return written;
121+
return await writeConfigFile(packageJson);
122+
}
123+
124+
125+
/**
126+
* Delete namespace from config file
127+
*/
128+
function removeNamespaceFromConfig(): void {
129+
// Delete config object
130+
packageJson.config[namespace] = undefined;
131+
packageJson = JSON.parse(JSON.stringify(packageJson));
59132
}
60133

61134

62135
/**
63136
* Fetch configuration object
64137
*/
65138
async function fetch(): Promise<Config> {
66-
return new Promise((resolve, reject) => {
67-
return resolve(packageJson.config[namespace]);
68-
});
139+
await refetchConfigFileContents();
140+
return Promise.resolve(packageJson.config[namespace]);
69141
}
70142

71143

72144
/**
73145
* Update configuration object
74146
* @param config New configuration object
75147
*/
76-
async function update(config: Config) {
148+
async function update(config: Config): Promise<boolean|Error> {
77149
packageJson.config[namespace] = {...packageJson.config[namespace], ...config};
78-
const updated = await configFile.write(packageJson);
79-
return updated;
150+
151+
// Update the file
152+
return await writeConfigFile(packageJson);
80153
}
81154

82155

83156
/**
84157
* Remove configuration from file and return the removed config object
85158
*/
86-
async function destroy(): Promise<Config> {
87-
// Save existing config
88-
const fileContent = await configFile.read();
89-
// Delete config object
90-
delete packageJson.config[namespace];
91-
// Update the file
92-
await configFile.write(packageJson);
93-
return fileContent.config[namespace];
159+
async function destroy(): Promise<Config|Error> {
160+
await refetchConfigFileContents();
161+
162+
try {
163+
// Store it to be returned later
164+
const config: Config = packageJson.config[namespace];
165+
// Delete config object
166+
removeNamespaceFromConfig();
167+
// If the file was saved, return the removed config object
168+
await writeConfigFile(packageJson);
169+
return config;
170+
} catch (error) {
171+
return Promise.reject(error);
172+
}
94173
}
95174

96175

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