Files
outlookEmailPlus/docs/TEST/TEST-00005-多邮箱统一管理-测试文档.md
T
胡绍康andClaude Opus 4.6 64d8a6c357 feat(multi-mailbox): 实现多邮箱统一管理功能(PRD-00005)
核心变更:
- 数据库:Schema v3,accounts 表新增 account_type/provider/imap_host/imap_port/imap_password 字段
- 后端:新增通用 IMAP 服务(imap_generic.py)与邮箱提供商配置(providers.py)
- 导入:支持 Outlook/QQ/163/Gmail/自定义 IMAP 多格式批量导入,兼容注释行与导出格式
- 导出:按邮箱类型分组导出,保留 provider 信息便于重新导入
- 邮件读取:IMAP 账号走通用 IMAP 服务,Outlook 保持原 Graph/XOAUTH2 链路
- 验证码提取:IMAP 账号通过 IMAP 获取邮件详情后提取
- 安全约束:IMAP 账号禁用远程删除,敏感字段(imap_password)加密存储
- 前端:新增邮箱类型下拉、自定义 IMAP 配置区、provider 标签显示
- 调度器:定时刷新仅处理 Outlook 账号(IMAP 无 OAuth token 刷新语义)
- 日志:配置 outlook_web 命名空间日志输出到 stderr

技术细节:
- 保持 Outlook 旧格式完全兼容(四段式:email----password----client_id----refresh_token)
- IMAP 账号使用空字符串占位 client_id/refresh_token(满足 DB NOT NULL 约束)
- 支持导出格式智能识别(5 段 custom、3 段 provider、2 段配合输入框)
- 前端 DOMPurify 未加载时回退为基本 HTML 过滤(CDN 不可达场景)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:54:39 +08:00

23 KiB
Raw Blame History

TEST-00005|多邮箱统一管理 — 测试文档

  • 文档编号: TEST-00005
  • 创建日期: 2026-03-02
  • 版本: V1.0
  • 状态: 草案
  • 对齐 PRD: docs/PRD/PRD-00005-多邮箱统一管理.md
  • 对齐 FD: docs/FD/FD-00005-多邮箱统一管理.md
  • 对齐 TDD: docs/TDD/TDD-00005-多邮箱统一管理.md
  • 测试文件: tests/test_multi_mailbox.py

目录

  1. 测试概述
  2. 测试策略
  3. 测试环境配置
  4. P0 — 数据库迁移测试
  5. P0 — providers 模块单元测试
  6. P0 — imap_generic 服务测试
  7. P0 — Repository IMAP 账号 CRUD 测试
  8. P0 — Controller emails 路由分发测试
  9. P0 — Controller accounts IMAP 接口测试
  10. P0 — Scheduler 过滤测试
  11. P0 — Outlook 功能回归测试
  12. P1 — 导出格式测试
  13. P2 — IMAP 文件夹映射测试
  14. 手动验收测试清单
  15. 测试数据准备
  16. 验收标准与门禁

1. 测试概述

1.1 测试目标

本测试文档覆盖"多邮箱统一管理"功能的完整验证,确保:

  1. 新功能正确性

    • DB Schema v3 迁移幂等、不破坏旧数据
    • IMAP 通用服务能正确认证、拉取邮件、返回统一格式
    • 按 account_type 路由分发逻辑准确
    • IMAP 账号的敏感字段加解密正确
    • 定时刷新任务只处理 Outlook 账号
  2. Outlook 流程零回归

    • 现有 Graph API → IMAP XOAUTH2 回退链不受影响
    • 现有批量导入格式(4 段)仍正常
    • 现有邮件删除、验证码提取、账号导出不受影响
  3. 安全约束满足

    • IMAP 密码在 DB 中以加密形式存储
    • 错误消息中不暴露 imap_password
    • 导出二次验证机制不受影响

1.2 测试范围

包含:

  • 数据库迁移(单元/集成)
  • providers.py 纯单元测试
  • imap_generic.py 单元测试(mock IMAP 连接)
  • Repository CRUD 集成测试
  • Controller API 集成测试(mock 服务层)
  • Scheduler 查询过滤验证
  • Outlook 全量回归(集成)
  • 手动端到端测试

不包含:

  • 真实 IMAP 服务器连通性测试(需要外部网络)
  • 前端自动化 E2E 测试(Selenium/Playwright)
  • 性能压测

1.3 测试分类

类型 工具 覆盖层
单元测试 unittest + unittest.mock providers.py / imap_generic.py
集成测试 Flask test client + 临时 SQLite DB Controller / Repository / DB
回归测试 现有测试套件(不改动) 所有 Outlook 功能
手动测试 浏览器 + curl 前端交互 / 真实 IMAP

2. 测试策略

2.1 核心测试原则

  1. 隔离 IMAP 网络调用:所有涉及 imaplib.IMAP4_SSL 的测试均通过 unittest.mock.patch 模拟,不发起真实网络请求。
  2. 真实 DB 操作:Repository 和 Controller 测试使用临时 SQLite 文件(_import_app.py 已处理),验证实际 SQL 行为。
  3. 最小改动原则:新增测试类,不修改现有测试文件,确保回归测试基线不变。
  4. 按 account_type 分支测试:每个 Controller 路由测试都要包含 Outlook 和 IMAP 两个分支。

2.2 Mock 策略

被 Mock 对象 路径 用途
imaplib.IMAP4_SSL outlook_web.services.imap_generic.imaplib.IMAP4_SSL 模拟 IMAP 连接成功/失败
imap_generic.get_emails_imap_generic outlook_web.controllers.emails.get_emails_imap_generic Controller 路由分发测试
imap_generic.get_email_detail_imap_generic outlook_web.controllers.emails.get_email_detail_imap_generic 验证码提取测试
graph.get_emails outlook_web.services.graph.get_emails Outlook 回归 mock

3. 测试环境配置

3.1 运行方式

# 运行所有多邮箱测试
python -m unittest tests.test_multi_mailbox -v

# 运行全量测试套件(含回归)
python -m unittest discover -s tests -v

3.2 依赖

  • Python 标准库:unittest、unittest.mock、sqlite3、tempfile、imaplib
  • 项目依赖:outlook_web.db、outlook_web.security.crypto、outlook_web.repositories.accounts
  • 新增依赖(实现后):outlook_web.services.providers、outlook_web.services.imap_generic

4. P0 — 数据库迁移测试

TC-DB-01:新建数据库包含 5 个 IMAP 新列

属性 值
测试 ID TC-DB-01
优先级 P0
前置条件 全新临时数据库(无历史数据)
操作步骤 调用 init_db(temp_path),然后执行 PRAGMA table_info(accounts)
预期结果 accounts 表包含:account_type、provider、imap_host、imap_port、imap_password

TC-DB-02:旧数据库增量迁移(幂等)

属性 值
测试 ID TC-DB-02
优先级 P0
前置条件 已有 accounts 表但不含 5 个新列;表中有 1 条旧 Outlook 账号数据
操作步骤 1. 创建旧 schema DB → 2. 插入旧账号 → 3. 调用 init_db(path) → 4. 查询列和数据
预期结果 5 个新列已添加;旧账号数据完整保留;account_type 列对旧记录为 NULL 或 'outlook'(SQLite ADD COLUMN DEFAULT 行为:旧行得到 NULL)

TC-DB-03:迁移幂等性(重复执行不报错)

属性 值
测试 ID TC-DB-03
优先级 P0
操作步骤 对同一数据库连续调用 init_db(path) 两次
预期结果 第二次调用不报错,列不重复

TC-DB-04:Schema 版本升级为 3

属性 值
测试 ID TC-DB-04
优先级 P0
操作步骤 init_db(path) 完成后,查询 settings 表中 db_schema_version 的值
预期结果 值为 '3'

TC-DB-05:imap_password 加密迁移

属性 值
测试 ID TC-DB-05
优先级 P0
前置条件 已有 IMAP 账号,imap_password 列存储的是明文字符串
操作步骤 调用 migrate_sensitive_data() 或 init_db() → 查询该账号的 imap_password
预期结果 imap_password 为加密格式(is_encrypted() 返回 True)

5. P0 — providers 模块单元测试

TC-PROV-01:get_imap_folder_candidates — 已知 provider

属性 值
测试 ID TC-PROV-01
优先级 P0
测试数据 provider='qq', folder='junkemail'
预期结果 返回 ["Junk", "&V4NXPpCuTvY-"]

TC-PROV-02:get_imap_folder_candidates — 未知 provider 退回 _default

属性 值
测试 ID TC-PROV-02
优先级 P0
测试数据 provider='unknown_xyz', folder='inbox'
预期结果 返回 _default 中 inbox 的候选列表 ["INBOX"]

TC-PROV-03:get_imap_folder_candidates — 所有已知 provider 的 inbox 必有候选

属性 值
测试 ID TC-PROV-03
优先级 P0
操作步骤 遍历所有已知 provider(gmail/qq/163/yahoo),对 folder='inbox' 调用函数
预期结果 每个 provider 返回非空列表,且必包含 "INBOX"

TC-PROV-04:get_provider_list — 返回 8 个 provider,outlook 在第一位

属性 值
测试 ID TC-PROV-04
优先级 P0
预期结果 列表长度为 8;第一个元素的 key 为 'outlook';最后一个为 'custom'

TC-PROV-05:get_provider_list — 每个元素包含必要字段

属性 值
测试 ID TC-PROV-05
优先级 P0
预期结果 每个元素包含 key、label、account_type、note 字段;outlook 的 account_type 为 'outlook';其余 IMAP provider 的 account_type 为 'imap'

6. P0 — imap_generic 服务测试

TC-IMAP-01:认证失败返回 IMAP_AUTH_FAILED,不暴露密码

属性 值
测试 ID TC-IMAP-01
优先级 P0
Mock 设置 IMAP4_SSL.login() 抛出 imaplib.error("AUTHENTICATIONFAILED")
预期结果 返回 {"success": False, "error_code": "IMAP_AUTH_FAILED"};error 字段不含 imap_password 字符串

TC-IMAP-02:连接失败返回 IMAP_CONNECT_FAILED

属性 值
测试 ID TC-IMAP-02
优先级 P0
Mock 设置 IMAP4_SSL.__init__ 抛出 ConnectionRefusedError
预期结果 返回 {"success": False, "error_code": "IMAP_CONNECT_FAILED"}

TC-IMAP-03:文件夹不存在返回 IMAP_FOLDER_NOT_FOUND

属性 值
测试 ID TC-IMAP-03
优先级 P0
Mock 设置 连接和登录成功;mail.select() 始终返回 ('NO', [b'No such mailbox'])
预期结果 返回 {"success": False, "error_code": "IMAP_FOLDER_NOT_FOUND"}

TC-IMAP-04:正常拉取邮件列表返回统一格式

属性 值
测试 ID TC-IMAP-04
优先级 P0
Mock 设置 模拟完整正常 IMAP 交互;uid('SEARCH') 返回 2 个 UID;uid('FETCH') 返回邮件头
预期结果 {"success": True, "emails": [2 封邮件], "method": "IMAP (Generic)", "has_more": False};每封邮件包含 id、subject、from、date、is_read、has_attachments、body_preview

TC-IMAP-05:logout 在 finally 块中被调用(正常和异常场景均调用)

属性 值
测试 ID TC-IMAP-05
优先级 P0
Mock 设置 正常场景和认证失败场景(登录成功后 SEARCH 失败)
预期结果 两种场景下 mail.logout() 都被调用(通过 assert_called 验证)

TC-IMAP-06:get_email_detail_imap_generic — 正常返回详情

属性 值
测试 ID TC-IMAP-06
优先级 P0
Mock 设置 模拟 uid('FETCH', uid, 'BODY[]') 返回完整邮件 MIME 内容
预期结果 返回包含 body_text(或 body_html)的字典

TC-IMAP-07:get_email_detail_imap_generic — 失败返回 None

属性 值
测试 ID TC-IMAP-07
优先级 P0
Mock 设置 mail.uid('FETCH', ...) 返回 ('NO', [])
预期结果 返回 None

7. P0 — Repository IMAP 账号 CRUD 测试

TC-REPO-01:add_account(IMAP) — imap_password 加密入库,占位字段为空字符串

属性 值
测试 ID TC-REPO-01
优先级 P0
操作步骤 调用 add_account(email, password='', client_id='', refresh_token='', account_type='imap', provider='qq', imap_host='imap.qq.com', imap_port=993, imap_password='my-auth-code') → 直接查询 DB
预期结果 DB 中 imap_password 为加密串(is_encrypted()=True);client_id='';refresh_token='';account_type='imap';provider='qq'

TC-REPO-02:load_accounts() — 自动解密 imap_password

属性 值
测试 ID TC-REPO-02
优先级 P0
前置条件 DB 中有一条 IMAP 账号,imap_password 为加密串
操作步骤 调用 load_accounts() → 获取该账号
预期结果 账号字典中 imap_password 为明文原始值

TC-REPO-03:add_account(IMAP,无 imap_password) — 抛出 ValueError

属性 值
测试 ID TC-REPO-03
优先级 P0
预期结果 抛出 ValueError,提示 imap_password 必须提供

TC-REPO-04:add_account(custom IMAP,无 imap_host) — 抛出 ValueError

属性 值
测试 ID TC-REPO-04
优先级 P0
测试数据 provider='custom',imap_host=None
预期结果 抛出 ValueError,提示 custom IMAP 必须提供 imap_host

TC-REPO-05:load_accounts() — 返回包含所有新字段

属性 值
测试 ID TC-REPO-05
优先级 P0
预期结果 账号字典包含 account_type、provider、imap_host、imap_port 字段

8. P0 — Controller emails 路由分发测试

TC-EMAIL-01:IMAP 账号 — GET /api/emails 触发 imap_generic,不调用 graph

属性 值
测试 ID TC-EMAIL-01
优先级 P0
前置条件 DB 中有一条 account_type='imap' 的账号
Mock patch get_emails_imap_generic 返回成功结果;patch graph.get_emails 未调用
操作步骤 GET /api/emails/<imap_email>
预期结果 HTTP 200;get_emails_imap_generic 被调用一次;graph.get_emails 未被调用;响应中 method 为 "IMAP (Generic)"

TC-EMAIL-02:Outlook 账号 — GET /api/emails 不调用 imap_generic

属性 值
测试 ID TC-EMAIL-02
优先级 P0
前置条件 DB 中有一条 account_type='outlook'(或 NULL)的账号
Mock patch get_emails_imap_generic;patch graph.get_emails 返回成功
操作步骤 GET /api/emails/<outlook_email>
预期结果 get_emails_imap_generic 未被调用;graph.get_emails 被调用

TC-EMAIL-03:IMAP 账号 — POST /api/emails/delete 返回"不支持远程删除"

属性 值
测试 ID TC-EMAIL-03
优先级 P0
前置条件 DB 中有一条 account_type='imap' 的账号
操作步骤 POST /api/emails/delete with {"email_addr": "<imap_email>", "message_id": "123"}
预期结果 HTTP 200;响应 {"success": False, "error": "IMAP 邮箱不支持远程删除..."};审计日志中有对应记录

TC-EMAIL-04:IMAP 账号 — 验证码提取走 imap_generic 链路

属性 值
测试 ID TC-EMAIL-04
优先级 P0
前置条件 DB 中有一条 account_type='imap' 的账号
Mock get_emails_imap_generic 返回 1 封邮件;get_email_detail_imap_generic 返回含验证码的邮件详情
操作步骤 GET /api/emails/<imap_email>/extract-verification
预期结果 HTTP 200;get_emails_imap_generic 和 get_email_detail_imap_generic 各被调用一次;响应中包含验证码

TC-EMAIL-05:IMAP 账号 — imap_generic 返回失败时验证码提取返回错误

属性 值
测试 ID TC-EMAIL-05
优先级 P0
Mock get_emails_imap_generic 返回 {"success": False, ...}
预期结果 响应 success=False,不抛出未捕获异常

9. P0 — Controller accounts IMAP 接口测试

TC-ACC-01:GET /api/providers — 返回完整 providers 列表

属性 值
测试 ID TC-ACC-01
优先级 P0
操作步骤 GET /api/providers(已登录)
预期结果 HTTP 200;{"success": True, "providers": [...]};providers 长度为 8;第一个 key 为 'outlook'

TC-ACC-02:POST /api/accounts with provider='qq' — 以 IMAP 格式导入

属性 值
测试 ID TC-ACC-02
优先级 P0
请求体 {"provider": "qq", "account_string": "user@qq.com----my-auth-code", "group_id": 1}
预期结果 HTTP 200;success=True;DB 中新增账号 account_type='imap',provider='qq',imap_password 为加密串,client_id=''

TC-ACC-03:POST /api/accounts without provider — 使用旧 Outlook 格式(回归)

属性 值
测试 ID TC-ACC-03
优先级 P0
请求体 {"account_string": "user@outlook.com----pwd----cid----rt", "group_id": 1}
预期结果 HTTP 200;success=True;DB 中新增账号 account_type='outlook',client_id 和 refresh_token 非空

TC-ACC-04:POST /api/accounts with provider='custom' — 自定义 IMAP 格式

属性 值
测试 ID TC-ACC-04
优先级 P0
请求体 {"provider": "custom", "account_string": "user@corp.com----pwd----imap.corp.com----993", "group_id": 1}
预期结果 DB 中 imap_host='imap.corp.com',imap_port=993,provider='custom'

10. P0 — Scheduler 过滤测试

TC-SCHED-01:定时刷新任务查询不返回 IMAP 账号

属性 值
测试 ID TC-SCHED-01
优先级 P0
前置条件 DB 中存在 1 条 account_type='outlook' 账号 + 1 条 account_type='imap' 账号
操作步骤 直接执行 scheduler 中的账号查询 SQL(或调用相关函数)
预期结果 返回结果不包含 account_type='imap' 的账号;仅包含 outlook 账号

TC-SCHED-02:account_type=NULL 的旧账号被定时任务处理

属性 值
测试 ID TC-SCHED-02
优先级 P0
前置条件 DB 中存在 1 条 account_type=NULL 的旧账号(模拟迁移前的旧数据)
预期结果 该账号出现在查询结果中(account_type IS NULL 也应被刷新)

11. P0 — Outlook 功能回归测试

TC-REG-01:现有全量测试套件通过

属性 值
测试 ID TC-REG-01
优先级 P0
操作步骤 python -m unittest discover -s tests -v
预期结果 所有测试通过,数量不少于改动前基线

TC-REG-02:旧 Outlook 账号邮件读取——Graph API 路径

属性 值
测试 ID TC-REG-02
优先级 P0
Mock graph.get_emails 返回成功结果
预期结果 account_type=NULL/outlook 的账号走 graph 路径;method='Graph API'

TC-REG-03:旧 Outlook 账号验证码提取——功能不变

属性 值
测试 ID TC-REG-03
优先级 P0
操作步骤 GET /api/emails/<outlook_email>/extract-verification
预期结果 走现有链路;imap_generic 未被调用

12. P1 — 导出格式测试

TC-EXP-01:混合账号导出 — Outlook 在前,IMAP 按 provider 分组在后

属性 值
测试 ID TC-EXP-01
优先级 P1
前置条件 DB 中有 Outlook 账号、QQ 账号、163 账号各 1 条
预期结果 导出文本中 === Outlook 账号 === 在前;=== IMAP 账号(QQ邮箱)=== 和 === IMAP 账号(163邮箱)=== 在后

TC-EXP-02:IMAP 非 custom 导出格式 — email----imap_password----provider

属性 值
测试 ID TC-EXP-02
优先级 P1
预期结果 QQ 账号导出行为 user@qq.com----明文授权码----qq

TC-EXP-03:IMAP custom 导出格式 — 含 host/port

属性 值
测试 ID TC-EXP-03
优先级 P1
预期结果 custom 账号导出行为 user@corp.com----明文密码----custom----imap.corp.com----993

13. P2 — IMAP 文件夹映射测试

TC-FOLDER-01:QQ 邮箱 junkemail 候选包含 UTF-7 编码文件夹名

属性 值
测试 ID TC-FOLDER-01
优先级 P2
预期结果 get_imap_folder_candidates('qq', 'junkemail') 包含 "&V4NXPpCuTvY-"

TC-FOLDER-02:_resolve_imap_folder 按优先级尝试,返回第一个成功的文件夹

属性 值
测试 ID TC-FOLDER-02
优先级 P2
Mock 设置 mail.select('INBOX') 返回 ('NO', []);mail.select('Inbox') 返回 ('OK', [])
预期结果 返回 'Inbox'

14. 手动验收测试清单

端到端流程测试(P0)

编号 测试步骤 预期结果
E2E-01 打开添加账号模态框,默认 provider 为 Outlook textarea placeholder 显示 4 段格式
E2E-02 切换 provider 为 QQ邮箱 placeholder 变为 email----授权码;hint 提示使用授权码
E2E-03 切换 provider 为自定义 IMAP 显示 IMAP 服务器地址和端口输入框
E2E-04 切换回 Outlook 服务器配置区消失;placeholder 恢复 4 段格式
E2E-05 使用 Outlook 格式导入一个账号 账号列表出现,tag 显示 [Outlook]
E2E-06 使用 QQ 格式导入一个账号(用真实授权码) 账号列表出现,tag 显示 [QQ邮箱]
E2E-07 点击 QQ 账号 → 拉取邮件 邮件列表显示(或 IMAP 错误提示)
E2E-08 对 QQ 账号邮件执行"删除" 提示"不支持远程删除"
E2E-09 对 QQ 账号执行"验证码提取" 成功提取(或返回"未找到")
E2E-10 导出全部账号 下载文件中 Outlook 在前,IMAP 在后
E2E-11 验证定时刷新任务日志 日志中只有 Outlook 账号被刷新,无 IMAP 账号

15. 测试数据准备

# Outlook 测试账号(用于回归测试)
OUTLOOK_ACCOUNT = {
    "email": "test-outlook@outlook.com",
    "password": "test_password",
    "client_id": "test_client_id",
    "refresh_token": "test_refresh_token",
}

# QQ 邮箱 IMAP 测试账号
QQ_IMAP_ACCOUNT = {
    "email": "test@qq.com",
    "account_type": "imap",
    "provider": "qq",
    "imap_host": "imap.qq.com",
    "imap_port": 993,
    "imap_password": "test-auth-code-123",
}

# 自定义 IMAP 测试账号
CUSTOM_IMAP_ACCOUNT = {
    "email": "test@corp.com",
    "account_type": "imap",
    "provider": "custom",
    "imap_host": "imap.corp.com",
    "imap_port": 993,
    "imap_password": "corp-password",
}

# 模拟 IMAP 邮件列表响应
MOCK_IMAP_EMAILS_RESPONSE = {
    "success": True,
    "emails": [
        {
            "id": "42",
            "subject": "验证码:123456",
            "from": "noreply@example.com",
            "date": "Mon, 02 Mar 2026 10:00:00 +0800",
            "is_read": False,
            "has_attachments": False,
            "body_preview": "您的验证码是 123456,有效期5分钟。",
        }
    ],
    "method": "IMAP (Generic)",
    "has_more": False,
}

# 模拟 IMAP 邮件详情响应
MOCK_IMAP_EMAIL_DETAIL = {
    "id": "42",
    "subject": "验证码:123456",
    "from": "noreply@example.com",
    "date": "Mon, 02 Mar 2026 10:00:00 +0800",
    "is_read": False,
    "has_attachments": False,
    "body_text": "您的验证码是 123456,有效期5分钟。",
    "body_html": "",
}

16. 验收标准与门禁

P0 验收门禁(必须全部通过才能合并)

门禁项 标准
自动化测试 python -m unittest discover -s tests -v 全部通过
测试数量 通过数量 ≥ 改动前基线 + 本次新增用例数
imap_password DB 检查 SELECT imap_password FROM accounts WHERE account_type='imap' 结果均为加密格式
Outlook 回归 TC-REG-01、TC-REG-02、TC-REG-03 全部通过
IMAP 路由分发 TC-EMAIL-01、TC-EMAIL-02、TC-EMAIL-03 全部通过
定时任务过滤 TC-SCHED-01 通过

P1 验收建议

  • TC-ACC-01(providers API)通过
  • TC-EXP-01(导出格式)通过
  • E2E-05~E2E-09(至少用 QQ 或 163 真实账号完成)

文档版本:V1.0 | 状态:草案