Skip to content

Commit f48488c

Browse files
committed
test: added middleware testing
1 parent a4fe7b6 commit f48488c

File tree

7 files changed

+476
-82
lines changed

7 files changed

+476
-82
lines changed

__tests__/middleware/config.test.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,5 +256,124 @@ describe('Middeleware: Config', () => {
256256
setConfig({});
257257
expect(exitMock).toHaveBeenCalledWith(2);
258258
});
259+
260+
test('Falls through to global config when local config JSON is malformed', () => {
261+
const globalConfig = getGlobalConfig();
262+
const globalFile = getGlobalFile();
263+
264+
homedirMock.mockReturnValue(globalFile.globalConfigPath);
265+
const derivedGlobalConfigPath = `${globalFile.globalConfigPath}${sep}.vonage`;
266+
const derivedGlobalConfigFile = `${derivedGlobalConfigPath}${sep}config.json`;
267+
268+
mockFiles.set(
269+
derivedGlobalConfigFile,
270+
JSON.stringify(Client.transformers.kebabCaseObjectKeys(globalConfig)),
271+
);
272+
273+
const localFile = getLocalFile();
274+
process.cwd = jest.fn(() => localFile.localConfigPath);
275+
const derivedLocalConfigFile = `${localFile.localConfigPath}${sep}.vonagerc`;
276+
277+
mockFiles.set(derivedLocalConfigFile, 'not valid json {{{');
278+
279+
const args = setConfig({});
280+
281+
expect(console.error).toHaveBeenCalled();
282+
expect(args.source).toBe('Global Config File');
283+
expect(args.apiKey).toBe(globalConfig.apiKey);
284+
});
285+
286+
test('Logs error and exits when global config JSON is malformed and no other config exists', () => {
287+
const globalFile = getGlobalFile();
288+
289+
homedirMock.mockReturnValue(globalFile.globalConfigPath);
290+
const derivedGlobalConfigPath = `${globalFile.globalConfigPath}${sep}.vonage`;
291+
const derivedGlobalConfigFile = `${derivedGlobalConfigPath}${sep}config.json`;
292+
293+
mockFiles.set(derivedGlobalConfigFile, '{ bad json');
294+
295+
setConfig({});
296+
297+
expect(console.error).toHaveBeenCalled();
298+
expect(exitMock).toHaveBeenCalledWith(2);
299+
});
300+
301+
test('Normalizes kebab-case keys from local config file', () => {
302+
const localFile = getLocalFile();
303+
process.cwd = jest.fn(() => localFile.localConfigPath);
304+
const derivedLocalConfigFile = `${localFile.localConfigPath}${sep}.vonagerc`;
305+
306+
const kebabConfig = {
307+
'api-key': 'test-api-key',
308+
'api-secret': 'test-api-secret',
309+
'private-key': 'test-private-key',
310+
'app-id': 'test-app-id',
311+
};
312+
313+
mockFiles.set(derivedLocalConfigFile, JSON.stringify(kebabConfig));
314+
315+
const args = setConfig({});
316+
317+
expect(args.apiKey).toBe('test-api-key');
318+
expect(args.apiSecret).toBe('test-api-secret');
319+
expect(args.privateKey).toBe('test-private-key');
320+
expect(args.appId).toBe('test-app-id');
321+
});
322+
323+
test('Normalizes kebab-case keys from global config file', () => {
324+
const globalFile = getGlobalFile();
325+
homedirMock.mockReturnValue(globalFile.globalConfigPath);
326+
327+
const derivedGlobalConfigPath = `${globalFile.globalConfigPath}${sep}.vonage`;
328+
const derivedGlobalConfigFile = `${derivedGlobalConfigPath}${sep}config.json`;
329+
330+
const kebabConfig = {
331+
'api-key': 'global-api-key',
332+
'api-secret': 'global-api-secret',
333+
'private-key': 'global-private-key',
334+
'app-id': 'global-app-id',
335+
};
336+
337+
mockFiles.set(derivedGlobalConfigFile, JSON.stringify(kebabConfig));
338+
339+
const args = setConfig({});
340+
341+
expect(args.apiKey).toBe('global-api-key');
342+
expect(args.apiSecret).toBe('global-api-secret');
343+
expect(args.privateKey).toBe('global-private-key');
344+
expect(args.appId).toBe('global-app-id');
345+
});
346+
347+
test('Only includes provided keys in CLI config', () => {
348+
homedirMock.mockReturnValue(`${sep}dev${sep}null`);
349+
process.cwd = jest.fn(() => `${sep}dev${sep}null`);
350+
351+
const args = setConfig({ apiKey: 'my-key', apiSecret: 'my-secret' });
352+
353+
expect(args.config.cli).toEqual({
354+
apiKey: 'my-key',
355+
apiSecret: 'my-secret',
356+
source: 'CLI Arguments',
357+
});
358+
expect(args.config.cli.privateKey).toBeUndefined();
359+
expect(args.config.cli.appId).toBeUndefined();
360+
});
361+
362+
test('CLI config is empty object when no auth args are passed', () => {
363+
const globalConfig = getGlobalConfig();
364+
const globalFile = getGlobalFile();
365+
366+
homedirMock.mockReturnValue(globalFile.globalConfigPath);
367+
const derivedGlobalConfigPath = `${globalFile.globalConfigPath}${sep}.vonage`;
368+
369+
mockFiles.set(
370+
`${derivedGlobalConfigPath}${sep}config.json`,
371+
JSON.stringify(Client.transformers.kebabCaseObjectKeys(globalConfig)),
372+
);
373+
374+
const args = setConfig({ someOtherFlag: true });
375+
376+
expect(args.config.cli).toEqual({});
377+
});
259378
});
260379

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { jest, describe, test, beforeEach, expect } from '@jest/globals';
2+
import { mockConsole } from '../helpers.js';
3+
4+
const getSettingsMock = jest.fn();
5+
const setSettingMock = jest.fn();
6+
const fetchMock = jest.fn();
7+
const dumpCommandMock = jest.fn((cmd) => cmd);
8+
9+
jest.unstable_mockModule('../../src/utils/settings.js', () => ({
10+
getSettings: getSettingsMock,
11+
setSetting: setSettingMock,
12+
}));
13+
14+
jest.unstable_mockModule('node-fetch', () => ({
15+
default: fetchMock,
16+
}));
17+
18+
jest.unstable_mockModule('module', () => ({
19+
createRequire: () => () => ({ version: '1.0.0' }),
20+
}));
21+
22+
jest.unstable_mockModule('../../src/ux/dump.js', () => ({
23+
dumpCommand: dumpCommandMock,
24+
}));
25+
26+
const { checkForUpdate } = await import('../../src/middleware/update.js');
27+
28+
const makeFetchResponse = (data, ok = true) => ({
29+
ok,
30+
statusText: ok ? 'OK' : 'Not Found',
31+
json: jest.fn().mockResolvedValue(data),
32+
});
33+
34+
const today = new Date();
35+
const todayInt = parseInt(
36+
`${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`,
37+
);
38+
39+
describe('Middleware: Update', () => {
40+
beforeEach(() => {
41+
mockConsole();
42+
getSettingsMock.mockReset();
43+
setSettingMock.mockReset();
44+
fetchMock.mockReset();
45+
dumpCommandMock.mockReset();
46+
dumpCommandMock.mockImplementation((cmd) => cmd);
47+
});
48+
49+
test('Skips everything when already notified today', async () => {
50+
getSettingsMock.mockReturnValue({ lastNotified: todayInt });
51+
52+
await checkForUpdate();
53+
54+
expect(fetchMock).not.toHaveBeenCalled();
55+
expect(setSettingMock).not.toHaveBeenCalled();
56+
});
57+
58+
test('Sets lastUpdateCheck on first run and skips fetch', async () => {
59+
getSettingsMock.mockReturnValue({});
60+
61+
await checkForUpdate();
62+
63+
expect(setSettingMock).toHaveBeenCalledWith('lastUpdateCheck', expect.any(Number));
64+
expect(fetchMock).not.toHaveBeenCalled();
65+
});
66+
67+
test('Skips fetch when last check was today and no pending update', async () => {
68+
getSettingsMock.mockReturnValue({
69+
needsUpdate: false,
70+
lastUpdateCheck: todayInt,
71+
});
72+
73+
await checkForUpdate();
74+
75+
expect(fetchMock).not.toHaveBeenCalled();
76+
});
77+
78+
test('Fetches when last check was today but needsUpdate is pending', async () => {
79+
getSettingsMock.mockReturnValue({
80+
needsUpdate: true,
81+
lastUpdateCheck: todayInt,
82+
});
83+
84+
fetchMock.mockResolvedValue(
85+
makeFetchResponse({ version: '2.0.0' }),
86+
);
87+
88+
await checkForUpdate();
89+
90+
expect(fetchMock).toHaveBeenCalled();
91+
});
92+
93+
test('Fetches registry and sets needsUpdate=true when a newer version exists', async () => {
94+
getSettingsMock.mockReturnValue({
95+
needsUpdate: false,
96+
lastUpdateCheck: 20200101,
97+
});
98+
99+
fetchMock.mockResolvedValue(
100+
makeFetchResponse({ version: '2.0.0', vonageCli: { forceMinVersion: '1.0.0' } }),
101+
);
102+
103+
await checkForUpdate();
104+
105+
expect(fetchMock).toHaveBeenCalledWith(
106+
'https://registry.npmjs.org/@vonage/cli/latest',
107+
{ signal: expect.any(Object) },
108+
);
109+
expect(setSettingMock).toHaveBeenCalledWith('needsUpdate', true);
110+
expect(setSettingMock).toHaveBeenCalledWith('latestVersion', '2.0.0');
111+
});
112+
113+
test('Outputs update notification when a newer version exists', async () => {
114+
getSettingsMock.mockReturnValue({
115+
needsUpdate: false,
116+
lastUpdateCheck: 20200101,
117+
});
118+
119+
fetchMock.mockResolvedValue(
120+
makeFetchResponse({ version: '2.0.0' }),
121+
);
122+
123+
await checkForUpdate();
124+
125+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('2.0.0'));
126+
});
127+
128+
test('Sets lastNotified after showing update notification', async () => {
129+
getSettingsMock.mockReturnValue({
130+
needsUpdate: false,
131+
lastUpdateCheck: 20200101,
132+
});
133+
134+
fetchMock.mockResolvedValue(makeFetchResponse({ version: '2.0.0' }));
135+
136+
await checkForUpdate();
137+
138+
expect(setSettingMock).toHaveBeenCalledWith('lastNotified', todayInt);
139+
});
140+
141+
test('Does not output notification when already on latest version', async () => {
142+
getSettingsMock.mockReturnValue({
143+
needsUpdate: false,
144+
lastUpdateCheck: 20200101,
145+
});
146+
147+
fetchMock.mockResolvedValue(makeFetchResponse({ version: '1.0.0' }));
148+
149+
await checkForUpdate();
150+
151+
expect(console.log).not.toHaveBeenCalled();
152+
expect(setSettingMock).not.toHaveBeenCalledWith('lastNotified', expect.anything());
153+
});
154+
155+
test('Sets needsUpdate=false when already on the latest version', async () => {
156+
getSettingsMock.mockReturnValue({
157+
needsUpdate: false,
158+
lastUpdateCheck: 20200101,
159+
});
160+
161+
fetchMock.mockResolvedValue(makeFetchResponse({ version: '1.0.0' }));
162+
163+
await checkForUpdate();
164+
165+
expect(setSettingMock).toHaveBeenCalledWith('needsUpdate', false);
166+
});
167+
168+
test('Sets forceUpdate=true when installed version is below forceMinVersion', async () => {
169+
getSettingsMock.mockReturnValue({
170+
needsUpdate: false,
171+
lastUpdateCheck: 20200101,
172+
});
173+
174+
fetchMock.mockResolvedValue(
175+
makeFetchResponse({ version: '2.0.0', vonageCli: { forceMinVersion: '1.5.0' } }),
176+
);
177+
178+
await checkForUpdate();
179+
180+
expect(setSettingMock).toHaveBeenCalledWith('forceUpdate', true);
181+
});
182+
183+
test('Sets forceUpdate=false when installed version meets forceMinVersion', async () => {
184+
getSettingsMock.mockReturnValue({
185+
needsUpdate: false,
186+
lastUpdateCheck: 20200101,
187+
});
188+
189+
fetchMock.mockResolvedValue(
190+
makeFetchResponse({ version: '2.0.0', vonageCli: { forceMinVersion: '0.9.0' } }),
191+
);
192+
193+
await checkForUpdate();
194+
195+
expect(setSettingMock).toHaveBeenCalledWith('forceUpdate', false);
196+
});
197+
198+
test('Handles network errors gracefully without throwing', async () => {
199+
getSettingsMock.mockReturnValue({
200+
needsUpdate: false,
201+
lastUpdateCheck: 20200101,
202+
});
203+
204+
fetchMock.mockRejectedValue(new Error('Network failure'));
205+
206+
await expect(checkForUpdate()).resolves.toBeUndefined();
207+
expect(setSettingMock).not.toHaveBeenCalledWith('needsUpdate', expect.anything());
208+
});
209+
210+
test('Handles non-ok HTTP response gracefully without throwing', async () => {
211+
getSettingsMock.mockReturnValue({
212+
needsUpdate: false,
213+
lastUpdateCheck: 20200101,
214+
});
215+
216+
fetchMock.mockResolvedValue(makeFetchResponse({}, false));
217+
218+
await expect(checkForUpdate()).resolves.toBeUndefined();
219+
expect(setSettingMock).not.toHaveBeenCalledWith('needsUpdate', expect.anything());
220+
});
221+
222+
test('Handles missing version in registry response gracefully', async () => {
223+
getSettingsMock.mockReturnValue({
224+
needsUpdate: false,
225+
lastUpdateCheck: 20200101,
226+
});
227+
228+
fetchMock.mockResolvedValue(makeFetchResponse({ vonageCli: {} }));
229+
230+
await expect(checkForUpdate()).resolves.toBeUndefined();
231+
expect(setSettingMock).not.toHaveBeenCalledWith('needsUpdate', expect.anything());
232+
});
233+
234+
test('Updates lastUpdateCheck after a successful check', async () => {
235+
getSettingsMock.mockReturnValue({
236+
needsUpdate: false,
237+
lastUpdateCheck: 20200101,
238+
});
239+
240+
fetchMock.mockResolvedValue(makeFetchResponse({ version: '1.0.0' }));
241+
242+
await checkForUpdate();
243+
244+
expect(setSettingMock).toHaveBeenCalledWith('lastUpdateCheck', expect.any(Number));
245+
});
246+
});

bin/vonage.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,7 @@ import { dumpCommand } from '../src/ux/dump.js';
99
import { getSettings } from '../src/utils/settings.js';
1010
const settings = getSettings();
1111

12-
const { needsUpdate, forceUpdate, forceMinVersion } = settings;
13-
14-
if (needsUpdate) {
15-
const settings = getSettings();
16-
const { latestVersion } = settings;
17-
console.log(`An update is available for the CLI. Please update to version ${latestVersion}`);
18-
console.log(`Run ${dumpCommand(`npm install -g @vonage/cli@${latestVersion}`)} to update`);
19-
}
12+
const { forceUpdate, forceMinVersion } = settings;
2013
const yargsInstance = yargs(hideBin(process.argv));
2114

2215
const vonageCLI = yargsInstance.fail((_, err) => {

0 commit comments

Comments
 (0)