Skip to content

Commit 8b2a2ec

Browse files
Merge pull request #116 from Yuvraj-Sarathe/config
Config
2 parents 143bbcd + b88837d commit 8b2a2ec

3 files changed

Lines changed: 256 additions & 102 deletions

File tree

src/__tests__/config.test.ts

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,72 @@ describe('config command', () => {
8989
expect(webhookPrompt.validate('not-a-webhook')).toBe('Must be a valid Discord webhook URL (including ID and Token)');
9090
});
9191

92+
it('should detect existing config and prompt for reconfiguration — cancel keeps current config', async () => {
93+
vi.mocked(configUtils.getConfig).mockReturnValue({ notification_service: 'discord' });
94+
vi.mocked(tui.select).mockResolvedValueOnce('no'); // decline reconfiguration
95+
96+
await program.parseAsync(['node', 'test', 'config', 'setup']);
97+
98+
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Current notification service is set to'));
99+
expect(tui.select).toHaveBeenCalledTimes(1); // only reconfiguration prompt
100+
expect(configUtils.setConfig).not.toHaveBeenCalled();
101+
});
102+
103+
it('should proceed with setup when user confirms reconfiguration', async () => {
104+
vi.mocked(configUtils.getConfig).mockReturnValue({ notification_service: 'discord' });
105+
vi.mocked(tui.select)
106+
.mockResolvedValueOnce('yes') // confirm reconfiguration
107+
.mockResolvedValueOnce('discord'); // select discord service
108+
vi.mocked(tui.input).mockResolvedValue('https://discord.com/api/webhooks/123456789/token-here');
109+
110+
await program.parseAsync(['node', 'test', 'config', 'setup']);
111+
112+
expect(tui.select).toHaveBeenCalledTimes(2); // reconfiguration + service selection
113+
expect(configUtils.setConfig).toHaveBeenCalledWith('notification_service', 'discord');
114+
expect(configUtils.setConfig).toHaveBeenCalledWith('discord_webhook', 'https://discord.com/api/webhooks/123456789/token-here');
115+
});
116+
117+
it('should skip reconfiguration prompt when no existing config', async () => {
118+
vi.mocked(configUtils.getConfig).mockReturnValue({});
119+
vi.mocked(tui.select).mockResolvedValue('none');
120+
121+
await program.parseAsync(['node', 'test', 'config', 'setup']);
122+
123+
expect(consoleLogSpy).not.toHaveBeenCalledWith(
124+
expect.stringContaining('Current notification service is set to'),
125+
);
126+
expect(tui.select).toHaveBeenCalledTimes(1); // only service selection
127+
expect(configUtils.setConfig).toHaveBeenCalledWith('notification_service', 'none');
128+
});
129+
130+
it('should handle select rejection gracefully during setup', async () => {
131+
vi.mocked(tui.select).mockRejectedValueOnce(new Error('select failed'));
132+
133+
await program.parseAsync(['node', 'test', 'config', 'setup']);
134+
135+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('select failed'));
136+
expect(configUtils.setConfig).not.toHaveBeenCalled();
137+
});
138+
139+
it('should handle setConfig failure gracefully during setup', async () => {
140+
vi.mocked(tui.select).mockResolvedValue('discord');
141+
vi.mocked(tui.input).mockResolvedValue('https://discord.com/api/webhooks/123456789/token-here');
142+
// Use mockImplementationOnce to avoid polluting subsequent tests
143+
vi.mocked(configUtils.setConfig).mockImplementationOnce(() => { throw new Error('write failed'); });
144+
145+
await program.parseAsync(['node', 'test', 'config', 'setup']);
146+
147+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('write failed'));
148+
});
149+
92150
it('should call select, multiple inputs and setConfig on email setup without password', async () => {
93151
vi.mocked(tui.select).mockResolvedValue('email');
94152
vi.mocked(tui.input)
95153
.mockResolvedValueOnce('smtp.gmail.com') // host
96154
.mockResolvedValueOnce('587') // port
97155
.mockResolvedValueOnce('user@test.com') // user
98-
.mockResolvedValueOnce('to@test.com'); // to
156+
.mockResolvedValueOnce('to@test.com') // to
157+
.mockResolvedValueOnce(''); // password (empty)
99158

100159
await program.parseAsync(['node', 'test', 'config', 'setup']);
101160

@@ -112,41 +171,78 @@ describe('config command', () => {
112171
expect(guideOrder).toBeLessThan(firstInputOrder());
113172
});
114173

115-
it('should not ask for or persist SMTP password during email setup', async () => {
116-
vi.mocked(tui.select).mockResolvedValue('email');
117-
vi.mocked(tui.input)
118-
.mockResolvedValueOnce('smtp.gmail.com') // host
119-
.mockResolvedValueOnce('587') // port
120-
.mockResolvedValueOnce('user@test.com') // user
121-
.mockResolvedValueOnce('to@test.com'); // to
174+
it('should save email_password if provided during email setup', async () => {
175+
vi.mocked(tui.select).mockResolvedValue('email');
176+
vi.mocked(tui.input)
177+
.mockResolvedValueOnce('smtp.gmail.com') // host
178+
.mockResolvedValueOnce('587') // port
179+
.mockResolvedValueOnce('user@test.com') // user
180+
.mockResolvedValueOnce('to@test.com') // to
181+
.mockResolvedValueOnce('pass123'); // password
122182

123-
await program.parseAsync(['node', 'test', 'config', 'setup']);
183+
await program.parseAsync(['node', 'test', 'config', 'setup']);
124184

125-
expect(tui.input).toHaveBeenCalledTimes(4);
126-
expect(configUtils.setConfig).not.toHaveBeenCalledWith('email_password', expect.any(String));
127-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('KDM_SMTP_PASSWORD'));
128-
});
185+
// Find the call that passed 'email_password' instead of assuming index
186+
const passwordCall = vi.mocked(configUtils.setConfig).mock.calls.find(
187+
call => call[0] === 'email_password'
188+
);
189+
expect(passwordCall).toBeDefined();
190+
expect(passwordCall?.[1]).toBe('pass123');
191+
});
129192

130-
it('should require an SMTP host during email setup', async () => {
131-
vi.mocked(tui.select).mockResolvedValue('email');
132-
vi.mocked(tui.input)
133-
.mockResolvedValueOnce('smtp.gmail.com')
134-
.mockResolvedValueOnce('587')
135-
.mockResolvedValueOnce('user@test.com')
136-
.mockResolvedValueOnce('to@test.com');
193+
it('should require an SMTP host during email setup and validate optional SMTP password', async () => {
194+
vi.mocked(tui.select).mockResolvedValue('email');
195+
vi.mocked(tui.input)
196+
.mockResolvedValueOnce('smtp.gmail.com')
197+
.mockResolvedValueOnce('587')
198+
.mockResolvedValueOnce('user@test.com')
199+
.mockResolvedValueOnce('to@test.com')
200+
.mockResolvedValueOnce('');
137201

138-
await program.parseAsync(['node', 'test', 'config', 'setup']);
202+
await program.parseAsync(['node', 'test', 'config', 'setup']);
139203

140-
const smtpHostPrompt = vi.mocked(tui.input).mock.calls[0][0];
141-
expect(smtpHostPrompt.validate('')).toBe('Host is required');
142-
});
204+
const smtpHostPrompt = vi.mocked(tui.input).mock.calls[0][0];
205+
expect(smtpHostPrompt.validate('')).toBe('Host is required');
206+
207+
// Find the password prompt by looking for the last input call
208+
const passwordPromptIndex = vi.mocked(tui.input).mock.calls.length - 1;
209+
const smtpPasswordPrompt = vi.mocked(tui.input).mock.calls[passwordPromptIndex][0];
210+
expect(smtpPasswordPrompt.validate('')).toBe(true);
211+
expect(smtpPasswordPrompt.validate('anything')).toBe(true);
212+
});
143213

144214
it('should call setConfig on config set', async () => {
145215
await program.parseAsync(['node', 'test', 'config', 'set', 'alert_email', 'test@test.com']);
146216
expect(configUtils.setConfig).toHaveBeenCalledWith('alert_email', 'test@test.com');
147217
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Set alert_email to test@test.com'));
148218
});
149219

220+
it('should show deprecation warning when setting credential key via config set', async () => {
221+
await program.parseAsync(['node', 'test', 'config', 'set', 'discord_webhook', 'https://discord.com/api/webhooks/test']);
222+
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Deprecation warning'));
223+
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('kdm config setup'));
224+
expect(configUtils.setConfig).toHaveBeenCalled(); // still executes (soft deprecation)
225+
});
226+
227+
it('should show deprecation warning for all credential keys', async () => {
228+
const credentialKeys = ['notification_service', 'discord_webhook', 'email_host', 'email_port', 'email_user', 'email_to'];
229+
for (const key of credentialKeys) {
230+
vi.clearAllMocks();
231+
// email_port gets parsed to int by the existing handler logic
232+
const testValue = key === 'email_port' ? '587' : 'test-value';
233+
const expectedValue = key === 'email_port' ? 587 : testValue;
234+
await program.parseAsync(['node', 'test', 'config', 'set', key, testValue]);
235+
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Deprecation warning'));
236+
expect(configUtils.setConfig).toHaveBeenCalledWith(key, expectedValue);
237+
}
238+
});
239+
240+
it('should not show deprecation warning for non-credential keys', async () => {
241+
await program.parseAsync(['node', 'test', 'config', 'set', 'alert_cooldown', '300']);
242+
expect(consoleLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('Deprecation warning'));
243+
expect(configUtils.setConfig).toHaveBeenCalledWith('alert_cooldown', 300);
244+
});
245+
150246
it('should parse integer for alert_cooldown', async () => {
151247
await program.parseAsync(['node', 'test', 'config', 'set', 'alert_cooldown', '123']);
152248
expect(configUtils.setConfig).toHaveBeenCalledWith('alert_cooldown', 123);

0 commit comments

Comments
 (0)