sotashimozono_obsidian-remo.../plugin/tests/SftpClient.kbdInteractive.test.ts
Souta b48f79afdb test: add unit tests for keyboard-interactive handler wiring (#173)
Extract `wireKeyboardInteractiveHandler()` from the inline logic in
`SftpClient.connect` so it can be tested with a plain EventEmitter
instead of mocking ssh2.Client.

Five test cases covering:
- echo:undefined normalisation to echo:false
- handler responses forwarded to finish()
- null (cancel) → finish([])
- handler error → finish([]) + warning log
- multi-prompt round-trip

Closes #173

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 22:17:10 +09:00

110 lines
3.4 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { EventEmitter } from 'events';
import { wireKeyboardInteractiveHandler } from '../src/ssh/SftpClient';
import type { KbdInteractiveHandlerFn } from '../src/ssh/SftpClient';
/**
* Emit a keyboard-interactive event on the fake client, matching ssh2's
* signature: (name, instructions, lang, prompts[], finish).
*/
function emitKbdInteractive(
client: EventEmitter,
prompts: Array<{ prompt: string; echo?: boolean }>,
): Promise<string[]> {
return new Promise((resolve) => {
const finish = (responses: string[]) => resolve(responses);
client.emit(
'keyboard-interactive',
'auth-name',
'Enter your code',
'',
prompts,
finish,
);
});
}
describe('wireKeyboardInteractiveHandler', () => {
it('normalises prompts where echo is undefined to echo: false', async () => {
const client = new EventEmitter();
const received: Array<{ prompt: string; echo: boolean }>[] = [];
const handler: KbdInteractiveHandlerFn = async (prompts) => {
received.push(prompts);
return prompts.map(() => '123456');
};
wireKeyboardInteractiveHandler(client, handler);
await emitKbdInteractive(client, [
{ prompt: 'TOTP: ' },
{ prompt: 'Password: ', echo: true },
]);
expect(received).toHaveLength(1);
expect(received[0]).toEqual([
{ prompt: 'TOTP: ', echo: false },
{ prompt: 'Password: ', echo: true },
]);
});
it('forwards handler responses to finish()', async () => {
const client = new EventEmitter();
const handler: KbdInteractiveHandlerFn = async () => ['abc', 'def'];
wireKeyboardInteractiveHandler(client, handler);
const responses = await emitKbdInteractive(client, [
{ prompt: 'Code: ' },
{ prompt: 'PIN: ' },
]);
expect(responses).toEqual(['abc', 'def']);
});
it('treats null (cancel) as empty array → finish([])', async () => {
const client = new EventEmitter();
const handler: KbdInteractiveHandlerFn = async () => null;
wireKeyboardInteractiveHandler(client, handler);
const responses = await emitKbdInteractive(client, [
{ prompt: 'Code: ' },
]);
expect(responses).toEqual([]);
});
it('catches handler errors and calls finish([]) with a warning log', async () => {
const client = new EventEmitter();
const { logger } = await import('../src/util/logger');
const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {});
const handler: KbdInteractiveHandlerFn = async () => {
throw new Error('modal crashed');
};
wireKeyboardInteractiveHandler(client, handler);
const responses = await emitKbdInteractive(client, [
{ prompt: 'Code: ' },
]);
expect(responses).toEqual([]);
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('modal crashed'),
);
warnSpy.mockRestore();
});
it('handles multi-prompt rounds correctly', async () => {
const client = new EventEmitter();
const handler: KbdInteractiveHandlerFn = async (prompts) => {
return prompts.map((_, i) => `answer-${i}`);
};
wireKeyboardInteractiveHandler(client, handler);
const responses = await emitKbdInteractive(client, [
{ prompt: 'Username: ', echo: true },
{ prompt: 'Password: ' },
{ prompt: 'TOTP: ' },
]);
expect(responses).toEqual(['answer-0', 'answer-1', 'answer-2']);
});
});