add:flask app

This commit is contained in:
gongwenxin 2025-06-06 14:52:08 +08:00
parent b72406df99
commit adc1a0053f
18 changed files with 4726 additions and 3468 deletions

BIN
.DS_Store vendored

Binary file not shown.

83
MANUAL.md Normal file
View File

@ -0,0 +1,83 @@
# 合规性验证工具使用手册
## 1. 用户注册与登录
为了保证测试环境的隔离和安全,所有操作都需要在登录后进行。
### 1.1 注册
首次访问时,您将被重定向到登录页面。点击 "点此注册" 链接进入注册页面。
![注册页面截图](images/register_page.png)
输入您想要的用户名和密码,然后点击 "注册" 按钮。如果用户名未被占用,系统将提示您 "注册成功! 请登录." 并跳转回登录页面。
### 1.2 登录
在登录页面,输入您刚刚注册的用户名和密码,点击 "登录"。
![登录页面截图](images/login_page.png)
登录成功后,您将看到应用的主界面。
## 2. 主界面功能介绍
应用的主界面是您配置和发起测试的核心区域。
![应用主界面截图](images/main_interface.png)
### 2.1 API 规范配置
这是进行测试的首要步骤。
- **API 规范类型**: 选择您的 API 定义文件类型,是 YAPI 还是 Swagger/OpenAPI。
- **YAPI/Swagger 文件路径**:
- 输入 API 规范文件在 **服务器上****绝对路径****相对于 `flask_app.py` 的相对路径**
- 例如,如果文件在 `/data/specs/my_api.json`,则输入该绝对路径。
- 如果文件在项目根目录下的 `assets/doc/api.json`,可以输入 `assets/doc/api.json`
- **加载分类/标签**: 输入文件路径后,点击此按钮。系统会解析文件,并在下方列出文件中定义的所有 API 分类YAPI或标签Swagger供您查阅。
### 2.2 基本配置
- **目标服务 Base URL**: 输入您要测试的 API 服务的基础地址。例如 `http://api.example.com/v1`。框架会将此 URL 与 API 规范中的相对路径拼接成完整的请求地址。
### 2.3 高级配置 (可折叠)
点击 "高级配置" 标题可以展开或收起以下选项,这些选项都有预设的默认值。
- **自定义测试用例目录**: 指向包含自定义测试用例(`BaseAPITestCase` 的子类)的文件夹路径。
- **自定义阶段目录**: 指向包含自定义测试阶段(`BaseAPIStage` 的子类)的文件夹路径。
- **报告输出目录**: 指定生成的测试报告JSON 摘要和 Markdown 详情)要保存到的目录。
默认值分别为 `./custom_testcases`, `./custom_stages`, 和 `./test_reports`
### 2.4 LLM 配置 (可折叠)
点击 "LLM 配置" 标题可以展开或收起此部分。这些配置用于启用和控制使用大语言模型LLM生成测试数据的功能。
- **LLM API Key**: 您的 LLM 服务提供商的 API 密钥。
- **LLM Base URL**: 您的 LLM 服务的 API 地址。
- **LLM 模型名称**: 您要使用的具体模型名称。
- **使用 LLM 生成...**: 勾选相应的复选框,可以启用 LLM 来自动生成请求体、路径参数、查询参数或请求头。
## 3. 执行测试与查看结果
配置完成后,点击页面底部的 "运行测试" 按钮。
![测试结果区域截图](images/results_section.png)
- **日志输出**: 测试过程中的实时日志会显示在此文本框中。
- **测试摘要**: 测试完成后,此处会显示一个总结性的表格,包含成功、失败、总计等信息。
- **报告链接**:
- **摘要报告 (JSON)**: 点击链接可以查看详细的 JSON 格式测试摘要。
- **API 调用详情 (Markdown)**: 点击链接可以下载一个 Markdown 格式的报告,其中包含了每一次 API 调用的详细信息请求头、请求体、响应头、响应体、cURL 命令等),并且每个条目都是可折叠的,方便查阅。
## 4. 查看报告详情
下载的 "API 调用详情 (Markdown)" 文件,可以使用任何支持 Markdown 的编辑器(如 VS Code, Typora打开以获得最佳的阅读体验。
![Markdown 报告截图](images/markdown_report.png)
报告中的每个 API 调用都是一个独立的、可折叠的部分,您可以轻松地展开您关心的失败或成功的请求,查看其所有细节。
---

View File

@ -2,7 +2,8 @@
# build:
# pyinstaller --name ddms_compliance_tool --onefile run_api_tests.py
db:
flask init-db
run:
python run_api_tests.py --base-url http://127.0.0.1:4523/m1/6389742-6086420-default --yapi assets/doc/井筒API示例_simple.json --custom-test-cases-dir ./custom_testcases --verbose --output test_report.json >log.txt 2>&1
run_stages:

Binary file not shown.

View File

@ -1,4 +1,5 @@
import os
import sys
import importlib.util
import inspect
import logging
@ -12,27 +13,38 @@ class TestCaseRegistry:
"""
负责发现加载和管理所有自定义的APITestCase类
"""
def __init__(self, test_cases_dir: str):
def __init__(self, test_cases_dir: Optional[str]):
"""
初始化 TestCaseRegistry
Args:
test_cases_dir: 存放自定义测试用例 (.py 文件) 的目录路径
"""
self.test_cases_dir = test_cases_dir
self.logger = logging.getLogger(__name__)
self.test_cases_dir = test_cases_dir
self._registry: Dict[str, Type[BaseAPITestCase]] = {}
self._test_case_classes: List[Type[BaseAPITestCase]] = []
self.discover_test_cases()
self._discovery_errors: List[str] = []
if self.test_cases_dir:
self.discover_test_cases()
else:
self.logger.info("No custom test cases directory provided. Skipping test case discovery.")
def discover_test_cases(self):
"""
扫描指定目录及其所有子目录动态导入模块并注册所有继承自 BaseAPITestCase 的类
"""
if not os.path.isdir(self.test_cases_dir):
self.logger.warning(f"测试用例目录不存在或不是一个目录: {self.test_cases_dir}")
if not self.test_cases_dir:
self.logger.info("Test cases directory is not set. Skipping discovery.")
return
self.logger.info(f"开始从目录 '{self.test_cases_dir}' 及其子目录发现测试用例...")
if not os.path.isdir(self.test_cases_dir):
self.logger.error(f"Custom test cases directory not found or is not a directory: {self.test_cases_dir}")
self._discovery_errors.append(f"Directory not found: {self.test_cases_dir}")
return
self.logger.info(f"Discovering custom test cases from: {self.test_cases_dir}")
sys.path.insert(0, self.test_cases_dir)
found_count = 0
# 使用 os.walk 进行递归扫描
for root_dir, _, files in os.walk(self.test_cases_dir):

View File

@ -18,6 +18,7 @@ from dataclasses import asdict as dataclass_asdict, is_dataclass
import copy
from collections import defaultdict
from pathlib import Path
from urllib.parse import urljoin # <-- ADDED
from pydantic import BaseModel, Field, create_model, HttpUrl # Added HttpUrl for Literal type hint if needed
from pydantic.networks import EmailStr
@ -425,7 +426,7 @@ class APITestOrchestrator:
"use_for_headers": use_llm_for_headers,
}
if llm_api_key and LLMService:
if llm_api_key and llm_base_url and LLMService: # <-- MODIFIED: Added check for llm_base_url
try:
self.llm_service = LLMService(api_key=llm_api_key, base_url=llm_base_url, model_name=llm_model_name)
self.logger.info(f"LLMService initialized with model: {self.llm_service.model_name}.")
@ -2155,7 +2156,7 @@ class APITestOrchestrator:
final_headers['Content-Type'] = 'application/json'
self.logger.debug(f"{step_log_prefix}: 为JSON请求体设置默认Content-Type: application/json")
full_request_url = self._format_url_with_path_params(api_op_spec.path, final_path_params)
full_request_url = urljoin(self.base_url, self._format_url_with_path_params(api_op_spec.path, final_path_params)) # <-- MODIFIED
api_request_obj = APIRequest(
method=api_op_spec.method,
url=full_request_url,
@ -2488,6 +2489,8 @@ class APITestOrchestrator:
summary.add_stage_result(failure_result)
self.logger.info(f"API Test Stage execution processed. Considered {total_stages_considered_for_execution} (stage_definition x api_group) combinations.")
return summary # <-- ADDED
def _execute_tests_from_parsed_spec(self,
parsed_spec: ParsedAPISpec,
@ -2497,54 +2500,30 @@ class APITestOrchestrator:
custom_test_cases_dir: Optional[str] = None
) -> TestSummary:
"""基于已解析的API规范对象执行测试用例。"""
# Restore the original start of the method body, the rest of the method should be intact from before.
if custom_test_cases_dir and (not self.test_case_registry or not hasattr(self.test_case_registry, 'test_cases_dir') or self.test_case_registry.test_cases_dir != custom_test_cases_dir):
self.logger.info(f"Re-initializing TestCaseRegistry from _execute_tests_from_parsed_spec with new directory: {custom_test_cases_dir}")
try:
# Assuming TestCaseRegistry can be re-initialized or its directory updated.
# If TestCaseRegistry is loaded in __init__, this might need adjustment
# For now, let's assume direct re-init is possible if dir changes.
self.test_case_registry = TestCaseRegistry()
self.test_case_registry.discover_and_load_test_cases(custom_test_cases_dir)
self.logger.info(f"TestCaseRegistry (re)initialized, found {len(self.test_case_registry.get_all_test_case_classes())} test case classes.")
except Exception as e:
self.logger.error(f"Failed to re-initialize TestCaseRegistry from _execute_tests_from_parsed_spec: {e}", exc_info=True)
# summary.finalize_summary() # Finalize might be premature here
return summary # Early exit if registry fails
if custom_test_cases_dir and (not self.test_case_registry or self.test_case_registry.test_cases_dir != custom_test_cases_dir):
self.logger.info(f"Re-initializing TestCaseRegistry with new directory: {custom_test_cases_dir}")
self.test_case_registry = TestCaseRegistry(test_cases_dir=custom_test_cases_dir)
endpoints_to_test: List[Union[YAPIEndpoint, SwaggerEndpoint]] = []
if isinstance(parsed_spec, ParsedYAPISpec):
endpoints_to_test = parsed_spec.endpoints
if categories:
# Ensure YAPIEndpoint has 'category_name' if this filter is used.
endpoints_to_test = [ep for ep in endpoints_to_test if hasattr(ep, 'category_name') and ep.category_name in categories]
elif isinstance(parsed_spec, ParsedSwaggerSpec):
endpoints_to_test = parsed_spec.endpoints
if tags:
# Ensure SwaggerEndpoint has 'tags' attribute for this filter.
endpoints_to_test = [ep for ep in endpoints_to_test if hasattr(ep, 'tags') and isinstance(ep.tags, list) and any(tag in ep.tags for tag in tags)]
else:
self.logger.warning(f"Unknown parsed_spec type: {type(parsed_spec)}. Cannot filter endpoints.")
# summary.finalize_summary() # Finalize might be premature
return summary
current_total_defined = summary.total_endpoints_defined
summary.set_total_endpoints_defined(current_total_defined + len(endpoints_to_test))
total_applicable_tcs_for_this_run = 0
summary.set_total_endpoints_defined(summary.total_endpoints_defined + len(endpoints_to_test))
if self.test_case_registry:
for endpoint_spec_obj in endpoints_to_test:
total_applicable_tcs_for_this_run += len(
self.test_case_registry.get_applicable_test_cases(
endpoint_spec_obj.method.upper(), endpoint_spec_obj.path
)
)
current_total_applicable = summary.total_test_cases_applicable
summary.set_total_test_cases_applicable(current_total_applicable + total_applicable_tcs_for_this_run)
total_applicable_tcs = sum(
len(self.test_case_registry.get_applicable_test_cases(ep.method.upper(), ep.path))
for ep in endpoints_to_test
)
summary.set_total_test_cases_applicable(summary.total_test_cases_applicable + total_applicable_tcs)
for endpoint in endpoints_to_test:
# global_api_spec 应该是包含完整定义的 ParsedYAPISpec/ParsedSwaggerSpec 对象
# 而不是其内部的 .spec 字典,因为 _execute_single_test_case 需要这个对象
result = self.run_test_for_endpoint(endpoint, global_api_spec=parsed_spec)
summary.add_endpoint_result(result)

View File

@ -4,8 +4,12 @@ import json
import logging
import argparse
import traceback # 用于更详细的错误日志
import uuid # For unique filenames
from pathlib import Path
from flask import Flask, request, jsonify, send_from_directory
import sqlite3 # <-- ADDED: For SQLite database
from werkzeug.security import generate_password_hash, check_password_hash # <-- ADDED: For password hashing
from werkzeug.utils import secure_filename # For safe filenames
from flask import Flask, request, jsonify, send_from_directory, session, redirect, url_for, render_template_string, g # <-- MODIFIED: Added session, redirect, url_for, render_template_string
from flask_cors import CORS # 用于处理跨域请求
# 将ddms_compliance_suite的父目录添加到sys.path
@ -17,6 +21,7 @@ from flask_cors import CORS # 用于处理跨域请求
# 或者更具体地添加包含ddms_compliance_suite的目录
# sys.path.insert(0, os.path.join(project_root, 'ddms_compliance_suite'))
from ddms_compliance_suite.api_caller.caller import APICallDetail
from ddms_compliance_suite.test_orchestrator import APITestOrchestrator, TestSummary
from ddms_compliance_suite.input_parser.parser import InputParser, ParsedYAPISpec, ParsedSwaggerSpec
# 从 run_api_tests.py 导入辅助函数 (如果它们被重构为可导入的)
@ -32,12 +37,310 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
# 获取 flask_app.py 脚本所在的目录
APP_ROOT = os.path.dirname(os.path.abspath(__file__))
UPLOAD_FOLDER = os.path.join(APP_ROOT, 'uploads')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
DATABASE = os.path.join(APP_ROOT, 'users.db') # <-- ADDED: Database path
app.config['SECRET_KEY'] = os.urandom(24) # <-- ADDED: Secret key for session management
app.config['DATABASE'] = DATABASE
# --- 数据库辅助函数 ---
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row # Access columns by name
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
def init_db(force_create=False):
"""Initializes the database from schema.sql."""
if force_create or not os.path.exists(DATABASE):
with app.app_context():
db = get_db()
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
logger.info("Database initialized!")
else:
logger.info("Database already exists.")
@app.cli.command('init-db')
def init_db_command():
"""CLI command to initialize the database."""
init_db(force_create=True)
print("Initialized the database.")
# --- 用户认证路由 ---
REGISTER_TEMPLATE = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>注册</title></head>
<body>
<h2>注册新用户</h2>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<form method="post">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required><br><br>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required><br><br>
<input type="submit" value="注册">
</form>
<p>已有账户? <a href="{{ url_for('login') }}">点此登录</a></p>
</body>
</html>
'''
LOGIN_TEMPLATE = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>登录</title></head>
<body>
<h2>请登录</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class=flashes>
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<form method="post">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required><br><br>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required><br><br>
<input type="submit" value="登录">
</form>
<p>没有账户? <a href="{{ url_for('register') }}">点此注册</a></p>
</body>
</html>
'''
@app.route('/register', methods=('GET', 'POST'))
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
if not username:
error = '用户名是必需的.'
elif not password:
error = '密码是必需的.'
if error is None:
try:
db.execute(
"INSERT INTO user (username, password_hash) VALUES (?, ?)",
(username, generate_password_hash(password)),
)
db.commit()
except db.IntegrityError: # Username already exists
error = f"用户 {username} 已被注册."
else:
flash('注册成功! 请登录.')
return redirect(url_for("login"))
flash(error)
return render_template_string(REGISTER_TEMPLATE)
@app.route('/login', methods=('GET', 'POST'))
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
user = db.execute(
'SELECT * FROM user WHERE username = ?',
(username,)
).fetchone()
if user is None:
error = '用户名不存在.'
elif not check_password_hash(user['password_hash'], password):
error = '密码错误.'
if error is None:
session.clear()
session['user_id'] = user['id']
session['username'] = user['username']
flash('登录成功!', 'success')
return redirect(url_for('serve_index'))
flash(error, 'error')
# If user is already logged in, redirect to index
if 'user_id' in session:
return redirect(url_for('serve_index'))
return render_template_string(LOGIN_TEMPLATE)
@app.route('/logout')
def logout():
session.clear()
flash('您已成功登出.')
return redirect(url_for('login'))
# --- 应用辅助函数和路由保护 ---
from functools import wraps
from flask import g, flash # ensure g and flash are imported
def login_required(view):
@wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
return redirect(url_for('login'))
return view(**kwargs)
return wrapped_view
@app.before_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = get_db().execute(
'SELECT * FROM user WHERE id = ?', (user_id,)
).fetchone()
# --- 辅助函数 ---
def save_api_call_details_to_file(api_call_details: list[APICallDetail], output_dir_path_str: str, filename: str = "api_call_details.md"):
"""将API调用详情保存到Markdown文件。"""
if not api_call_details:
logger.info("没有API调用详情可保存。")
return None
output_dir = Path(output_dir_path_str)
output_dir.mkdir(parents=True, exist_ok=True)
file_path = output_dir / filename
unique_id_counter = 0
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write("# API 调用详情记录\\n\\n")
for detail_obj in api_call_details:
unique_id_counter +=1
unique_id = f"api-call-{unique_id_counter}"
# Convert elapsed time from seconds to milliseconds for display
elapsed_ms = detail_obj.response_elapsed_time * 1000
f.write(f"<details id='{unique_id}'>\\n")
f.write(f"<summary><b>{detail_obj.request_method.upper()}</b> {detail_obj.request_url} - <b>状态: {detail_obj.response_status_code}</b> - 耗时: {elapsed_ms:.2f}ms</summary>\\n\\n")
# 请求详情
f.write("#### 请求 (Request)\\n")
f.write(f"- **Method:** `{detail_obj.request_method.upper()}`\\n")
f.write(f"- **URL:** `{detail_obj.request_url}`\\n")
if detail_obj.request_headers:
f.write("- **Headers:**\\n")
f.write("```json\\n")
f.write(json.dumps(detail_obj.request_headers, indent=2, ensure_ascii=False) + "\\n")
f.write("```\\n")
if detail_obj.request_params:
f.write("- **Query Parameters:**\\n")
f.write("```json\\n")
f.write(json.dumps(detail_obj.request_params, indent=2, ensure_ascii=False) + "\\n")
f.write("```\\n")
if detail_obj.request_body:
f.write("- **Request Body:**\\n")
f.write("```json\\n") # 假设请求体是JSON如果不是则可能需要调整
# 尝试解析为JSON如果失败则按原样写入
try:
body_json = json.loads(detail_obj.request_body) if isinstance(detail_obj.request_body, str) else detail_obj.request_body
f.write(json.dumps(body_json, indent=2, ensure_ascii=False) + "\\n")
except json.JSONDecodeError:
f.write(str(detail_obj.request_body) + "\\n") # Fallback to string
f.write("```\\n")
f.write(f"- **cURL Command:**\\n")
f.write("```bash\\n")
f.write(detail_obj.curl_command + "\\n")
f.write("```\\n\\n")
# 响应详情
f.write("#### 响应 (Response)\\n")
f.write(f"- **Status Code:** `{detail_obj.response_status_code}`\\n")
f.write(f"- **Elapsed Time:** {elapsed_ms:.2f} ms\\n")
if detail_obj.response_headers:
f.write("- **Response Headers:**\\n")
f.write("```json\\n")
f.write(json.dumps(detail_obj.response_headers, indent=2, ensure_ascii=False) + "\\n")
f.write("```\\n")
if detail_obj.response_body:
f.write("- **Response Body:**\\n")
# 首先尝试格式化为JSON如果失败则保持原样
try:
# 假设 response_body 是字符串或者可以被json.loads处理的字节串
body_to_write = detail_obj.response_body
if isinstance(body_to_write, bytes):
try:
body_to_write = body_to_write.decode('utf-8')
except UnicodeDecodeError:
body_to_write = str(body_to_write) # Fallback if not UTF-8
if isinstance(body_to_write, str):
try:
parsed_json = json.loads(body_to_write)
f.write("```json\\n")
f.write(json.dumps(parsed_json, indent=2, ensure_ascii=False) + "\\n")
f.write("```\\n")
except json.JSONDecodeError: # Not a JSON string
f.write("```text\\n") # Treat as plain text
f.write(body_to_write + "\\n")
f.write("```\\n")
else: # Already a dict/list (shouldn't happen if APICallDetail.response_body is str/bytes)
f.write("```json\\n")
f.write(json.dumps(body_to_write, indent=2, ensure_ascii=False) + "\\n")
f.write("```\\n")
except Exception as e_resp_body:
logger.error(f"Error processing response body for API call to {detail_obj.request_url}: {e_resp_body}")
f.write("```text\\n")
f.write(f"(Error processing response body: {e_resp_body})\\n")
f.write(str(detail_obj.response_body) + "\\n") # Fallback
f.write("```\\n")
else:
f.write("- Response Body: (empty)\\n")
f.write("\\n</details>\\n\\n")
f.write("---\\n\\n") # Separator
logger.info(f"API 调用详情已成功保存到: {file_path}")
return str(file_path)
except IOError as e:
logger.error(f"保存 API 调用详情到文件时发生IO错误 '{file_path}': {e}", exc_info=True)
return None
except Exception as e:
logger.error(f"保存 API 调用详情时发生未知错误 '{file_path}': {e}", exc_info=True)
return None
def get_orchestrator_from_config(config: dict) -> APITestOrchestrator:
"""根据配置字典实例化APITestOrchestrator"""
return APITestOrchestrator(
base_url=config.get('base_url', ''),
custom_test_cases_dir=config.get('custom_test_cases_dir'),
stages_dir=config.get('stages_dir'),
llm_api_key=config.get('llm_api_key'),
llm_base_url=config.get('llm_base_url'),
llm_model_name=config.get('llm_model_name'),
@ -50,159 +353,182 @@ def get_orchestrator_from_config(config: dict) -> APITestOrchestrator:
# --- API 端点 ---
@app.route('/')
@login_required # Protect the main page
def serve_index():
# Initialize DB if it doesn't exist when first accessing the app
# This is a simple way, for production you might want a separate init step.
if not os.path.exists(DATABASE):
init_db(force_create=True)
return send_from_directory(app.static_folder, 'index.html')
@app.route('/run-tests', methods=['POST'])
@login_required # Protect this endpoint
def run_tests_endpoint():
logger.info("Received request to run tests.")
output_dir = None # To hold the path for report links
temp_spec_path = None # To hold the path for the uploaded spec file
try:
config_data = request.json
if not config_data:
return jsonify({"error": "Request body must be JSON"}), 400
# The form is now sent as multipart/form-data
config_data = request.form.to_dict()
logger.info(f"Received config: {config_data}")
logger.info(f"接收到测试运行请求: {config_data}")
# Handle file upload
if 'api_spec_file' not in request.files:
logger.error("API spec file part is missing from the request.")
return jsonify({"error": "API spec file part is missing"}), 400
file = request.files['api_spec_file']
if file.filename == '':
logger.error("No API spec file selected.")
return jsonify({"error": "No API spec file selected"}), 400
# 校验必需参数
if not config_data.get('base_url'):
return jsonify({"error": "'base_url' is required"}), 400
if not config_data.get('yapi_file_path') and not config_data.get('swagger_file_path'):
return jsonify({"error": "Either 'yapi_file_path' or 'swagger_file_path' is required"}), 400
if file:
filename = secure_filename(file.filename)
# Create a unique filename to avoid conflicts
unique_filename = f"{uuid.uuid4()}_{filename}"
temp_spec_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
file.save(temp_spec_path)
logger.info(f"Saved uploaded spec file to {temp_spec_path}")
else:
# This case should ideally not be reached if the above checks are in place
return jsonify({"error": "Invalid file object received"}), 400
# Create orchestrator from form data
orchestrator = get_orchestrator_from_config(config_data)
summary = TestSummary() # 为本次运行创建新的摘要
# Prepare summary object
summary = TestSummary()
# Determine API spec type and parse the uploaded file
api_spec_type = config_data.get('api_spec_type', 'YAPI')
logger.info(f"API Spec Type: {api_spec_type}")
parser = InputParser()
parsed_spec = None
api_spec_type = ""
if config_data.get('yapi_file_path'):
api_spec_type = "YAPI"
yapi_path = config_data['yapi_file_path']
if not os.path.isabs(yapi_path):
yapi_path = os.path.join(os.getcwd(), yapi_path) # 假设相对路径相对于服务器工作目录
if not os.path.exists(yapi_path):
return jsonify({"error": f"YAPI file not found: {yapi_path}"}), 400
logger.info(f"解析YAPI文件: {yapi_path}")
parsed_spec = orchestrator.parser.parse_yapi_spec(yapi_path)
if not parsed_spec:
logger.error(f"解析YAPI文件失败: {yapi_path}")
return jsonify({"error": f"Failed to parse YAPI file: {yapi_path}"}), 500
if api_spec_type == "YAPI":
parsed_spec = parser.parse_yapi_spec(temp_spec_path)
elif api_spec_type == "Swagger":
parsed_spec = parser.parse_swagger_spec(temp_spec_path)
elif config_data.get('swagger_file_path'):
api_spec_type = "Swagger/OpenAPI"
swagger_path = config_data['swagger_file_path']
if not os.path.isabs(swagger_path):
swagger_path = os.path.join(os.getcwd(), swagger_path) # 假设相对路径
if not os.path.exists(swagger_path):
return jsonify({"error": f"Swagger file not found: {swagger_path}"}), 400
logger.info(f"解析Swagger/OpenAPI文件: {swagger_path}")
parsed_spec = orchestrator.parser.parse_swagger_spec(swagger_path)
if not parsed_spec:
logger.error(f"解析Swagger文件失败: {swagger_path}")
return jsonify({"error": f"Failed to parse Swagger file: {swagger_path}"}), 500
if not parsed_spec:
error_msg = f"Failed to parse the uploaded {api_spec_type} file."
logger.error(error_msg)
return jsonify({"error": error_msg}), 400
logger.info("Successfully parsed API specification.")
# 执行测试用例
logger.info(f"开始从已解析的 {api_spec_type} 规范执行测试用例...")
# Execute tests from parsed spec
logger.info(f"Starting test execution from parsed {api_spec_type} spec...")
summary = orchestrator._execute_tests_from_parsed_spec(
parsed_spec=parsed_spec,
summary=summary,
categories=config_data.get('categories'), # 逗号分隔的字符串,需要转为列表
tags=config_data.get('tags'), # 同上
categories=config_data.get('categories').split(',') if config_data.get('categories') else None,
tags=config_data.get('tags').split(',') if config_data.get('tags') else None,
custom_test_cases_dir=config_data.get('custom_test_cases_dir')
)
logger.info("测试用例执行完成。")
logger.info("Test case execution finished.")
# 执行场景测试 (如果指定了目录)
scenarios_dir = config_data.get('scenarios_dir')
if scenarios_dir and parsed_spec:
if not os.path.isabs(scenarios_dir):
scenarios_dir = os.path.join(os.getcwd(), scenarios_dir)
logger.info(f"开始执行API场景测试目录: {scenarios_dir}")
orchestrator.run_scenarios_from_spec(
scenarios_dir=scenarios_dir,
parsed_spec=parsed_spec,
summary=summary
)
logger.info("API场景测试执行完毕。")
# Execute test stages
logger.info("Starting stage execution...")
summary = orchestrator.run_stages_from_spec(
parsed_spec=parsed_spec,
summary=summary
)
logger.info("Stage execution finished.")
# Handle output and reporting
output_dir = config_data.get('output_dir', './test_reports')
if not os.path.isabs(output_dir):
output_dir = os.path.join(APP_ROOT, output_dir)
summary.finalize_summary() # 最终确定摘要,计算总时长等
# summary.print_summary_to_console() # 后端服务通常不直接打印到控制台
os.makedirs(output_dir, exist_ok=True)
summary_path = os.path.join(output_dir, "test_summary.json")
details_path = os.path.join(output_dir, "api_call_details.md")
# 可以在这里决定如何保存报告,例如保存到 output_dir (如果提供)
output_dir_path_str = config_data.get('output_dir')
main_report_file_path_str = ""
api_calls_output_path_str = ""
api_calls_filename = "api_call_details.md"
if output_dir_path_str:
output_path = Path(output_dir_path_str)
output_path.mkdir(parents=True, exist_ok=True)
main_report_file_path = output_path / f"summary_report.json" # 默认保存为json
main_report_file_path_str = str(main_report_file_path)
with open(main_report_file_path, 'w', encoding='utf-8') as f:
f.write(summary.to_json(pretty=True))
logger.info(f"主测试报告已保存到: {main_report_file_path}")
# 保存API调用详情
api_calls_output_path_str = str(output_path)
# (需要从 run_api_tests.py 移植 save_api_call_details_to_file 或类似功能)
# 暂时跳过保存 api_call_details 文件,因为 orchestrator.get_api_call_details() 需要被调用
# 并且保存逻辑也需要移植。
# save_api_call_details_to_file(orchestrator.get_api_call_details(), api_calls_output_path_str, api_calls_filename)
with open(summary_path, 'w', encoding='utf-8') as f:
f.write(summary.to_json(pretty=True))
logger.info(f"Test summary saved to {summary_path}")
save_api_call_details_to_file(orchestrator.get_api_call_details(), output_dir)
logger.info(f"API call details saved to {details_path}")
return jsonify({
"message": "测试执行完成。",
"summary": summary.to_dict(),
"report_file": main_report_file_path_str, # 报告文件路径(如果保存了)
# "api_calls_file": api_calls_output_path_str + "/" + api_calls_filename # API调用详情文件路径
}), 200
"summary_report_path": f"/download/{os.path.basename(output_dir)}/test_summary.json",
"details_report_path": f"/download/{os.path.basename(output_dir)}/api_call_details.md"
})
except Exception as e:
logger.error(f"执行测试时发生错误: {e}\n{traceback.format_exc()}")
return jsonify({"error": f"执行测试时发生内部错误: {str(e)}"}), 500
error_msg = f"An unexpected error occurred during testing: {traceback.format_exc()}"
logger.error(error_msg)
return jsonify({"error": error_msg}), 500
finally:
# Clean up the uploaded file
if temp_spec_path and os.path.exists(temp_spec_path):
os.remove(temp_spec_path)
logger.info(f"Cleaned up temporary file: {temp_spec_path}")
@app.route('/list-yapi-categories', methods=['POST'])
@login_required
def list_yapi_categories_endpoint():
if 'api_spec_file' not in request.files:
return jsonify({"error": "api_spec_file part is missing"}), 400
file = request.files['api_spec_file']
if file.filename == '':
return jsonify({"error": "No file selected"}), 400
temp_spec_path = None
try:
data = request.json
yapi_file = data.get('yapi_file_path')
if not yapi_file:
return jsonify({"error": "'yapi_file_path' is required"}), 400
if not os.path.isabs(yapi_file):
yapi_file = os.path.join(os.getcwd(), yapi_file)
if not os.path.exists(yapi_file):
return jsonify({"error": f"YAPI file not found: {yapi_file}"}), 400
filename = secure_filename(file.filename)
unique_filename = f"{uuid.uuid4()}_{filename}"
temp_spec_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
file.save(temp_spec_path)
parser = InputParser()
parsed_yapi = parser.parse_yapi_spec(yapi_file)
if not parsed_yapi or not parsed_yapi.categories:
parsed_yapi = parser.parse_yapi_spec(temp_spec_path)
if not parsed_yapi or not hasattr(parsed_yapi, 'categories') or not parsed_yapi.categories:
return jsonify({"error": "Failed to parse YAPI categories or no categories found"}), 500
categories_list = [
{"name": cat.get('name', '未命名'), "description": cat.get('desc', '无描述')}
{
"name": cat.get('name', '未命名'),
"description": cat.get('desc') if cat.get('desc') else cat.get('description') if cat.get('description') else '无描述'
}
for cat in parsed_yapi.categories
]
return jsonify(categories_list), 200
except Exception as e:
logger.error(f"列出YAPI分类时出错: {e}\n{traceback.format_exc()}")
return jsonify({"error": f"处理YAPI分类列表时出错: {str(e)}"}), 500
logger.error(f"Error fetching YAPI categories: {traceback.format_exc()}")
return jsonify({"error": str(e)}), 500
finally:
if temp_spec_path and os.path.exists(temp_spec_path):
os.remove(temp_spec_path)
@app.route('/list-swagger-tags', methods=['POST'])
@login_required
def list_swagger_tags_endpoint():
try:
data = request.json
swagger_file = data.get('swagger_file_path')
if not swagger_file:
return jsonify({"error": "'swagger_file_path' is required"}), 400
if 'api_spec_file' not in request.files:
return jsonify({"error": "api_spec_file part is missing"}), 400
file = request.files['api_spec_file']
if file.filename == '':
return jsonify({"error": "No file selected"}), 400
if not os.path.isabs(swagger_file):
swagger_file = os.path.join(os.getcwd(), swagger_file)
if not os.path.exists(swagger_file):
return jsonify({"error": f"Swagger file not found: {swagger_file}"}), 400
temp_spec_path = None
try:
filename = secure_filename(file.filename)
unique_filename = f"{uuid.uuid4()}_{filename}"
temp_spec_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
file.save(temp_spec_path)
parser = InputParser()
parsed_swagger = parser.parse_swagger_spec(swagger_file)
if not parsed_swagger or not parsed_swagger.tags:
parsed_swagger = parser.parse_swagger_spec(temp_spec_path)
if not parsed_swagger or not hasattr(parsed_swagger, 'tags') or not parsed_swagger.tags:
return jsonify({"error": "Failed to parse Swagger tags or no tags found"}), 500
tags_list = [
@ -211,9 +537,33 @@ def list_swagger_tags_endpoint():
]
return jsonify(tags_list), 200
except Exception as e:
logger.error(f"列出Swagger标签时出错: {e}\n{traceback.format_exc()}")
return jsonify({"error": f"处理Swagger标签列表时出错: {str(e)}"}), 500
logger.error(f"Error fetching Swagger tags: {traceback.format_exc()}")
return jsonify({"error": str(e)}), 500
finally:
if temp_spec_path and os.path.exists(temp_spec_path):
os.remove(temp_spec_path)
@app.route('/download/<path:filepath>')
@login_required
def download_file(filepath):
"""Serve files from a designated reports directory."""
# This is a simplified download endpoint. For production, consider:
# - More robust security checks on the filepath.
# - Configuring the reports directory from a central config.
reports_base_dir = os.path.join(APP_ROOT, 'test_reports')
# Basic security check to prevent directory traversal
if '..' in filepath or os.path.isabs(filepath):
from flask import abort
abort(404)
logger.info(f"Attempting to serve file: {filepath} from directory: {reports_base_dir}")
return send_from_directory(reports_base_dir, filepath, as_attachment=True)
if __name__ == '__main__':
# 注意在生产环境中应使用Gunicorn或uWSGI等WSGI服务器运行Flask应用
# For initial setup, you might need to run the init_db function once.
# You can do this by running flask --app flask_app init-db in your terminal
# or by uncommenting the line below for the very first run:
# init_db(force_create=False)
app.run(debug=True, host='0.0.0.0', port=5050)

View File

@ -15,4 +15,6 @@ prance[osv]>=23.0.0,<24.0.0
# 测试框架 (可选, 推荐)
# pytest>=7.0.0,<8.0.0
# pytest-cov>=4.0.0,<5.0.0
# httpx>=0.20.0,<0.28.0 # for testing API calls
# httpx>=0.20.0,<0.28.0 # for testing API calls
Flask-Cors>=3.0

7
schema.sql Normal file
View File

@ -0,0 +1,7 @@
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL
);

View File

@ -12,7 +12,7 @@
<div class="configuration-section">
<h2>测试配置</h2>
<form id="test-config-form">
<form id="test-form">
<div class="form-group">
<label for="base_url">API 基础URL (必填):</label>
<input type="text" id="base_url" name="base_url" required placeholder="例如http://localhost:8080/api/v1">
@ -21,73 +21,85 @@
<fieldset>
<legend>API 定义源 (选择一个)</legend>
<div class="form-group">
<label for="yapi_file_path">YAPI 文件路径:</label>
<input type="text" id="yapi_file_path" name="yapi_file_path" placeholder="例如:./assets/doc/yapi_spec.json">
<button type="button" class="action-button" onclick="fetchYapiCategories()">加载分类</button>
<div id="yapi-categories-container" class="categories-tags-container"></div>
<label for="api_spec_type">API 规范类型:</label>
<select id="api_spec_type" name="api_spec_type">
<option value="YAPI">YAPI (.json)</option>
<option value="Swagger">Swagger/OpenAPI (.json, .yaml)</option>
</select>
</div>
<div class="form-group">
<label for="swagger_file_path">Swagger/OpenAPI 文件路径:</label>
<input type="text" id="swagger_file_path" name="swagger_file_path" placeholder="例如:./assets/doc/swagger_spec.json">
<button type="button" class="action-button" onclick="fetchSwaggerTags()">加载标签</button>
<div id="swagger-tags-container" class="categories-tags-container"></div>
<label for="api_spec_file">上传 API 规范文件:</label>
<input type="file" id="api_spec_file" name="api_spec_file" accept=".json,.yaml,.yml" required>
</div>
<div class="form-group">
<button type="button" id="load-spec-btn">加载分类/标签</button>
</div>
<div id="yapi-categories-container" class="checkbox-container"></div>
<div id="swagger-tags-container" class="checkbox-container"></div>
</fieldset>
<div class="form-group">
<label for="custom_test_cases_dir">自定义测试用例目录:</label>
<input type="text" id="custom_test_cases_dir" name="custom_test_cases_dir" placeholder="例如:./custom_testcases">
</div>
<div class="form-group">
<label for="scenarios_dir">自定义场景目录:</label>
<input type="text" id="scenarios_dir" name="scenarios_dir" placeholder="例如:./custom_scenarios">
</div>
<div class="form-group">
<label for="output_dir">报告输出目录:</label>
<input type="text" id="output_dir" name="output_dir" placeholder="例如:./test_reports">
</div>
<details>
<summary>高级配置 (点击展开)</summary>
<div class="form-group">
<label for="custom_test_cases_dir">自定义测试用例目录:</label>
<input type="text" id="custom_test_cases_dir" name="custom_test_cases_dir" placeholder="例如:./custom_testcases" value="./custom_testcases">
</div>
<div class="form-group">
<label for="stages_dir">自定义阶段目录:</label>
<input type="text" id="stages_dir" name="stages_dir" placeholder="例如:./custom_stages" value="./custom_stages">
</div>
<div class="form-group">
<label for="output_dir">报告输出目录:</label>
<input type="text" id="output_dir" name="output_dir" placeholder="例如:./test_reports" value="./test_reports">
</div>
</details>
<fieldset>
<legend>LLM 配置 (可选)</legend>
<div class="form-group">
<label for="llm_api_key">LLM API Key:</label>
<input type="password" id="llm_api_key" name="llm_api_key" placeholder="留空则尝试读取环境变量">
</div>
<div class="form-group">
<label for="llm_base_url">LLM Base URL:</label>
<input type="text" id="llm_base_url" name="llm_base_url" placeholder="例如https://dashscope.aliyuncs.com/compatible-mode/v1">
</div>
<div class="form-group">
<label for="llm_model_name">LLM 模型名称:</label>
<input type="text" id="llm_model_name" name="llm_model_name" placeholder="例如qwen-plus">
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_request_body" name="use_llm_for_request_body">
<label for="use_llm_for_request_body">使用LLM生成请求体</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_path_params" name="use_llm_for_path_params">
<label for="use_llm_for_path_params">使用LLM生成路径参数</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_query_params" name="use_llm_for_query_params">
<label for="use_llm_for_query_params">使用LLM生成查询参数</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_headers" name="use_llm_for_headers">
<label for="use_llm_for_headers">使用LLM生成头部参数</label>
</div>
</fieldset>
<details>
<summary>LLM 配置 (可选, 点击展开)</summary>
<fieldset>
<legend>LLM 配置 (可选)</legend>
<div class="form-group">
<label for="llm_api_key">LLM API Key:</label>
<input type="password" id="llm_api_key" name="llm_api_key" placeholder="留空则尝试读取环境变量">
</div>
<div class="form-group">
<label for="llm_base_url">LLM Base URL:</label>
<input type="text" id="llm_base_url" name="llm_base_url" placeholder="例如https://dashscope.aliyuncs.com/compatible-mode/v1">
</div>
<div class="form-group">
<label for="llm_model_name">LLM 模型名称:</label>
<input type="text" id="llm_model_name" name="llm_model_name" placeholder="例如qwen-plus">
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_request_body" name="use_llm_for_request_body">
<label for="use_llm_for_request_body">使用LLM生成请求体</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_path_params" name="use_llm_for_path_params">
<label for="use_llm_for_path_params">使用LLM生成路径参数</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_query_params" name="use_llm_for_query_params">
<label for="use_llm_for_query_params">使用LLM生成查询参数</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="use_llm_for_headers" name="use_llm_for_headers">
<label for="use_llm_for_headers">使用LLM生成头部参数</label>
</div>
</fieldset>
</details>
<button type="submit" class="submit-button">运行测试</button>
</form>
</div>
<div class="results-section">
<h2>测试状态与结果</h2>
<div id="status-area">等待配置并运行测试...</div>
<pre id="results-output"></pre>
<div id="report-link-area"></div>
<h2>测试日志与结果</h2>
<label for="log-output">实时日志:</label>
<textarea id="log-output" readonly style="width:100%"></textarea>
<div id="results-container">
<!-- 测试结果将在此处动态生成 -->
</div>
</div>
</div>

View File

@ -1,140 +1,139 @@
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('test-config-form');
const statusArea = document.getElementById('status-area');
const resultsOutput = document.getElementById('results-output');
const reportLinkArea = document.getElementById('report-link-area');
const form = document.getElementById('test-form');
const logOutput = document.getElementById('log-output');
const resultsContainer = document.getElementById('results-container');
const loadSpecBtn = document.getElementById('load-spec-btn');
const apiSpecFileInput = document.getElementById('api_spec_file');
const apiSpecTypeSelect = document.getElementById('api_spec_type');
const yapiCategoriesContainer = document.getElementById('yapi-categories-container');
const swaggerTagsContainer = document.getElementById('swagger-tags-container');
// Make the log output area larger as per user request
if (logOutput) {
logOutput.rows = 25;
}
// Event listener for the new "Load Categories/Tags" button
if (loadSpecBtn) {
loadSpecBtn.addEventListener('click', async () => {
const specType = apiSpecTypeSelect.value;
const file = apiSpecFileInput.files[0];
if (!file) {
alert('请先选择一个 API 规范文件。');
return;
}
const formData = new FormData();
formData.append('api_spec_file', file);
let url = '';
let container = null;
if (specType === 'YAPI') {
url = '/list-yapi-categories';
container = yapiCategoriesContainer;
swaggerTagsContainer.style.display = 'none';
yapiCategoriesContainer.style.display = 'block';
} else { // Swagger
url = '/list-swagger-tags';
container = swaggerTagsContainer;
yapiCategoriesContainer.style.display = 'none';
swaggerTagsContainer.style.display = 'block';
}
container.innerHTML = '<p>正在加载...</p>';
try {
const response = await fetch(url, {
method: 'POST',
body: formData, // Send FormData directly
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: '无法解析错误响应' }));
throw new Error(errorData.error || `请求 ${specType} 分类/标签时出错`);
}
const data = await response.json();
renderItemsList(container, data, specType); // Using a simplified renderer
} catch (error) {
console.error(`请求${specType}分类时出错:`, error);
container.innerHTML = `<p class="error">加载失败: ${error.message}</p>`;
}
});
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
statusArea.textContent = '正在运行测试,请稍候...';
resultsOutput.textContent = '';
reportLinkArea.innerHTML = '';
logOutput.value = '正在开始测试...\n';
resultsContainer.innerHTML = '';
// FormData will correctly handle all form fields, including the file upload
const formData = new FormData(form);
const config = {};
formData.forEach((value, key) => {
// 处理复选框
if (key.startsWith('use_llm_for_')) {
config[key] = form.elements[key].checked;
} else if (value.trim() !== '') { // 只添加非空值
config[key] = value.trim();
}
});
// 如果复选框未被选中FormData 不会包含它们,所以要确保它们是 false
['use_llm_for_request_body', 'use_llm_for_path_params', 'use_llm_for_query_params', 'use_llm_for_headers'].forEach(key => {
if (!(key in config)) {
config[key] = false;
}
});
// 从 YAPI 分类和 Swagger 标签中获取选中的项
const selectedYapiCategories = Array.from(document.querySelectorAll('#yapi-categories-container input[type="checkbox"]:checked'))
.map(cb => cb.value);
if (selectedYapiCategories.length > 0) {
config['categories'] = selectedYapiCategories.join(',');
}
const selectedSwaggerTags = Array.from(document.querySelectorAll('#swagger-tags-container input[type="checkbox"]:checked'))
.map(cb => cb.value);
if (selectedSwaggerTags.length > 0) {
config['tags'] = selectedSwaggerTags.join(',');
}
try {
const response = await fetch('/run-tests', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
// For FormData, the browser sets the Content-Type to multipart/form-data with the correct boundary.
// Do not set the 'Content-Type' header manually.
body: formData,
});
const result = await response.json();
if (response.ok) {
statusArea.textContent = `测试完成: ${result.message || '成功'}`;
resultsOutput.textContent = JSON.stringify(result.summary, null, 2);
if (result.report_file) {
reportLinkArea.innerHTML = `<p>测试报告已保存到: <strong>${result.report_file}</strong></p>`;
}
} else {
statusArea.textContent = `测试失败: ${result.error || '未知错误'}`;
resultsOutput.textContent = JSON.stringify(result, null, 2);
if (!response.ok) {
// Try to parse the error, provide a fallback message.
const errorMessage = result.error || '运行测试时发生未知错误';
logOutput.value += `\n错误: ${errorMessage}`;
throw new Error(errorMessage);
}
// Restore summary to the log output
logOutput.value += '\n测试执行完成。\n\n';
logOutput.value += '--- 测试摘要 ---\n';
logOutput.value += JSON.stringify(result.summary, null, 2);
displayResults(result);
} catch (error) {
statusArea.textContent = '运行测试时发生网络错误或服务器内部错误。';
resultsOutput.textContent = error.toString();
console.error('运行测试出错:', error);
console.error('运行测试时捕获到错误:', error);
logOutput.value += `\n\n发生严重错误: ${error.message}`;
resultsContainer.innerHTML = `<p class="error">测试运行失败: ${error.message}</p>`;
}
});
// A simplified function to render categories/tags as a list
function renderItemsList(container, items, type) {
if (!items || items.length === 0) {
container.innerHTML = '<p>未找到任何项。</p>';
return;
}
let html = `<h4>${type} ${type === 'YAPI' ? '分类' : '标签'}:</h4><ul>`;
items.forEach(item => {
html += `<li><strong>${item.name}</strong>: ${item.description || '无描述'}</li>`;
});
html += '</ul>';
container.innerHTML = html;
}
function displayResults(result) {
// Per user request, only show download links and remove the summary view.
let linksHtml = '<h3>下载报告</h3>';
if (result.summary_report_path) {
linksHtml += `<p><a href="${result.summary_report_path}" target="_blank" class="report-link">摘要报告 (JSON)</a></p>`;
}
if (result.details_report_path) {
linksHtml += `<p><a href="${result.details_report_path}" target="_blank" class="report-link">API 调用详情 (Markdown)</a></p>`;
}
if (!result.summary_report_path && !result.details_report_path) {
linksHtml += '<p>没有可用的报告文件。</p>';
}
resultsContainer.innerHTML = linksHtml;
}
});
async function fetchYapiCategories() {
const yapiFilePath = document.getElementById('yapi_file_path').value;
const container = document.getElementById('yapi-categories-container');
container.innerHTML = '正在加载分类...';
if (!yapiFilePath) {
container.innerHTML = '<p style="color: red;">请输入YAPI文件路径。</p>';
return;
}
try {
const response = await fetch('/list-yapi-categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ yapi_file_path: yapiFilePath })
});
const categories = await response.json();
if (response.ok) {
renderCheckboxes(container, categories, 'yapi_category');
} else {
container.innerHTML = `<p style="color: red;">加载YAPI分类失败: ${categories.error || '未知错误'}</p>`;
}
} catch (error) {
container.innerHTML = `<p style="color: red;">请求YAPI分类时出错: ${error}</p>`;
}
}
async function fetchSwaggerTags() {
const swaggerFilePath = document.getElementById('swagger_file_path').value;
const container = document.getElementById('swagger-tags-container');
container.innerHTML = '正在加载标签...';
if (!swaggerFilePath) {
container.innerHTML = '<p style="color: red;">请输入Swagger文件路径。</p>';
return;
}
try {
const response = await fetch('/list-swagger-tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ swagger_file_path: swaggerFilePath })
});
const tags = await response.json();
if (response.ok) {
renderCheckboxes(container, tags, 'swagger_tag');
} else {
container.innerHTML = `<p style="color: red;">加载Swagger标签失败: ${tags.error || '未知错误'}</p>`;
}
} catch (error) {
container.innerHTML = `<p style="color: red;">请求Swagger标签时出错: ${error}</p>`;
}
}
function renderCheckboxes(container, items, groupName) {
if (!items || items.length === 0) {
container.innerHTML = '<p>未找到任何项。</p>';
return;
}
let html = items.map((item, index) => {
const id = `${groupName}_${index}`;
return `<div>
<input type="checkbox" id="${id}" name="${groupName}[]" value="${item.name}">
<label for="${id}">${item.name} ${item.description ? '(' + item.description + ')' : ''}</label>
</div>`;
}).join('');
container.innerHTML = html;
}
// The old functions fetchYapiCategories, fetchSwaggerTags, and renderCheckboxes are no longer needed
// and should be removed if they exist elsewhere in this file.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
users.db Normal file

Binary file not shown.