功能: - 代理类型定义 (ProxyType, ProxyConfig) - 代理工具函数 (验证、解析、构建URL) + 24 测试 - SSH 连接通过 HTTP CONNECT / SOCKS5 隧道 - 数据库 proxy 列持久化 (JSON) - SessionDialog 代理配置 UI (可折叠面板) - 连接池传递 proxy 到 IPC - sessionStore 类型添加 proxy 支持
408 lines
10 KiB
Markdown
408 lines
10 KiB
Markdown
# TDD_PLAN.md - 测试驱动开发计划
|
||
|
||
本文档规划 OpsXShell 的测试策略,覆盖从单元测试到 E2E 测试的完整体系。
|
||
|
||
---
|
||
|
||
## 当前状态
|
||
|
||
||| 指标 | 当前值 | 目标值 ||
|
||
||------|--------|--------|--
|
||
|| 测试文件数 | 6 | 15+ ||
|
||
|| 测试用例数 | 96 | 150+ ||
|
||
|| 估算覆盖率 | ~25% | ≥50% ||
|
||
|| 核心模块覆盖 | 60% | ≥80% ||
|
||
|
||
### 已有测试
|
||
|
||
||| 测试文件 | 用例数 | 覆盖模块 | 质量 ||
|
||
||----------|--------|----------|------|
|
||
|| `test/stores/tabStore.test.ts` | 14 | tabStore | ✅ 完整 ||
|
||
|| `test/stores/connectionPool.test.ts` | 8 | poolStore | 🟡 仅接口验证 ||
|
||
|| `test/components/sftp/utils.test.ts` | 16 | sftp/utils | ✅ 完整 ||
|
||
|| `test/services/crypto.test.ts` | 11 | crypto | ✅ 完整 ||
|
||
|| `test/stores/keybindingStore.test.ts` | 23 | keybindingStore | ✅ 完整 ||
|
||
|| `test/utils/proxy.test.ts` | 24 | proxy utils | ✅ 完整 ||
|
||
|
||
---
|
||
|
||
## 测试策略
|
||
|
||
```
|
||
v1.3.0 测试计划
|
||
├── P0: 核心安全模块(crypto, ssh-service)
|
||
├── P1: 数据层(database, error-handler, stores)
|
||
├── P1: IPC 通信层(8 个 IPC Handler)
|
||
├── P2: UI 层(TerminalView, useSFTP hooks)
|
||
└── P3: E2E 集成(Playwright)
|
||
```
|
||
|
||
### 原则
|
||
|
||
1. **安全先行**: 加密/解密模块必须 100% 测试覆盖
|
||
2. **核心优先**: SSH 连接逻辑是应用核心,优先覆盖
|
||
3. **可 mock 性**: 所有外部依赖(ssh2, better-sqlite3, electron)必须可 mock
|
||
4. **独立性**: 每个测试独立运行,不依赖外部状态
|
||
5. **快速反馈**: 单元测试 < 5s,集成测试 < 30s
|
||
|
||
---
|
||
|
||
## 测试环境配置
|
||
|
||
### 已有配置
|
||
|
||
```typescript
|
||
// vitest.config.ts
|
||
export default defineConfig({
|
||
test: {
|
||
globals: true,
|
||
environment: 'jsdom',
|
||
setupFiles: ['./test/setup.ts'],
|
||
coverage: {
|
||
provider: 'v8',
|
||
reporter: ['text', 'html', 'lcov'],
|
||
thresholds: {
|
||
statements: 50, // 当前远未达到
|
||
branches: 40,
|
||
functions: 50,
|
||
lines: 50,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
```
|
||
|
||
### Mock 策略
|
||
|
||
```typescript
|
||
// test/setup.ts 已有:
|
||
// - window.electronAPI 完整 mock
|
||
// - localStorage mock
|
||
|
||
// 需要新增:
|
||
// - ssh2 Client mock
|
||
// - better-sqlite3 mock
|
||
// - zmodem-ts mock
|
||
```
|
||
|
||
---
|
||
|
||
## P0: 核心模块测试(第一批)
|
||
|
||
### 1. crypto.ts 测试 — 加密/解密正确性
|
||
|
||
**文件**: `test/services/crypto.test.ts`
|
||
**用例目标**: 15+
|
||
|
||
```typescript
|
||
describe('Crypto Service', () => {
|
||
describe('encrypt / decrypt', () => {
|
||
it('应正确加密并解密明文字符串')
|
||
it('应正确加密并解密空字符串')
|
||
it('应正确加密并解密特殊字符和 Unicode')
|
||
it('每次加密应生成不同的密文(随机 IV)')
|
||
it('密文篡改后解密应失败(Auth Tag 验证)')
|
||
it('错误密钥解密应失败')
|
||
})
|
||
|
||
describe('deriveKey', () => {
|
||
it('相同密码和 salt 应派生相同密钥')
|
||
it('不同 salt 应派生不同密钥')
|
||
it('应使用 100,000 次 PBKDF2 迭代')
|
||
})
|
||
|
||
describe('generateSalt / generateIV', () => {
|
||
it('salt 应为 32 字节随机值')
|
||
it('IV 应为 12 字节随机值(GCM 推荐)')
|
||
it('每次生成的值应不同')
|
||
})
|
||
})
|
||
```
|
||
|
||
### 2. ssh-service.ts 测试 — SSH 连接逻辑
|
||
|
||
**文件**: `test/services/ssh-service.test.ts`
|
||
**用例目标**: 20+
|
||
**Mock**: ssh2 Client
|
||
|
||
```typescript
|
||
// Mock ssh2
|
||
vi.mock('ssh2', () => ({
|
||
Client: vi.fn().mockImplementation(() => ({
|
||
connect: vi.fn(),
|
||
on: vi.fn(),
|
||
end: vi.fn(),
|
||
shell: vi.fn(),
|
||
forwardOut: vi.fn(),
|
||
forwardIn: vi.fn(),
|
||
sftp: vi.fn(),
|
||
})),
|
||
}))
|
||
|
||
describe('SSH Service', () => {
|
||
describe('connect', () => {
|
||
it('应使用密码认证连接')
|
||
it('应使用私钥认证连接')
|
||
it('应使用 SSH Agent 认证连接')
|
||
it('连接失败应返回错误')
|
||
it('连接超时应返回超时错误')
|
||
it('重复连接同一主机应复用')
|
||
})
|
||
|
||
describe('createShell', () => {
|
||
it('应创建交互式 Shell')
|
||
it('应传入终端尺寸 cols/rows')
|
||
it('Shell 创建失败应返回错误')
|
||
})
|
||
|
||
describe('disconnect', () => {
|
||
it('应正确关闭连接')
|
||
it('应清理所有 Shell 会话')
|
||
it('应清理端口转发')
|
||
})
|
||
|
||
describe('portForward', () => {
|
||
it('应创建本地端口转发')
|
||
it('应创建远程端口转发')
|
||
it('应创建 SOCKS5 动态代理')
|
||
it('端口被占用应返回错误')
|
||
})
|
||
|
||
describe('connectionPool', () => {
|
||
it('不应超过最大连接数限制')
|
||
it('空闲连接应被自动回收')
|
||
it('LRU 淘汰策略应正确工作')
|
||
})
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## P1: 数据层 + Store 测试(第二批)
|
||
|
||
### 3. database-service.ts 测试
|
||
|
||
**文件**: `test/services/database.test.ts`
|
||
**用例目标**: 15+
|
||
|
||
```typescript
|
||
describe('Database Service', () => {
|
||
describe('Session CRUD', () => {
|
||
it('应创建新会话并返回 ID')
|
||
it('应获取所有会话列表')
|
||
it('应根据 ID 获取单个会话')
|
||
it('应更新会话字段')
|
||
it('应删除会话')
|
||
it('密码字段应加密存储')
|
||
it('读取时密码字段应解密')
|
||
})
|
||
|
||
describe('Settings', () => {
|
||
it('应保存和读取设置')
|
||
it('未设置的键应返回默认值')
|
||
it('应支持复杂对象类型设置')
|
||
})
|
||
|
||
describe('数据完整性', () => {
|
||
it('数据库文件不存在应自动创建')
|
||
it('Schema 版本升级应自动迁移')
|
||
})
|
||
})
|
||
```
|
||
|
||
### 4. error-handler.ts 测试
|
||
|
||
**文件**: `test/utils/error-handler.test.ts`
|
||
**用例目标**: 10+
|
||
|
||
```typescript
|
||
describe('Error Handler', () => {
|
||
describe('inferErrorCode', () => {
|
||
it('应从 ECONNREFUSED 推断连接错误')
|
||
it('应从 ETIMEDOUT 推断超时错误')
|
||
it('应从 auth 推断认证错误')
|
||
it('应从 ENOENT 推断文件不存在错误')
|
||
it('未知错误应返回通用错误码')
|
||
})
|
||
|
||
describe('withErrorHandling', () => {
|
||
it('应包装异步函数并捕获错误')
|
||
it('应返回统一错误格式')
|
||
})
|
||
})
|
||
```
|
||
|
||
### 5. poolStore 测试增强
|
||
|
||
**文件**: `test/stores/connectionPool.test.ts`
|
||
**用例目标**: 15+ (新增 8)
|
||
|
||
```typescript
|
||
describe('Connection Pool Store', () => {
|
||
// 已有: 基本接口验证
|
||
// 新增:
|
||
it('应拒绝超过最大连接数的新连接')
|
||
it('LRU 淘汰应移除最久未使用的连接')
|
||
it('后台连接超过最大数应断开最旧')
|
||
it('活跃连接不应被 LRU 淘汰')
|
||
it('连接状态机转换应完整覆盖')
|
||
it('挂起超时 30 分钟应彻底断开')
|
||
it('保活心跳应重置空闲计时器')
|
||
})
|
||
```
|
||
|
||
### 6. lifecycleStore 测试
|
||
|
||
**文件**: `test/stores/lifecycleStore.test.ts`
|
||
**用例目标**: 10+
|
||
|
||
---
|
||
|
||
## P1: IPC Handler 测试(第三批)
|
||
|
||
### 7. SSH IPC 测试
|
||
|
||
**文件**: `test/ipc/ssh.ipc.test.ts`
|
||
**用例目标**: 8+
|
||
|
||
```typescript
|
||
describe('SSH IPC Handlers', () => {
|
||
it('ssh:connect 应调用 sshService.connect')
|
||
it('ssh:disconnect 应调用 sshService.disconnect')
|
||
it('ssh:create-shell 应调用 sshService.createShell')
|
||
it('ssh:shell-write 应调用 sshService.shellWrite')
|
||
it('ssh:shell-resize 应调用 sshService.shellResize')
|
||
it('ssh:forward-local 应调用 sshService.forwardLocal')
|
||
it('ssh:forward-remote 应调用 sshService.forwardRemote')
|
||
it('无效参数应返回错误')
|
||
})
|
||
```
|
||
|
||
### 8. SFTP IPC 测试
|
||
|
||
**文件**: `test/ipc/sftp.ipc.test.ts`
|
||
**用例目标**: 8+
|
||
|
||
### 9. Session IPC 测试
|
||
|
||
**文件**: `test/ipc/session.ipc.test.ts`
|
||
**用例目标**: 6+
|
||
|
||
### 10. Settings IPC 测试
|
||
|
||
**文件**: `test/ipc/settings.ipc.test.ts`
|
||
**用例目标**: 4+
|
||
|
||
### 11. 其他 IPC 测试
|
||
|
||
**文件**: `test/ipc/{dialog,file,zmodem,app}.ipc.test.ts`
|
||
**用例目标**: 每个文件 3-5+
|
||
|
||
---
|
||
|
||
## P2: UI 层测试(第四批)
|
||
|
||
### 12. TerminalView 组件测试
|
||
|
||
**文件**: `test/components/TerminalView.test.tsx`
|
||
**用例目标**: 10+
|
||
|
||
```typescript
|
||
// Mock xterm
|
||
vi.mock('@xterm/xterm', () => ({
|
||
Terminal: vi.fn().mockImplementation(() => ({
|
||
onData: vi.fn(), onResize: vi.fn(), open: vi.fn(),
|
||
write: vi.fn(), dispose: vi.fn(), cols: 80, rows: 24,
|
||
})),
|
||
}))
|
||
|
||
describe('TerminalView', () => {
|
||
it('应渲染终端容器')
|
||
it('应显示连接中状态')
|
||
it('连接成功应初始化 XTerm')
|
||
it('连接失败应显示错误信息')
|
||
it('Shell 数据应写入终端')
|
||
it('终端输入应发送到 Shell')
|
||
it('窗口调整应 resize 终端')
|
||
it('组件卸载应释放资源')
|
||
})
|
||
```
|
||
|
||
### 13. useSFTP Hooks 测试
|
||
|
||
**文件**: `test/hooks/useSFTP.test.ts`
|
||
**用例目标**: 15+
|
||
|
||
### 14. 其他组件测试
|
||
|
||
- `test/components/Sidebar.test.tsx` — 5+
|
||
- `test/components/SessionDialog.test.tsx` — 5+
|
||
- `test/components/SFTPView.test.tsx` — 5+
|
||
|
||
---
|
||
|
||
## P3: E2E 集成测试(远期)
|
||
|
||
### Playwright 配置
|
||
|
||
```typescript
|
||
// playwright.config.ts
|
||
import { defineConfig } from '@playwright/test'
|
||
export default defineConfig({
|
||
testDir: './test/e2e',
|
||
timeout: 30000,
|
||
use: { baseURL: 'http://localhost:5173' },
|
||
})
|
||
```
|
||
|
||
### E2E 用例
|
||
|
||
- 快速连接 → SSH 终端出现
|
||
- 侧边栏会话 → 点击 → 终端连接
|
||
- SFTP → 选择服务器 → 浏览目录
|
||
- 标签页 → 创建 → 切换 → 关闭
|
||
|
||
---
|
||
|
||
## 测试执行
|
||
|
||
### 命令
|
||
|
||
```bash
|
||
npm test # 运行所有测试
|
||
npm test -- --coverage # 带覆盖率报告
|
||
npm test -- test/services/ # 只跑 services 测试
|
||
npm test -- --reporter=verbose # 详细输出
|
||
npm test -- --watch # 监听模式
|
||
```
|
||
|
||
### CI 集成(规划)
|
||
|
||
```yaml
|
||
# .github/workflows/test.yml
|
||
- name: Test
|
||
run: npm test -- --coverage
|
||
- name: Coverage Check
|
||
run: |
|
||
if [ $(npm test -- --coverage --reporter=json 2>/dev/null | jq '.total.lines.pct') -lt 50 ]; then
|
||
echo "Coverage below 50%"
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
---
|
||
|
||
## 覆盖率追踪
|
||
|
||
|| 版本 | 总覆盖率 | 核心模块 | 用例数 | 日期 |
|
||
|------|----------|----------|--------|------|
|
||
| v1.0.0 | ~10% | 0% | 34 | 2026-04-06 |
|
||
| v1.1.0 | ~12% | 0% | 34 | — |
|
||
| v1.2.0 | ~15% | 0% | 40 | — |
|
||
| v1.3.0 | ~20% | 60% | 49 | 2026-04-24 |
|
||
| v1.4.0-batch1 | ~25% | 60% | 72 | 2026-04-24 |
|
||
| v1.4.0-proxy | ~25% | 65% | 96 | 2026-04-25 |
|
||
| v1.3.0-batch2 | ~35% | 60% | 104 | — |
|
||
| v1.3.0-batch3 | ~45% | 70% | 143 | — |
|
||
| v1.3.0-final | **≥50%** | **≥80%** | **150+** | — |
|