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
目录
- 测试概述
- 测试策略
- 测试环境配置
- P0 — 数据库迁移测试
- P0 — providers 模块单元测试
- P0 — imap_generic 服务测试
- P0 — Repository IMAP 账号 CRUD 测试
- P0 — Controller emails 路由分发测试
- P0 — Controller accounts IMAP 接口测试
- P0 — Scheduler 过滤测试
- P0 — Outlook 功能回归测试
- P1 — 导出格式测试
- P2 — IMAP 文件夹映射测试
- 手动验收测试清单
- 测试数据准备
- 验收标准与门禁
1. 测试概述
1.1 测试目标
本测试文档覆盖"多邮箱统一管理"功能的完整验证,确保:
-
新功能正确性
- DB Schema v3 迁移幂等、不破坏旧数据
- IMAP 通用服务能正确认证、拉取邮件、返回统一格式
- 按
account_type 路由分发逻辑准确
- IMAP 账号的敏感字段加解密正确
- 定时刷新任务只处理 Outlook 账号
-
Outlook 流程零回归
- 现有 Graph API → IMAP XOAUTH2 回退链不受影响
- 现有批量导入格式(4 段)仍正常
- 现有邮件删除、验证码提取、账号导出不受影响
-
安全约束满足
- 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 核心测试原则
- 隔离 IMAP 网络调用:所有涉及
imaplib.IMAP4_SSL 的测试均通过 unittest.mock.patch 模拟,不发起真实网络请求。
- 真实 DB 操作:Repository 和 Controller 测试使用临时 SQLite 文件(
_import_app.py 已处理),验证实际 SQL 行为。
- 最小改动原则:新增测试类,不修改现有测试文件,确保回归测试基线不变。
- 按 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 运行方式
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. 测试数据准备
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 | 状态:草案