@@ -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