diff --git a/docs/BUGS/BUG-00001-临时邮箱生成失败.md b/docs/BUGS/BUG-00001-临时邮箱生成失败.md new file mode 100644 index 0000000..9c8070f --- /dev/null +++ b/docs/BUGS/BUG-00001-临时邮箱生成失败.md @@ -0,0 +1,287 @@ +# BUG-00001:临时邮箱生成失败 + +## 基本信息 + +- **BUG编号**:BUG-00001 +- **发现日期**:2026-02-23 +- **报告人**:用户 +- **严重程度**:P1(高优先级) +- **状态**:✅ 已修复 +- **影响范围**:临时邮箱功能 +- **修复日期**:2026-02-23 + +## 问题描述 + +### 用户错误信息 +``` +生成临时邮箱失败,请稍后重试 +``` + +### 错误详情 +- **Code**: LEGACY_ERROR +- **Type**: LegacyError +- **Status**: 400 +- **Trace ID**: 557910def72a4ea7a59c8b6fb7880a98 + +## 复现步骤 + +1. 登录系统 +2. 进入临时邮箱管理页面 +3. 点击"生成临时邮箱"按钮 +4. 系统返回错误提示 + +## 预期行为 + +- 成功生成临时邮箱地址 +- 返回邮箱地址给用户 +- 在数据库中记录该临时邮箱 + +## 实际行为 + +- 生成失败 +- 返回笼统的错误信息 +- 用户无法知道具体失败原因 + +## 技术分析 + +### 相关代码位置 + +1. **路由层**:`outlook_web/routes/temp_emails.py:9` + - 路由:`/api/temp-emails/generate` + - 方法:POST + +2. **业务逻辑层**:`outlook_web/legacy.py:4171-4186` + ```python + def api_generate_temp_email(): + """生成新的临时邮箱""" + data = request.json or {} + prefix = data.get('prefix') + domain = data.get('domain') + + email_addr = generate_temp_email(prefix, domain) + + if email_addr: + if add_temp_email(email_addr): + log_audit('create', 'temp_email', email_addr, "生成临时邮箱") + return jsonify({'success': True, 'email': email_addr, 'message': '临时邮箱创建成功'}) + else: + return jsonify({'success': False, 'error': '邮箱已存在'}) + else: + retursonify({'success': False, 'error': '生成临时邮箱失败,请稍后重试'}) + ``` + +3. **服务层**:`outlook_web/services/gptmail.py:42-57` + ```python + def generate_temp_email(prefix: str = None, domain: str = None) -> Optional[str]: + """生成临时邮箱地址""" + json_data = {} + if prefix: + json_data["prefix"] = prefix + if domain: + json_data["domain"] = domain + + if json_data: + result = gptmail_request("POST", "/api/generate-email", json_data=json_data) + else: + result = gptmail_request("GET", "/api/generate-email") + + if result and result.get("success"): + return result.get("data", {}).get("email") + return None + ``` + +4. **错误处理**:`outlook_web/legacy.py:253-267` + - 将字符串错误转换为 LEGACY_ERROR + +### 根本原因分析 + +1. **直接原因**:`generate_temp_email()` 函数返回 `None` + +2. **可能的根本原因**: + - ❌ GPTMail API Key 未配置或无效 + - ❌ GPTMail API 服务不可用 + - ❌ 网络连接问题 + - ❌ API 请求超时(30秒) + - ❌ API 返回数据格式不符合预期 + +3. **错误处理问题**: + - ❌ `gptmail_request` 返回的详细错误信息被丢弃 + - ❌ `generate_temp_email` 只返回 None,不返回错误原因 + - ❌ `api_generate_temp_email` 使用笼统的错误信息 + - ❌ 没有记录详细的错误日志 + +### 错误传播链 + +``` +GPTMail API 失败 + ↓ +gptmail_request() 返回 {"success": False, "error": "具体错误"} + ↓ +generate_temp_email() 返回 None(错误信息丢失) + ↓ +api_generate_temp_email() 返回 "生成临时邮箱失败,请稍后重试" + ↓ +错误处理中间件转换为 LEGACY_ERROR + ↓ +用户看到笼统的错误信息 +``` + +## 修复方案 + +### 方案1:改进错误传播(推荐) + +1. **修改 `generate_temp_email` 函数**: + - 返回 `(email_addr, error_msg)` 元组 + - 保留详细的错误信息 + +2. **修改 `api_generate_temp_email` 函数**: + - 使用详细的错误信息 + - 添加错误日志记录 + +3. **改进 `gptmail_request` 函数**: + - 返回更详细的错误信息 + - 区分不同类型的错误(网络错误、API错误、超时等) + +### 方案2:使用异常处理 + +1. 在 `gptmail_request` 中抛出自定义异常 +2. 在 `api_generate_temp_email` 中捕获并处理异常 +3. 返回详细的错误信息给用户 + +### 推荐实施步骤 + +1. ✅ 创建 BUG 文档 +2. ✅ 改进 `gptmail_request` 函数的错误返回 +3. ✅ 改进 `generate_temp_email` 函数的返回值 +4. ✅ 改进 `api_generate_temp_email` 函数的错误处理 +5. ✅ 添加详细的错误日志 +6. ✅ 更新前端错误提示 +7. ✅ 编写单元测试(11个新测试) +8. ✅ 测试验证(51/51 测试通过) + +## 影响评估 + +- **用户体验**:严重影响,用户无法使用临时邮箱功能 +- **数据完整性**:无影响 +- **系统稳定性**:无影响 +- **安全性**:无影响 + +## 相关文档 + +- PRD:`docs/PRD/Outlook邮件管理工具-重构升级PRD.md` +- TDD:`docs/TDD/Outlook邮件管理工具-重构升级TDD.md` +- 开发者指南:`docs/DEV/00002-前后端拆分-开发者指南.md` + +## 测试计划 + +### 单元测试 +- [x] 测试 `gptmail_request` 的各种错误场景 + - API Key 未配置 + - API Key 无效(401) + - 请求超时 + - 网络连接失败 + - 服务器错误(500) + - 请求频率超限(429) +- [x] 测试 `generate_temp_email` 的错误处理 + - 成功生成 + - API Key 未配置 + - 请求超时 + - 数据格式错误 + - 使用 prefix 和 domain +- [x] 测试 `api_generate_temp_email` 的错误响应(已包含在原有测试中) + +### 集成测试 +- [x] 测试 API Key 无效的场景(通过 mock) +- [x] 测试网络超时的场景(通过 mock) +- [x] 测试 API 服务不可用的场景(通过 mock) +- [x] 测试正常生成临时邮箱的场景 + +### 回归测试 +- [x] 验证其他临时邮箱功能不受影响 +- [x] 验证错误处理中间件正常工作 +- [x] 所有测试通过(51/51) + +## 修复记录 + +- **2026-02-23 20:30**:创建 BUG 文档,完成根本原因分析 +- **2026-02-23 20:45**:实施修复方案 + - 改进 `gptmail_request` 函数,返回详细的错误信息(区分 10+ 种错误类型) + - 改进 `generate_temp_email` 函数,返回 `(email_addr, error_msg)` 元组 + - 改进 `api_generate_temp_email` 函数,使用详细错误信息并添加日志 + - 更新前端错误提示,显示详细错误信息 + - 编写 11 个新的单元测试 + - 更新 2 个现有测试的 mock +- **2026-02-23 21:00**:测试验证通过(51/51 测试全部通过) +- **2026-02-23 21:05**:准备提交修复 + +## 修复详情 + +### 1. 改进 `gptmail_request` 函数(outlook_web/services/gptmail.py) + +**改进内容**: +- 添加 API Key 配置检查 +- 区分 10+ 种错误类型: + - `CONFIG_ERROR`:API Key 未配置 + - `AUTH_ERROR`:API Key 无效(401) + - `PERMISSION_ERROR`:访问被拒绝(403) + - `RATE_LIMIT_ERROR`:请求频率超限(429) + - `SERVER_ERROR`:服务器错误(500+) + - `HTTP_ERROR`:其他 HTTP 错误 + - `TIMEOUT_ERROR`:请求超时 + - `CONNECTION_ERROR`:网络连接失败 + - `REQUEST_ERROR`:其他请求异常 + - `METHOD_ERROR`:不支持的请求方法 + - `UNKNOWN_ERROR`:未知错误 +- 返回结构化错误信息:`{success, error, error_type, details}` + +### 2. 改进 `generate_temp_email` 函数(outlook_web/services/gptmail.py) + +**改进内容**: +- 返回值从 `Optional[str]` 改为 `Tuple[Optional[str], Optional[str]]` +- 成功时返回 `(email_addr, None)` +- 失败时返回 `(None, error_message)`,保留详细错误信息 +- 添加数据格式验证 + +### 3. 改进 `api_generate_temp_email` 函数(outlook_web/legacy.py) + +**改进内容**: +- 解包 `generate_temp_email` 的返回值:`email_addr, error_msg = generate_temp_email(...)` +- 使用详细的错误信息返回给前端 +- 添加日志记录(成功、警告、错误三个级别) + +### 4. 更新前端错误提示(static/js/features/temp_emails.js) + +**改进内容**: +- 检查错误对象的结构 +- 优先显示 `error.message` 或 `error.error` +- 支持结构化错误对象和字符串错误 +- 保留错误详情用于调试 + +### 5. 编写单元测试(tests/test_gptmail_error_handling.py) + +**新增测试**: +- 11 个新的单元测试,覆盖所有错误场景 +- 使用 mock 模拟各种失败情况 +- 验证错误类型和错误信息的正确性 + +## 修复效果 + +### 修复前 +- 用户看到:`生成临时邮箱失败,请稍后重试` +- 错误码:`LEGACY_ERROR` +- 无法知道具体失败原因 + +### 修复后 +- 用户看到详细错误信息,例如: + - `GPTMail API Key 未配置(请在系统设置中配置 GPTMail API Key)` + - `API 请求超时(请求超过 30 秒未响应,请检查网络连接或稍后重试)` + - `无法连接到 GPTMail 服务(网络连接失败: ...)` + - `API Key 无效或已过期(HTTP 401: 请检查 API Key 是否正确)` +- 后端日志记录详细的错误信息 +- 开发者可以快速定位问题 + +## 备注 + +- 建议同时检查其他使用 `gptmail_request` 的函数,确保错误处理一致 +- 考虑添加 GPTMail API 健康检查功能 +- 考虑在设置页面添加 API Key 验证功能 diff --git a/outlook_web/legacy.py b/outlook_web/legacy.py index c7ffb65..feb3a91 100644 --- a/outlook_web/legacy.py +++ b/outlook_web/legacy.py @@ -4170,20 +4170,29 @@ def api_get_temp_emails(): @login_required def api_generate_temp_email(): """生成新的临时邮箱""" + import logging + logger = logging.getLogger(__name__) + data = request.json or {} prefix = data.get('prefix') domain = data.get('domain') - - email_addr = generate_temp_email(prefix, domain) - + + # 调用改进后的 generate_temp_email,返回 (email_addr, error_msg) + email_addr, error_msg = generate_temp_email(prefix, domain) + if email_addr: + # 成功生成邮箱地址 if add_temp_email(email_addr): log_audit('create', 'temp_email', email_addr, "生成临时邮箱") + logger.info(f"临时邮箱生成成功: {email_addr}") return jsonify({'success': True, 'email': email_addr, 'message': '临时邮箱创建成功'}) else: + logger.warning(f"临时邮箱已存在: {email_addr}") return jsonify({'success': False, 'error': '邮箱已存在'}) else: - return jsonify({'success': False, 'error': '生成临时邮箱失败,请稍后重试'}) + # 生成失败,返回详细错误信息 + logger.error(f"临时邮箱生成失败: {error_msg}, prefix={prefix}, domain={domain}") + return jsonify({'success': False, 'error': error_msg or '生成临时邮箱失败,请稍后重试'}) @app.route('/api/temp-emails/', methods=['DELETE']) diff --git a/outlook_web/services/gptmail.py b/outlook_web/services/gptmail.py index aaf597b..c9abc61 100644 --- a/outlook_web/services/gptmail.py +++ b/outlook_web/services/gptmail.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple import requests @@ -14,15 +14,32 @@ def gptmail_request( params: dict = None, json_data: dict = None, ) -> Optional[Dict]: - """发送 GPTMail API 请求""" + """ + 发送 GPTMail API 请求 + + 返回格式: + - 成功:{"success": True, "data": {...}} + - 失败:{"success": False, "error": "错误信息", "error_type": "错误类型", "details": "详细信息"} + """ try: url = f"{config.get_gptmail_base_url()}{endpoint}" api_key = get_gptmail_api_key() + + # 检查 API Key 是否配置 + if not api_key: + return { + "success": False, + "error": "GPTMail API Key 未配置", + "error_type": "CONFIG_ERROR", + "details": "请在系统设置中配置 GPTMail API Key" + } + headers = { "X-API-Key": api_key, "Content-Type": "application/json", } + # 发送请求 if method.upper() == "GET": response = requests.get(url, headers=headers, params=params, timeout=30) elif method.upper() == "POST": @@ -30,17 +47,90 @@ def gptmail_request( elif method.upper() == "DELETE": response = requests.delete(url, headers=headers, params=params, timeout=30) else: - return None + return { + "success": False, + "error": f"不支持的请求方法: {method}", + "error_type": "METHOD_ERROR", + "details": "仅支持 GET、POST、DELETE 方法" + } + # 处理响应 if response.status_code == 200: return response.json() - return {"success": False, "error": f"API 请求失败: {response.status_code}"} + elif response.status_code == 401: + return { + "success": False, + "error": "API Key 无效或已过期", + "error_type": "AUTH_ERROR", + "details": f"HTTP {response.status_code}: 请检查 API Key 是否正确" + } + elif response.status_code == 403: + return { + "success": False, + "error": "API 访问被拒绝", + "error_type": "PERMISSION_ERROR", + "details": f"HTTP {response.status_code}: 请检查 API Key 权限" + } + elif response.status_code == 429: + return { + "success": False, + "error": "API 请求频率超限", + "error_type": "RATE_LIMIT_ERROR", + "details": f"HTTP {response.status_code}: 请稍后重试" + } + elif response.status_code >= 500: + return { + "success": False, + "error": "GPTMail 服务暂时不可用", + "error_type": "SERVER_ERROR", + "details": f"HTTP {response.status_code}: 服务器错误,请稍后重试" + } + else: + return { + "success": False, + "error": f"API 请求失败", + "error_type": "HTTP_ERROR", + "details": f"HTTP {response.status_code}: {response.text[:200]}" + } + + except requests.exceptions.Timeout: + return { + "success": False, + "error": "API 请求超时", + "error_type": "TIMEOUT_ERROR", + "details": "请求超过 30 秒未响应,请检查网络连接或稍后重试" + } + except requests.exceptions.ConnectionError as e: + return { + "success": False, + "error": "无法连接到 GPTMail 服务", + "error_type": "CONNECTION_ERROR", + "details": f"网络连接失败: {str(e)[:200]}" + } + except requests.exceptions.RequestException as e: + return { + "success": False, + "error": "网络请求异常", + "error_type": "REQUEST_ERROR", + "details": f"请求失败: {str(e)[:200]}" + } except Exception as e: - return {"success": False, "error": f"请求异常: {str(e)}"} + return { + "success": False, + "error": "未知错误", + "error_type": "UNKNOWN_ERROR", + "details": f"异常: {str(e)[:200]}" + } -def generate_temp_email(prefix: str = None, domain: str = None) -> Optional[str]: - """生成临时邮箱地址""" +def generate_temp_email(prefix: str = None, domain: str = None) -> Tuple[Optional[str], Optional[str]]: + """ + 生成临时邮箱地址 + + 返回: + - (email_addr, None) - 成功 + - (None, error_message) - 失败,包含详细错误信息 + """ json_data = {} if prefix: json_data["prefix"] = prefix @@ -52,9 +142,28 @@ def generate_temp_email(prefix: str = None, domain: str = None) -> Optional[str] else: result = gptmail_request("GET", "/api/generate-email") - if result and result.get("success"): - return result.get("data", {}).get("email") - return None + if not result: + return None, "GPTMail API 请求失败" + + if result.get("success"): + email = result.get("data", {}).get("email") + if email: + return email, None + else: + return None, "API 返回数据格式错误:缺少 email 字段" + else: + # 返回详细的错误信息 + error = result.get("error", "未知错误") + error_type = result.get("error_type", "UNKNOWN") + details = result.get("details", "") + + # 组合错误信息 + if details: + error_message = f"{error}({details})" + else: + error_message = error + + return None, error_message def get_temp_emails_from_api(email_addr: str) -> Optional[List[Dict]]: @@ -83,4 +192,3 @@ def clear_temp_emails_from_api(email_addr: str) -> bool: """清空 GPTMail 邮箱的所有邮件""" result = gptmail_request("DELETE", "/api/emails/clear", params={"email": email_addr}) return bool(result and result.get("success", False)) - diff --git a/static/js/features/temp_emails.js b/static/js/features/temp_emails.js index b04743a..a5a49a6 100644 --- a/static/js/features/temp_emails.js +++ b/static/js/features/temp_emails.js @@ -84,7 +84,16 @@ loadTempEmails(true); loadGroups(); } else { - handleApiError(data, '生成临时邮箱失败'); + // 显示详细的错误信息 + const errorMsg = data.error || '生成临时邮箱失败'; + if (data.error && typeof data.error === 'object') { + // 结构化错误对象 + const detailedError = data.error.message || data.error.error || errorMsg; + showToast(detailedError, 'error', data.error); + } else { + // 字符串错误 + showToast(errorMsg, 'error'); + } } } catch (error) { showToast('生成临时邮箱失败', 'error'); diff --git a/tests/__pycache__/test_core_features.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_core_features.cpython-313-pytest-8.4.2.pyc new file mode 100644 index 0000000..f00f61e Binary files /dev/null and b/tests/__pycache__/test_core_features.cpython-313-pytest-8.4.2.pyc differ diff --git a/tests/__pycache__/test_core_features.cpython-313.pyc b/tests/__pycache__/test_core_features.cpython-313.pyc index e8dceb0..eb6ea33 100644 Binary files a/tests/__pycache__/test_core_features.cpython-313.pyc and b/tests/__pycache__/test_core_features.cpython-313.pyc differ diff --git a/tests/__pycache__/test_distributed_lock.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_distributed_lock.cpython-313-pytest-8.4.2.pyc new file mode 100644 index 0000000..b63e05f Binary files /dev/null and b/tests/__pycache__/test_distributed_lock.cpython-313-pytest-8.4.2.pyc differ diff --git a/tests/__pycache__/test_error_and_trace.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_error_and_trace.cpython-313-pytest-8.4.2.pyc new file mode 100644 index 0000000..5fc437c Binary files /dev/null and b/tests/__pycache__/test_error_and_trace.cpython-313-pytest-8.4.2.pyc differ diff --git a/tests/__pycache__/test_gptmail_error_handling.cpython-313.pyc b/tests/__pycache__/test_gptmail_error_handling.cpython-313.pyc new file mode 100644 index 0000000..77356e7 Binary files /dev/null and b/tests/__pycache__/test_gptmail_error_handling.cpython-313.pyc differ diff --git a/tests/__pycache__/test_masking_audit_and_import.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_masking_audit_and_import.cpython-313-pytest-8.4.2.pyc new file mode 100644 index 0000000..99b5780 Binary files /dev/null and b/tests/__pycache__/test_masking_audit_and_import.cpython-313-pytest-8.4.2.pyc differ diff --git a/tests/__pycache__/test_masking_audit_and_import.cpython-313.pyc b/tests/__pycache__/test_masking_audit_and_import.cpython-313.pyc index 5fd2225..24bbf43 100644 Binary files a/tests/__pycache__/test_masking_audit_and_import.cpython-313.pyc and b/tests/__pycache__/test_masking_audit_and_import.cpython-313.pyc differ diff --git a/tests/__pycache__/test_module_boundaries.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_module_boundaries.cpython-313-pytest-8.4.2.pyc new file mode 100644 index 0000000..7c7c602 Binary files /dev/null and b/tests/__pycache__/test_module_boundaries.cpython-313-pytest-8.4.2.pyc differ diff --git a/tests/__pycache__/test_smoke_contract.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_smoke_contract.cpython-313-pytest-8.4.2.pyc new file mode 100644 index 0000000..cb53e1a Binary files /dev/null and b/tests/__pycache__/test_smoke_contract.cpython-313-pytest-8.4.2.pyc differ diff --git a/tests/test_core_features.py b/tests/test_core_features.py index aa9edb8..ac0c3ed 100644 --- a/tests/test_core_features.py +++ b/tests/test_core_features.py @@ -172,7 +172,8 @@ class CoreFeatureTests(unittest.TestCase): self._login(client) email_addr = f"temp_{uuid.uuid4().hex}@example.com" - with patch.object(self.module.impl, "generate_temp_email", return_value=email_addr): + # Mock generate_temp_email 返回元组 (email_addr, None) + with patch.object(self.module.impl, "generate_temp_email", return_value=(email_addr, None)): created = client.post("/api/temp-emails/generate", json={"prefix": "x", "domain": "y"}) self.assertEqual(created.status_code, 200) created_data = created.get_json() diff --git a/tests/test_gptmail_error_handling.py b/tests/test_gptmail_error_handling.py new file mode 100644 index 0000000..bf855a4 --- /dev/null +++ b/tests/test_gptmail_error_handling.py @@ -0,0 +1,159 @@ +import unittest +from unittest.mock import patch, MagicMock +import requests + +from outlook_web.services import gptmail + + +class TestGPTMailErrorHandling(unittest.TestCase): + """测试 GPTMail 服务的错误处理""" + + def test_gptmail_request_no_api_key(self): + """测试 API Key 未配置的情况""" + with patch('outlook_web.services.gptmail.get_gptmail_api_key', return_value=''): + result = gptmail.gptmail_request('GET', '/api/test') + + self.assertFalse(result['success']) + self.assertEqual(result['error_type'], 'CONFIG_ERROR') + self.assertIn('未配置', result['error']) + + def test_gptmail_request_401_unauthorized(self): + """测试 API Key 无效的情况""" + mock_response = MagicMock() + mock_response.status_code = 401 + + with patch('outlook_web.services.gptmail.get_gptmail_api_key', return_value='invalid_key'): + with patch('requests.get', return_value=mock_response): + result = gptmail.gptmail_request('GET', '/api/test') + + self.assertFalse(result['success']) + self.assertEqual(result['error_type'], 'AUTH_ERROR') + self.assertIn('无效', result['error']) + + def test_gptmail_request_timeout(self): + """测试请求超时的情况""" + with patch('outlook_web.services.gptmail.get_gptmail_api_key', return_value='test_key'): + with patch('requests.get', side_effect=requests.exceptions.Timeout()): + result = gptmail.gptmail_request('GET', '/api/test') + + self.assertFalse(result['success']) + self.assertEqual(result['error_type'], 'TIMEOUT_ERROR') + self.assertIn('超时', result['error']) + + def test_gptmail_request_connection_error(self): + """测试网络连接失败的情况""" + with patch('outlook_web.services.gptmail.get_gptmail_api_key', return_value='test_key'): + with patch('requests.get', side_effect=requests.exceptions.ConnectionError('Connection refused')): + result = gptmail.gptmail_request('GET', '/api/test') + + self.assertFalse(result['success']) + self.assertEqual(result['error_type'], 'CONNECTION_ERROR') + self.assertIn('连接', result['error']) + + def test_gptmail_request_500_server_error(self): + """测试服务器错误的情况""" + mock_response = MagicMock() + mock_response.status_code = 500 + + with patch('outlook_web.services.gptmail.get_gptmail_api_key', return_value='test_key'): + with patch('requests.get', return_value=mock_response): + result = gptmail.gptmail_request('GET', '/api/test') + + self.assertFalse(result['success']) + self.assertEqual(result['error_type'], 'SERVER_ERROR') + self.assertIn('不可用', result['error']) + + def test_gptmail_request_429_rate_limit(self): + """测试请求频率超限的情况""" + mock_response = MagicMock() + mock_response.status_code = 429 + + with patch('outlook_web.services.gptmail.get_gptmail_api_key', return_value='test_key'): + with patch('requests.get', return_value=mock_response): + result = gptmail.gptmail_request('GET', '/api/test') + + self.assertFalse(result['success']) + self.assertEqual(result['error_type'], 'RATE_LIMIT_ERROR') + self.assertIn('频率超限', result['error']) + + def test_generate_temp_email_success(self): + """测试成功生成临时邮箱""" + mock_result = { + 'success': True, + 'data': {'email': 'test@example.com'} + } + + with patch('outlook_web.services.gptmail.gptmail_request', return_value=mock_result): + email, error = gptmail.generate_temp_email() + + self.assertEqual(email, 'test@example.com') + self.assertIsNone(error) + + def test_generate_temp_email_api_key_not_configured(self): + """测试 API Key 未配置时生成临时邮箱""" + mock_result = { + 'success': False, + 'error': 'GPTMail API Key 未配置', + 'error_type': 'CONFIG_ERROR', + 'details': '请在系统设置中配置 GPTMail API Key' + } + + with patch('outlook_web.services.gptmail.gptmail_request', return_value=mock_result): + email, error = gptmail.generate_temp_email() + + self.assertIsNone(email) + self.assertIn('未配置', error) + self.assertIn('系统设置', error) + + def test_generate_temp_email_timeout(self): + """测试请求超时时生成临时邮箱""" + mock_result = { + 'success': False, + 'error': 'API 请求超时', + 'error_type': 'TIMEOUT_ERROR', + 'details': '请求超过 30 秒未响应,请检查网络连接或稍后重试' + } + + with patch('outlook_web.services.gptmail.gptmail_request', return_value=mock_result): + email, error = gptmail.generate_temp_email() + + self.assertIsNone(email) + self.assertIn('超时', error) + self.assertIn('30 秒', error) + + def test_generate_temp_email_missing_email_field(self): + """测试 API 返回数据缺少 email 字段""" + mock_result = { + 'success': True, + 'data': {} # 缺少 email 字段 + } + + with patch('outlook_web.services.gptmail.gptmail_request', return_value=mock_result): + email, error = gptmail.generate_temp_email() + + self.assertIsNone(email) + self.assertIn('格式错误', error) + self.assertIn('email', error) + + def test_generate_temp_email_with_prefix_and_domain(self): + """测试使用 prefix 和 domain 生成临时邮箱""" + mock_result = { + 'success': True, + 'data': {'email': 'custom@example.com'} + } + + with patch('outlook_web.services.gptmail.gptmail_request', return_value=mock_result) as mock_request: + email, error = gptmail.generate_temp_email(prefix='custom', domain='example.com') + + self.assertEqual(email, 'custom@example.com') + self.assertIsNone(error) + + # 验证调用参数 + mock_request.assert_called_once() + call_args = mock_request.call_args + self.assertEqual(call_args[0][0], 'POST') + self.assertEqual(call_args[1]['json_data'], {'prefix': 'custom', 'domain': 'example.com'}) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_masking_audit_and_import.py b/tests/test_masking_audit_and_import.py index 08e45af..7149142 100644 --- a/tests/test_masking_audit_and_import.py +++ b/tests/test_masking_audit_and_import.py @@ -187,7 +187,8 @@ class MaskingAuditAndImportTests(unittest.TestCase): unique = uuid.uuid4().hex email_addr = f"tmp_{unique}@example.com" - with patch.object(self.module.impl, "generate_temp_email", return_value=email_addr): + # Mock generate_temp_email 返回元组 (email_addr, None) + with patch.object(self.module.impl, "generate_temp_email", return_value=(email_addr, None)): resp = client.post("/api/temp-emails/generate", json={}) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.get_json().get("success"), True)