Files
opsxshell/TDD_PLAN.md
T
OpsXShell Dev 07c4a5b14f feat(proxy): HTTP/SOCKS5 代理支持,按会话独立配置 | 15 files +483/-23 | 96/96 tests passing
功能:
- 代理类型定义 (ProxyType, ProxyConfig)
- 代理工具函数 (验证、解析、构建URL) + 24 测试
- SSH 连接通过 HTTP CONNECT / SOCKS5 隧道
- 数据库 proxy 列持久化 (JSON)
- SessionDialog 代理配置 UI (可折叠面板)
- 连接池传递 proxy 到 IPC
- sessionStore 类型添加 proxy 支持
2026-04-25 05:14:27 +08:00

10 KiB
Raw Blame History

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

测试环境配置

已有配置

// 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 策略

// 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+

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

// 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+

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+

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)

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+

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+

// 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 配置

// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
  testDir: './test/e2e',
  timeout: 30000,
  use: { baseURL: 'http://localhost:5173' },
})

E2E 用例

  • 快速连接 → SSH 终端出现
  • 侧边栏会话 → 点击 → 终端连接
  • SFTP → 选择服务器 → 浏览目录
  • 标签页 → 创建 → 切换 → 关闭

测试执行

命令

npm test                           # 运行所有测试
npm test -- --coverage             # 带覆盖率报告
npm test -- test/services/         # 只跑 services 测试
npm test -- --reporter=verbose     # 详细输出
npm test -- --watch                # 监听模式

CI 集成(规划)

# .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+ | — |