mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 06:52:07 +00:00
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>
110 lines
3.4 KiB
TypeScript
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']);
|
|
});
|
|
});
|