Files

115 lines
5.9 KiB
TypeScript

import type { Page, WebSocketRoute } from '@playwright/test';
const fingerprint = `SHA256:${'A'.repeat(43)}`;
const host = (id: string, name: string, address: string) => ({
id, name, host: address, port: 22, username: 'root', group: '测试主机',
authMethod: 'password', initialCommand: 'echo should-not-run-in-files',
termType: 'xterm-256color', encoding: 'utf-8', fingerprint,
location: null, system: null, hasCredential: true, updatedAt: Date.now(),
});
const entry = (name: string, type = 'file', size = 12) => ({
name, type, size, mtime: 1_790_100_000, permissions: type === 'directory' ? '0755' : '0644', owner: 'root', group: 'root',
});
// 只在测试浏览器内模拟 API/协议,不访问真实主机、不增加生产认证绕过。
export async function fileFixture(page: Page, options: { firstSeen?: boolean; credentialError?: boolean; noHosts?: boolean; holdUpload?: boolean } = {}) {
const hosts = options.noHosts ? [] : [host('alpha', 'Tokyo production', '192.0.2.10'), host('beta', 'Singapore backup', '192.0.2.20')];
const directories = new Map([
['/', [entry('root', 'directory')]],
['/root', [entry('backups', 'directory'), entry('deploy', 'directory'), entry('README.md'), entry('nginx.conf', 'file', 2480)]],
['/root/backups', []],
['/root/deploy', []],
]);
const calls: Array<Record<string, any>> = [];
const sshSockets: WebSocketRoute[] = [];
const sftpSockets: WebSocketRoute[] = [];
const uploaded: Buffer[] = [];
await page.route('**/api/**', async (route) => {
const path = new URL(route.request().url()).pathname;
if (path === '/api/auth/me') return route.fulfill({ json: { account: { username: 'Administrator' }, provider: 'cloudflare' } });
if (path === '/api/session') return route.fulfill({ json: { ticket: 'test-ticket', sessionId: crypto.randomUUID() } });
if (path.endsWith('/credentials')) {
return route.fulfill(options.credentialError
? { status: 500, json: { error: '读取凭据失败,请重试。' } }
: { json: { password: 'test-only-password' } });
}
if (path === '/api/hosts') return route.fulfill({ json: { hosts } });
if (path === '/api/snippets') return route.fulfill({ json: { snippets: [] } });
return route.fulfill({ json: { host: hosts.find((item) => path.includes(item.id)) ?? hosts[0] } });
});
await page.routeWebSocket('**/api/ssh?*', (ws) => {
sshSockets.push(ws);
const ready = () => {
ws.send(JSON.stringify({ type: 'ready' }));
ws.send(JSON.stringify({ type: 'sftp_attach', url: '/api/sftp?session=test&token=test' }));
};
ws.onMessage((raw) => {
if (typeof raw !== 'string') { calls.push({ type: 'terminal-input', data: raw.toString() }); return; }
const message = JSON.parse(raw);
calls.push(message);
if (message.type === 'connect') {
if (options.firstSeen) {
// 返回变化指纹,确保仍需真实 UI 确认,而不是由测试直接放行。
ws.send(JSON.stringify({ type: 'host_key', fingerprint: `SHA256:${'B'.repeat(43)}`, expectedFingerprint: fingerprint, keyType: 'ssh-ed25519', trusted: false }));
} else ready();
}
if (message.type === 'host_key_decision' && message.accept) ready();
});
});
await page.routeWebSocket('**/api/sftp?*', (ws) => {
sftpSockets.push(ws);
let upload: { requestId: string; path: string; size: number } | undefined;
const send = (data: object) => ws.send(JSON.stringify(data));
const parent = (path: string) => path.slice(0, path.lastIndexOf('/')) || '/';
const name = (path: string) => path.slice(path.lastIndexOf('/') + 1);
ws.onMessage((raw) => {
if (typeof raw !== 'string') {
uploaded.push(Buffer.from(raw));
send({ type: 'sftp_upload_progress', requestId: upload!.requestId, loaded: uploaded.reduce((sum, chunk) => sum + chunk.length, 0) });
return;
}
const message = JSON.parse(raw);
calls.push(message);
const { type, requestId, path } = message;
if (type === 'sftp_init') send({ type: 'sftp_ready', cwd: '/root', version: 3 });
if (type === 'sftp_list') {
if (path === '/forbidden') send({ type: 'sftp_error', requestId, message: 'Permission denied' });
else send({ type: 'sftp_list_result', requestId, path, entries: directories.get(path) ?? [] });
}
if (type === 'sftp_mkdir') {
directories.get(parent(path))!.push(entry(name(path), 'directory'));
directories.set(path, []);
}
if (type === 'sftp_rename') {
directories.get(parent(message.oldPath))!.find((item) => item.name === name(message.oldPath))!.name = name(message.newPath);
}
if (type === 'sftp_delete' || type === 'sftp_rmdir') {
directories.set(parent(path), directories.get(parent(path))!.filter((item) => item.name !== name(path)));
}
if (['sftp_mkdir', 'sftp_rename', 'sftp_delete', 'sftp_rmdir'].includes(type)) send({ type: `${type}_result`, requestId });
if (type === 'sftp_upload_start') {
upload = message;
if (!options.holdUpload) send({ type: 'sftp_upload_ready', requestId });
}
if (type === 'sftp_upload_end') {
directories.get(parent(upload!.path))!.push(entry(name(upload!.path), 'file', upload!.size));
send({ type: 'sftp_upload_complete', requestId });
}
if (type === 'sftp_download') {
const data = Buffer.from('hello files\n');
send({ type: 'sftp_download_start', requestId, size: data.length });
ws.send(data);
send({ type: 'sftp_download_done', requestId, size: data.length });
}
});
});
await page.goto('/');
await page.locator('#rail-files').click();
return { hosts, calls, sshSockets, sftpSockets, uploaded, directories };
}
export async function connectFiles(page: Page, id = 'alpha') {
await page.locator('#files-host').selectOption(id);
await page.locator('#files-connect').click();
}