展示报告
This commit is contained in:
parent
fca20523ca
commit
864a36fe61
@ -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/<path:run_id>')
|
||||
@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)
|
||||
|
||||
|
||||
# --- 根路径重定向 ---
|
||||
|
||||
@ -63,6 +63,10 @@
|
||||
- 添加API端点的搜索和过滤功能
|
||||
- 改进LLM参数生成的质量和效率
|
||||
- 支持更复杂的测试场景和数据依赖
|
||||
- **引入响应适配层**: 设计并实现可插拔的"响应适配层",以灵活适配非标准API响应。该机制包括:
|
||||
- **响应数据提取器**: 智能提取被包装的(如`{code, data}`)或直接返回的核心业务数据。
|
||||
- **动态Schema提供者**: 支持通过独立API请求获取并缓存数据模型(Schema)。
|
||||
- **灵活Schema验证测试用例**: 整合上述机制,实现一个可复用的、用于非标准API风格的验证用例。
|
||||
|
||||
### 长期目标 (3+个月)
|
||||
- 开发更强大的测试报告分析工具
|
||||
|
||||
@ -53,6 +53,7 @@
|
||||
## 待完成工作
|
||||
|
||||
### 功能增强
|
||||
- ⏳ **响应适配层与灵活验证**: 实现用于处理非标准API响应的适配层,包括动态Schema获取和智能数据提取。
|
||||
- ⏳ 多线程/并行测试执行支持
|
||||
- ⏳ 测试覆盖率分析工具
|
||||
- ⏳ 更强大的LLM回退和缓存机制
|
||||
|
||||
113
memory-bank/response_adaptation_layer_design.md
Normal file
113
memory-bank/response_adaptation_layer_design.md
Normal file
@ -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规范中的自定义字段来驱动,适应性强,无需硬编码。
|
||||
* **代码复用**:将通用的适配逻辑沉淀到工具库中,避免在不同测试用例中重复实现。
|
||||
@ -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;
|
||||
}
|
||||
@ -19,15 +19,28 @@
|
||||
</div>
|
||||
<div class="download-actions">
|
||||
<a href="{{ url_for('download_report', run_id=run_id, filename='summary.json') }}" class="button download-btn">下载JSON报告</a>
|
||||
{% if has_md_report %}
|
||||
<a href="{{ url_for('download_report', run_id=run_id, filename='api_call_details.md') }}" class="button download-btn">下载MD报告</a>
|
||||
{% endif %}
|
||||
{% if has_pdf_report %}
|
||||
<a href="{{ url_for('download_report', run_id=run_id, filename='report_cn.pdf') }}" class="button download-btn">下载PDF报告</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details-container">
|
||||
{% if has_pdf_report %}
|
||||
<h2 id="pdf-section">PDF 报告</h2>
|
||||
<div class="pdf-container">
|
||||
<iframe src="{{ url_for('view_pdf_report', run_id=run_id) }}" width="100%" height="800px" style="border:1px solid #ccc;"></iframe>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2 id="summary-section">测试摘要 (JSON)</h2>
|
||||
<pre class="json-output">{{ summary_content }}</pre>
|
||||
|
||||
|
||||
|
||||
<h2 id="details-section">API 调用详情</h2>
|
||||
<div class="markdown-body">
|
||||
{{ details_content|safe }}
|
||||
@ -35,7 +48,11 @@
|
||||
</div>
|
||||
|
||||
<div class="floating-nav">
|
||||
{% if has_pdf_report %}
|
||||
<a href="#pdf-section" class="button nav-btn" title="跳转到PDF报告">PDF报告</a>
|
||||
{% endif %}
|
||||
<a href="#summary-section" class="button nav-btn" title="跳转到摘要">测试摘要</a>
|
||||
|
||||
<a href="#details-section" class="button nav-btn" title="跳转到详情">API调用详情</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user