diff --git a/history_viewer.py b/history_viewer.py index 8b6dff4..62672af 100644 --- a/history_viewer.py +++ b/history_viewer.py @@ -193,6 +193,27 @@ def download_report(run_id, filename): return send_from_directory(run_dir, filename_safe, as_attachment=True) +# --- 新增:PDF文件预览路由 --- +@app.route('/view_pdf/') +@login_required +def view_pdf_report(run_id): + """安全地提供PDF报告文件以内联方式查看。""" + run_id_safe = Path(run_id).name + filename_safe = "report_cn.pdf" + + reports_dir = Path(app.config['REPORTS_DIR']).resolve() + run_dir = (reports_dir / run_id_safe).resolve() + + # 安全检查 + if not run_dir.is_dir() or run_dir.parent != reports_dir: + abort(404, "找不到指定的测试记录或权限不足。") + + pdf_path = run_dir / filename_safe + if not pdf_path.exists(): + abort(404, "未找到PDF报告文件。") + + return send_from_directory(run_dir, filename_safe) + # --- 历史记录视图 --- @app.route('/') @login_required @@ -237,9 +258,13 @@ def show_details(run_id): summary_path = run_dir / 'summary.json' details_path = run_dir / 'api_call_details.md' + pdf_path = run_dir / 'report_cn.pdf' # 新增PDF路径 summary_content = "{}" details_content = "### 未找到API调用详情报告" + + has_pdf_report = pdf_path.exists() # 检查PDF是否存在 + has_md_report = details_path.exists() # 检查MD报告是否存在 if summary_path.exists(): try: @@ -249,7 +274,7 @@ def show_details(run_id): except Exception as e: summary_content = f"加载摘要文件出错: {e}" - if details_path.exists(): + if has_md_report: try: with open(details_path, 'r', encoding='utf-8') as f: # 将Markdown转换为HTML @@ -257,7 +282,12 @@ def show_details(run_id): except Exception as e: details_content = f"加载详情文件出错: {e}" - return render_template('history_detail.html', run_id=run_id, summary_content=summary_content, details_content=details_content) + return render_template('history_detail.html', + run_id=run_id, + summary_content=summary_content, + details_content=details_content, + has_pdf_report=has_pdf_report, + has_md_report=has_md_report) # --- 根路径重定向 --- diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index ec66ad5..70796bd 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -63,6 +63,10 @@ - 添加API端点的搜索和过滤功能 - 改进LLM参数生成的质量和效率 - 支持更复杂的测试场景和数据依赖 +- **引入响应适配层**: 设计并实现可插拔的"响应适配层",以灵活适配非标准API响应。该机制包括: + - **响应数据提取器**: 智能提取被包装的(如`{code, data}`)或直接返回的核心业务数据。 + - **动态Schema提供者**: 支持通过独立API请求获取并缓存数据模型(Schema)。 + - **灵活Schema验证测试用例**: 整合上述机制,实现一个可复用的、用于非标准API风格的验证用例。 ### 长期目标 (3+个月) - 开发更强大的测试报告分析工具 diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 45e6636..1c71d3a 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -53,6 +53,7 @@ ## 待完成工作 ### 功能增强 +- ⏳ **响应适配层与灵活验证**: 实现用于处理非标准API响应的适配层,包括动态Schema获取和智能数据提取。 - ⏳ 多线程/并行测试执行支持 - ⏳ 测试覆盖率分析工具 - ⏳ 更强大的LLM回退和缓存机制 diff --git a/memory-bank/response_adaptation_layer_design.md b/memory-bank/response_adaptation_layer_design.md new file mode 100644 index 0000000..529d816 --- /dev/null +++ b/memory-bank/response_adaptation_layer_design.md @@ -0,0 +1,113 @@ +# 设计文档:响应适配层 (Response Adaptation Layer) + +## 1. 问题背景 + +在对企业内部大量存量的API进行合规性测试时,我们遇到了一个常见的挑战:API的设计风格存在不一致性。具体表现为: + +1. **响应体结构不统一**: + * 部分API遵循标准的包装格式,如 `{"code": 0, "message": "success", "data": {...}}`,真正的业务数据位于 `data` 字段内。 + * 另一部分API则直接返回业务数据对象,如 `{...}`。 + * 当返回列表时,业务数据可能是 `data: [...]`,也可能是直接返回 `[...]`。 + +2. **数据模型(Schema)来源不统一**: + * 标准的API会在其OpenAPI/Swagger规范中直接定义响应体的Schema。 + * 但存在一类特殊的"核心模型"API,其响应数据对应的Schema需要通过调用另一个独立的API端点来动态获取。 + +这些不一致性给自动化的Schema验证带来了极大的困难。如果为每种情况都编写一个独立的测试用例,会导致代码大量冗余,且难以维护。 + +## 2. 核心理念与设计目标 + +为了优雅地解决上述问题,我们引入 **"响应适配层" (Response Adaptation Layer)** 的概念。 + +其核心理念是将 **"适配"** 过程(即,如何找到真正需要验证的数据和其对应的Schema)与核心的 **"验证"** 逻辑(即,数据是否符合Schema的规范)完全解耦。 + +**设计目标**: +* **灵活性与可扩展性**:能够轻松适应未来可能出现的更多样的API风格,只需增加新的适配策略,而无需修改核心验证框架。 +* **高内聚,低耦合**:将适配逻辑集中处理,保持核心验证测试用例的纯净和通用。 +* **配置驱动**:适配规则应通过外部配置来定义,而非硬编码在代码中。 + +## 3. 架构设计 + +响应适配层位于 "API调用" 和 "测试断言" 之间。它接收原始的HTTP响应和API端点信息,输出归一化后的数据和Schema,供下游的验证引擎使用。 + +```mermaid +flowchart TD + subgraph 测试执行流程 + A[调用API获取响应] --> B{响应适配层}; + + subgraph B + C[响应数据提取器] + D[动态Schema提供者] + end + + B --> E[归一化的数据和Schema]; + E --> F[执行Schema验证]; + end +``` + +## 4. 核心组件详述 + +### 4.1. 响应数据提取器 (Response Data Extractor) + +这是一个工具模块,提供一个核心函数 `extract_data_for_validation(response_json)`,负责从原始API响应中智能地提取出需要被验证的核心业务数据。 + +**提取策略**: +1. **检查标准包装**:检查响应体 `response_json` 是否同时包含 `code` 和 `data` 字段。 + * **是**:返回 `response_json['data']` 的内容作为待处理数据。 + * **否**:返回整个 `response_json` 作为待处理数据。 +2. **处理列表与对象**:对上一步获取的待处理数据进行检查。 + * **是列表 (List)**: + * 如果列表为空 `[]`,则认为无法进行Schema验证,直接返回 `None`,跳过验证。 + * 如果列表非空,则返回列表的第一个元素 `list[0]` 作为最终需要验证的数据。这是基于"一个列表内的所有元素都应遵循相同Schema"的假设。 + * **是对象 (Dict)**:直接返回该对象作为最终需要验证的数据。 + +### 4.2. 动态Schema提供者 (Dynamic Schema Provider) + +这是一个独立的模块,负责根据API端点信息,为其提供正确的JSON Schema,特别是在Schema需要从外部动态获取的场景下。 + +**工作机制**: +1. **配置驱动**:在测试项目的配置文件中,增加一个映射关系表 `dynamic_schema_mapping`,用于定义哪个API端点(或哪种模式的API)的Schema需要从哪个URL获取。 + + *示例配置 (config.yaml)*: + ```yaml + dynamic_schema_mapping: + # key可以是精确路径,也可以是正则表达式 + "/api/v1/core-models/get/{model_id}": "https://api.internal/schemas/{model_id}" + "/api/v2/special-data/.*": "https://api.internal/schemas/special-data.json" + ``` + +2. **按需获取**: 当测试用例需要Schema时,提供者会检查当前测试的API端点是否在 `dynamic_schema_mapping` 中有匹配项。 + * **有匹配**: 根据配置的URL模板(例如,从路径中提取 `model_id`)构造完整的Schema URL,然后发起HTTP GET请求获取Schema内容。 + * **无匹配**: 回退到默认行为,即从当前API的OpenAPI规范中查找其定义的响应Schema。 + +3. **缓存机制**: 为了避免对同一个Schema URL的重复网络请求,提供者内部会维护一个缓存(如一个简单的字典)。在发起请求前,会先检查缓存中是否已有该URL的Schema。 + +## 5. 整合与实现 + +我们将创建一个新的、高度灵活的测试用例 `FlexibleSchemaValidationCase` 来整合上述两个组件。 + +**实现步骤**: +1. **测试用例识别**: 在 `applies_to` 方法中,定义此测试用例适用于哪些API。我们可以通过在OpenAPI规范中使用一个自定义扩展字段 `x-flexible-validation: true` 来标记目标API。 + + *示例 (openapi.yaml)*: + ```yaml + paths: + /api/v1/core-models/get/{model_id}: + get: + summary: "获取核心模型数据" + x-flexible-validation: true # 标记适用此测试用例 + ... + ``` +2. **执行逻辑 (`execute` 方法)**: + a. 调用 **动态Schema提供者**,传入当前端点信息,获取对应的JSON Schema。如果获取失败,则测试用例断言失败。 + b. 正常调用该API,获得HTTP响应。 + c. 将响应体JSON传递给 **响应数据提取器**,获取归一化后的核心业务数据。如果返回`None`(例如空列表),则该测试用例标记为 `SKIPPED`。 + d. 使用标准的 `jsonschema.validate` 函数,用步骤a获取的Schema来验证步骤c提取的数据。 + e. 根据验证结果,断言测试成功或失败。 + +## 6. 方案优势总结 + +* **关注点分离**:将复杂的适配逻辑与核心的验证逻辑解耦,代码更清晰,可维护性更高。 +* **高度可扩展**:未来若有新的API风格,只需在"提取器"或"提供者"中增加新策略,而无需改动大量测试用例。 +* **配置灵活**:通过外部配置文件和API规范中的自定义字段来驱动,适应性强,无需硬编码。 +* **代码复用**:将通用的适配逻辑沉淀到工具库中,避免在不同测试用例中重复实现。 \ No newline at end of file diff --git a/static/history_style.css b/static/history_style.css index a1b9604..d4ca948 100644 --- a/static/history_style.css +++ b/static/history_style.css @@ -212,4 +212,16 @@ padding: 0; margin: 0; font-size: 100%; +} + +/* PDF container styles */ +.pdf-container { + margin-bottom: 2rem; + border: 1px solid #ddd; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.pdf-container iframe { + display: block; + border: none; } \ No newline at end of file diff --git a/templates/history_detail.html b/templates/history_detail.html index fbef12a..813b32b 100644 --- a/templates/history_detail.html +++ b/templates/history_detail.html @@ -19,15 +19,28 @@
下载JSON报告 + {% if has_md_report %} 下载MD报告 + {% endif %} + {% if has_pdf_report %} + 下载PDF报告 + {% endif %}
+ {% if has_pdf_report %} +

PDF 报告

+
+ +
+ {% endif %}

测试摘要 (JSON)

{{ summary_content }}
+ +

API 调用详情

{{ details_content|safe }} @@ -35,7 +48,11 @@
+ {% if has_pdf_report %} + PDF报告 + {% endif %} 测试摘要 + API调用详情