- Extend AppLanguage to 7 locales: zh-CN, zh-TW, en, ja, ko, vi, ru - Auto-detect system language via navigator.languages; fallback to English - Add BCP47 locale mapping for date formatting and sorting - Create 50 translation JSON files (5 languages × 10 namespaces, 1137 keys each) - Update language selector with native language names - Extend locale key parity test to cover all 7 languages (70 assertions) - Add VNC/RDP desktop, serial terminal, AI agent, port forwarding, and SFTP features - Add credential encryption, host metrics, session backup crypto, and zmodem bridge - Update Rust backend: desktop services, host metrics, SSH/SFTP enhancements
162 lines
5.9 KiB
TypeScript
162 lines
5.9 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import SFTPView from '../../src/components/sftp/SFTPView'
|
|
import { usePoolStore } from '../../src/stores/connection/poolStore'
|
|
import { useSessionStore } from '../../src/stores/sessionStore'
|
|
import { useTabStore } from '../../src/stores/tabStore'
|
|
import { mockDesktopApi } from '../setup'
|
|
|
|
const session = {
|
|
id: 'opsbase',
|
|
name: 'OpsBase',
|
|
host: 'opsbase.cn',
|
|
port: 55555,
|
|
username: 'root',
|
|
authType: 'agent' as const,
|
|
}
|
|
|
|
const remoteFile = {
|
|
filename: 'remote.txt',
|
|
longname: '-rw-r--r-- 1 root root 12 Jul 30 12:00 remote.txt',
|
|
attrs: { size: 12, mtime: 1_700_000_000, mode: 0o100644 },
|
|
}
|
|
|
|
function createDataTransfer(): DataTransfer {
|
|
const values = new Map<string, string>()
|
|
const transfer = {
|
|
effectAllowed: 'all',
|
|
dropEffect: 'none',
|
|
files: [] as unknown as FileList,
|
|
types: [] as string[],
|
|
setData(type: string, value: string) {
|
|
values.set(type, value)
|
|
if (!this.types.includes(type)) this.types.push(type)
|
|
},
|
|
getData(type: string) {
|
|
return values.get(type) || ''
|
|
},
|
|
}
|
|
return transfer as unknown as DataTransfer
|
|
}
|
|
|
|
describe('SFTP basic transfer acceptance', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
window.opsxApi = mockDesktopApi
|
|
usePoolStore.setState({ connections: new Map() })
|
|
useSessionStore.setState({ sessions: [session] })
|
|
useTabStore.setState({
|
|
tabs: [{ id: 'sftp-tab', title: 'SFTP: OpsBase', type: 'sftp', sessionId: session.id, isActive: true }],
|
|
activeTabId: 'sftp-tab',
|
|
})
|
|
vi.mocked(mockDesktopApi.sshConnect).mockResolvedValue({ success: true, connectionId: 'conn-sftp' })
|
|
vi.mocked(mockDesktopApi.sessionGetById).mockResolvedValue({ success: true, session })
|
|
vi.mocked(mockDesktopApi.sftpConnect).mockResolvedValue({ success: true, sftpId: 'sftp-native' })
|
|
vi.mocked(mockDesktopApi.sftpListDirectory).mockResolvedValue({ success: true, files: [remoteFile] })
|
|
vi.mocked(mockDesktopApi.getHomePath).mockResolvedValue({ success: true, path: '/Users/test' })
|
|
vi.mocked(mockDesktopApi.localReadDir).mockResolvedValue({ success: true, files: [] })
|
|
})
|
|
|
|
it('uploads the absolute path returned by the native file picker', async () => {
|
|
const user = userEvent.setup()
|
|
vi.mocked(mockDesktopApi.showOpenDialog).mockResolvedValueOnce({
|
|
canceled: false,
|
|
filePaths: ['/tmp/actual-selected.txt'],
|
|
})
|
|
vi.mocked(mockDesktopApi.localGetStats).mockResolvedValueOnce({
|
|
success: true,
|
|
stats: { name: 'actual-selected.txt', path: '/tmp/actual-selected.txt', type: 'file', size: 17 },
|
|
})
|
|
|
|
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
|
await screen.findByRole('button', { name: '上传' })
|
|
await user.click(screen.getByRole('button', { name: '上传' }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockDesktopApi.sftpUpload).toHaveBeenCalledWith(
|
|
'sftp-native',
|
|
'/tmp/actual-selected.txt',
|
|
'/root/actual-selected.txt'
|
|
)
|
|
})
|
|
})
|
|
|
|
it('downloads a selected remote file into the current local directory', async () => {
|
|
const user = userEvent.setup()
|
|
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
|
|
|
await screen.findByRole('button', { name: '上传' })
|
|
await user.click(await screen.findByText('remote.txt'))
|
|
await user.click(screen.getByRole('button', { name: '下载' }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockDesktopApi.sftpDownload).toHaveBeenCalledWith(
|
|
'sftp-native',
|
|
'/root/remote.txt',
|
|
'/Users/test/remote.txt'
|
|
)
|
|
})
|
|
})
|
|
|
|
it('shows the native upload failure instead of reporting success', async () => {
|
|
const user = userEvent.setup()
|
|
vi.mocked(mockDesktopApi.showOpenDialog).mockResolvedValueOnce({
|
|
canceled: false,
|
|
filePaths: ['/tmp/denied.txt'],
|
|
})
|
|
vi.mocked(mockDesktopApi.localGetStats).mockResolvedValueOnce({
|
|
success: true,
|
|
stats: { name: 'denied.txt', path: '/tmp/denied.txt', type: 'file', size: 4 },
|
|
})
|
|
vi.mocked(mockDesktopApi.sftpUpload).mockResolvedValueOnce({
|
|
success: false,
|
|
error: 'Permission denied',
|
|
})
|
|
|
|
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
|
await screen.findByRole('button', { name: '上传' })
|
|
await user.click(screen.getByRole('button', { name: '上传' }))
|
|
|
|
expect(await screen.findByText(/权限不足|Permission denied/)).toBeVisible()
|
|
})
|
|
|
|
it('uploads by dragging a local row onto the remote panel', async () => {
|
|
vi.mocked(mockDesktopApi.localReadDir).mockResolvedValue({
|
|
success: true,
|
|
files: [{ name: 'local.txt', path: '/Users/test/local.txt', type: 'file', size: 8, modified: 1_700_000_000 }],
|
|
})
|
|
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
|
await screen.findByRole('button', { name: '上传' })
|
|
|
|
const transfer = createDataTransfer()
|
|
fireEvent.dragStart(await screen.findByText('local.txt'), { dataTransfer: transfer })
|
|
fireEvent.drop(screen.getByLabelText('远程文件面板'), { dataTransfer: transfer })
|
|
|
|
await waitFor(() => {
|
|
expect(mockDesktopApi.sftpUpload).toHaveBeenCalledWith(
|
|
'sftp-native',
|
|
'/Users/test/local.txt',
|
|
'/root/local.txt'
|
|
)
|
|
})
|
|
})
|
|
|
|
it('downloads by dragging a remote row onto the local panel', async () => {
|
|
render(<SFTPView tabId="sftp-tab" sessionId={session.id} />)
|
|
await screen.findByRole('button', { name: '上传' })
|
|
|
|
const transfer = createDataTransfer()
|
|
fireEvent.dragStart(await screen.findByText('remote.txt'), { dataTransfer: transfer })
|
|
fireEvent.drop(screen.getByLabelText('本地文件面板'), { dataTransfer: transfer })
|
|
|
|
await waitFor(() => {
|
|
expect(mockDesktopApi.sftpDownload).toHaveBeenCalledWith(
|
|
'sftp-native',
|
|
'/root/remote.txt',
|
|
'/Users/test/remote.txt'
|
|
)
|
|
})
|
|
})
|
|
})
|