add:flask app
This commit is contained in:
parent
b72406df99
commit
adc1a0053f
83
MANUAL.md
Normal file
83
MANUAL.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# 合规性验证工具使用手册
|
||||||
|
|
||||||
|
## 1. 用户注册与登录
|
||||||
|
|
||||||
|
为了保证测试环境的隔离和安全,所有操作都需要在登录后进行。
|
||||||
|
|
||||||
|
### 1.1 注册
|
||||||
|
|
||||||
|
首次访问时,您将被重定向到登录页面。点击 "点此注册" 链接进入注册页面。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
输入您想要的用户名和密码,然后点击 "注册" 按钮。如果用户名未被占用,系统将提示您 "注册成功! 请登录." 并跳转回登录页面。
|
||||||
|
|
||||||
|
### 1.2 登录
|
||||||
|
|
||||||
|
在登录页面,输入您刚刚注册的用户名和密码,点击 "登录"。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
登录成功后,您将看到应用的主界面。
|
||||||
|
|
||||||
|
## 2. 主界面功能介绍
|
||||||
|
|
||||||
|
应用的主界面是您配置和发起测试的核心区域。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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. 执行测试与查看结果
|
||||||
|
|
||||||
|
配置完成后,点击页面底部的 "运行测试" 按钮。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **日志输出**: 测试过程中的实时日志会显示在此文本框中。
|
||||||
|
- **测试摘要**: 测试完成后,此处会显示一个总结性的表格,包含成功、失败、总计等信息。
|
||||||
|
- **报告链接**:
|
||||||
|
- **摘要报告 (JSON)**: 点击链接可以查看详细的 JSON 格式测试摘要。
|
||||||
|
- **API 调用详情 (Markdown)**: 点击链接可以下载一个 Markdown 格式的报告,其中包含了每一次 API 调用的详细信息(请求头、请求体、响应头、响应体、cURL 命令等),并且每个条目都是可折叠的,方便查阅。
|
||||||
|
|
||||||
|
## 4. 查看报告详情
|
||||||
|
|
||||||
|
下载的 "API 调用详情 (Markdown)" 文件,可以使用任何支持 Markdown 的编辑器(如 VS Code, Typora)打开,以获得最佳的阅读体验。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
报告中的每个 API 调用都是一个独立的、可折叠的部分,您可以轻松地展开您关心的失败或成功的请求,查看其所有细节。
|
||||||
|
|
||||||
|
---
|
||||||
3
Makefile
3
Makefile
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
# build:
|
# build:
|
||||||
# pyinstaller --name ddms_compliance_tool --onefile run_api_tests.py
|
# pyinstaller --name ddms_compliance_tool --onefile run_api_tests.py
|
||||||
|
db:
|
||||||
|
flask init-db
|
||||||
run:
|
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
|
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:
|
run_stages:
|
||||||
|
|||||||
BIN
__pycache__/flask_app.cpython-312.pyc
Normal file
BIN
__pycache__/flask_app.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
@ -12,27 +13,38 @@ class TestCaseRegistry:
|
|||||||
"""
|
"""
|
||||||
负责发现、加载和管理所有自定义的APITestCase类。
|
负责发现、加载和管理所有自定义的APITestCase类。
|
||||||
"""
|
"""
|
||||||
def __init__(self, test_cases_dir: str):
|
def __init__(self, test_cases_dir: Optional[str]):
|
||||||
"""
|
"""
|
||||||
初始化 TestCaseRegistry。
|
初始化 TestCaseRegistry。
|
||||||
Args:
|
Args:
|
||||||
test_cases_dir: 存放自定义测试用例 (.py 文件) 的目录路径。
|
test_cases_dir: 存放自定义测试用例 (.py 文件) 的目录路径。
|
||||||
"""
|
"""
|
||||||
self.test_cases_dir = test_cases_dir
|
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self.test_cases_dir = test_cases_dir
|
||||||
self._registry: Dict[str, Type[BaseAPITestCase]] = {}
|
self._registry: Dict[str, Type[BaseAPITestCase]] = {}
|
||||||
self._test_case_classes: List[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):
|
def discover_test_cases(self):
|
||||||
"""
|
"""
|
||||||
扫描指定目录及其所有子目录,动态导入模块,并注册所有继承自 BaseAPITestCase 的类。
|
扫描指定目录及其所有子目录,动态导入模块,并注册所有继承自 BaseAPITestCase 的类。
|
||||||
"""
|
"""
|
||||||
if not os.path.isdir(self.test_cases_dir):
|
if not self.test_cases_dir:
|
||||||
self.logger.warning(f"测试用例目录不存在或不是一个目录: {self.test_cases_dir}")
|
self.logger.info("Test cases directory is not set. Skipping discovery.")
|
||||||
return
|
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
|
found_count = 0
|
||||||
# 使用 os.walk 进行递归扫描
|
# 使用 os.walk 进行递归扫描
|
||||||
for root_dir, _, files in os.walk(self.test_cases_dir):
|
for root_dir, _, files in os.walk(self.test_cases_dir):
|
||||||
|
|||||||
@ -18,6 +18,7 @@ from dataclasses import asdict as dataclass_asdict, is_dataclass
|
|||||||
import copy
|
import copy
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
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 import BaseModel, Field, create_model, HttpUrl # Added HttpUrl for Literal type hint if needed
|
||||||
from pydantic.networks import EmailStr
|
from pydantic.networks import EmailStr
|
||||||
@ -425,7 +426,7 @@ class APITestOrchestrator:
|
|||||||
"use_for_headers": use_llm_for_headers,
|
"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:
|
try:
|
||||||
self.llm_service = LLMService(api_key=llm_api_key, base_url=llm_base_url, model_name=llm_model_name)
|
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}.")
|
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'
|
final_headers['Content-Type'] = 'application/json'
|
||||||
self.logger.debug(f"{step_log_prefix}: 为JSON请求体设置默认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(
|
api_request_obj = APIRequest(
|
||||||
method=api_op_spec.method,
|
method=api_op_spec.method,
|
||||||
url=full_request_url,
|
url=full_request_url,
|
||||||
@ -2488,6 +2489,8 @@ class APITestOrchestrator:
|
|||||||
summary.add_stage_result(failure_result)
|
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.")
|
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,
|
def _execute_tests_from_parsed_spec(self,
|
||||||
parsed_spec: ParsedAPISpec,
|
parsed_spec: ParsedAPISpec,
|
||||||
@ -2497,54 +2500,30 @@ class APITestOrchestrator:
|
|||||||
custom_test_cases_dir: Optional[str] = None
|
custom_test_cases_dir: Optional[str] = None
|
||||||
) -> TestSummary:
|
) -> TestSummary:
|
||||||
"""基于已解析的API规范对象执行测试用例。"""
|
"""基于已解析的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 self.test_case_registry.test_cases_dir != custom_test_cases_dir):
|
||||||
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 with new directory: {custom_test_cases_dir}")
|
||||||
self.logger.info(f"Re-initializing TestCaseRegistry from _execute_tests_from_parsed_spec with new directory: {custom_test_cases_dir}")
|
self.test_case_registry = TestCaseRegistry(test_cases_dir=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
|
|
||||||
|
|
||||||
endpoints_to_test: List[Union[YAPIEndpoint, SwaggerEndpoint]] = []
|
endpoints_to_test: List[Union[YAPIEndpoint, SwaggerEndpoint]] = []
|
||||||
if isinstance(parsed_spec, ParsedYAPISpec):
|
if isinstance(parsed_spec, ParsedYAPISpec):
|
||||||
endpoints_to_test = parsed_spec.endpoints
|
endpoints_to_test = parsed_spec.endpoints
|
||||||
if categories:
|
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]
|
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):
|
elif isinstance(parsed_spec, ParsedSwaggerSpec):
|
||||||
endpoints_to_test = parsed_spec.endpoints
|
endpoints_to_test = parsed_spec.endpoints
|
||||||
if tags:
|
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)]
|
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:
|
if self.test_case_registry:
|
||||||
for endpoint_spec_obj in endpoints_to_test:
|
total_applicable_tcs = sum(
|
||||||
total_applicable_tcs_for_this_run += len(
|
len(self.test_case_registry.get_applicable_test_cases(ep.method.upper(), ep.path))
|
||||||
self.test_case_registry.get_applicable_test_cases(
|
for ep in endpoints_to_test
|
||||||
endpoint_spec_obj.method.upper(), endpoint_spec_obj.path
|
)
|
||||||
)
|
summary.set_total_test_cases_applicable(summary.total_test_cases_applicable + total_applicable_tcs)
|
||||||
)
|
|
||||||
current_total_applicable = summary.total_test_cases_applicable
|
|
||||||
summary.set_total_test_cases_applicable(current_total_applicable + total_applicable_tcs_for_this_run)
|
|
||||||
|
|
||||||
for endpoint in endpoints_to_test:
|
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)
|
result = self.run_test_for_endpoint(endpoint, global_api_spec=parsed_spec)
|
||||||
summary.add_endpoint_result(result)
|
summary.add_endpoint_result(result)
|
||||||
|
|
||||||
|
|||||||
570
flask_app.py
570
flask_app.py
@ -4,8 +4,12 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
import traceback # 用于更详细的错误日志
|
import traceback # 用于更详细的错误日志
|
||||||
|
import uuid # For unique filenames
|
||||||
from pathlib import Path
|
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 # 用于处理跨域请求
|
from flask_cors import CORS # 用于处理跨域请求
|
||||||
|
|
||||||
# 将ddms_compliance_suite的父目录添加到sys.path
|
# 将ddms_compliance_suite的父目录添加到sys.path
|
||||||
@ -17,6 +21,7 @@ from flask_cors import CORS # 用于处理跨域请求
|
|||||||
# 或者更具体地添加包含ddms_compliance_suite的目录
|
# 或者更具体地添加包含ddms_compliance_suite的目录
|
||||||
# sys.path.insert(0, os.path.join(project_root, '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.test_orchestrator import APITestOrchestrator, TestSummary
|
||||||
from ddms_compliance_suite.input_parser.parser import InputParser, ParsedYAPISpec, ParsedSwaggerSpec
|
from ddms_compliance_suite.input_parser.parser import InputParser, ParsedYAPISpec, ParsedSwaggerSpec
|
||||||
# 从 run_api_tests.py 导入辅助函数 (如果它们被重构为可导入的)
|
# 从 run_api_tests.py 导入辅助函数 (如果它们被重构为可导入的)
|
||||||
@ -32,12 +37,310 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
def get_orchestrator_from_config(config: dict) -> APITestOrchestrator:
|
||||||
"""根据配置字典实例化APITestOrchestrator"""
|
"""根据配置字典实例化APITestOrchestrator"""
|
||||||
return APITestOrchestrator(
|
return APITestOrchestrator(
|
||||||
base_url=config.get('base_url', ''),
|
base_url=config.get('base_url', ''),
|
||||||
custom_test_cases_dir=config.get('custom_test_cases_dir'),
|
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_api_key=config.get('llm_api_key'),
|
||||||
llm_base_url=config.get('llm_base_url'),
|
llm_base_url=config.get('llm_base_url'),
|
||||||
llm_model_name=config.get('llm_model_name'),
|
llm_model_name=config.get('llm_model_name'),
|
||||||
@ -50,159 +353,182 @@ def get_orchestrator_from_config(config: dict) -> APITestOrchestrator:
|
|||||||
|
|
||||||
# --- API 端点 ---
|
# --- API 端点 ---
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
@login_required # Protect the main page
|
||||||
def serve_index():
|
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')
|
return send_from_directory(app.static_folder, 'index.html')
|
||||||
|
|
||||||
@app.route('/run-tests', methods=['POST'])
|
@app.route('/run-tests', methods=['POST'])
|
||||||
|
@login_required # Protect this endpoint
|
||||||
def run_tests_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:
|
try:
|
||||||
config_data = request.json
|
# The form is now sent as multipart/form-data
|
||||||
if not config_data:
|
config_data = request.form.to_dict()
|
||||||
return jsonify({"error": "Request body must be JSON"}), 400
|
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 file:
|
||||||
if not config_data.get('base_url'):
|
filename = secure_filename(file.filename)
|
||||||
return jsonify({"error": "'base_url' is required"}), 400
|
# Create a unique filename to avoid conflicts
|
||||||
if not config_data.get('yapi_file_path') and not config_data.get('swagger_file_path'):
|
unique_filename = f"{uuid.uuid4()}_{filename}"
|
||||||
return jsonify({"error": "Either 'yapi_file_path' or 'swagger_file_path' is required"}), 400
|
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)
|
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
|
parsed_spec = None
|
||||||
api_spec_type = ""
|
if api_spec_type == "YAPI":
|
||||||
|
parsed_spec = parser.parse_yapi_spec(temp_spec_path)
|
||||||
if config_data.get('yapi_file_path'):
|
elif api_spec_type == "Swagger":
|
||||||
api_spec_type = "YAPI"
|
parsed_spec = parser.parse_swagger_spec(temp_spec_path)
|
||||||
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
|
|
||||||
|
|
||||||
elif config_data.get('swagger_file_path'):
|
if not parsed_spec:
|
||||||
api_spec_type = "Swagger/OpenAPI"
|
error_msg = f"Failed to parse the uploaded {api_spec_type} file."
|
||||||
swagger_path = config_data['swagger_file_path']
|
logger.error(error_msg)
|
||||||
if not os.path.isabs(swagger_path):
|
return jsonify({"error": error_msg}), 400
|
||||||
swagger_path = os.path.join(os.getcwd(), swagger_path) # 假设相对路径
|
|
||||||
if not os.path.exists(swagger_path):
|
logger.info("Successfully parsed API specification.")
|
||||||
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
|
|
||||||
|
|
||||||
# 执行测试用例
|
# Execute tests from parsed spec
|
||||||
logger.info(f"开始从已解析的 {api_spec_type} 规范执行测试用例...")
|
logger.info(f"Starting test execution from parsed {api_spec_type} spec...")
|
||||||
summary = orchestrator._execute_tests_from_parsed_spec(
|
summary = orchestrator._execute_tests_from_parsed_spec(
|
||||||
parsed_spec=parsed_spec,
|
parsed_spec=parsed_spec,
|
||||||
summary=summary,
|
summary=summary,
|
||||||
categories=config_data.get('categories'), # 逗号分隔的字符串,需要转为列表
|
categories=config_data.get('categories').split(',') if config_data.get('categories') else None,
|
||||||
tags=config_data.get('tags'), # 同上
|
tags=config_data.get('tags').split(',') if config_data.get('tags') else None,
|
||||||
custom_test_cases_dir=config_data.get('custom_test_cases_dir')
|
custom_test_cases_dir=config_data.get('custom_test_cases_dir')
|
||||||
)
|
)
|
||||||
logger.info("测试用例执行完成。")
|
logger.info("Test case execution finished.")
|
||||||
|
|
||||||
# 执行场景测试 (如果指定了目录)
|
# Execute test stages
|
||||||
scenarios_dir = config_data.get('scenarios_dir')
|
logger.info("Starting stage execution...")
|
||||||
if scenarios_dir and parsed_spec:
|
summary = orchestrator.run_stages_from_spec(
|
||||||
if not os.path.isabs(scenarios_dir):
|
parsed_spec=parsed_spec,
|
||||||
scenarios_dir = os.path.join(os.getcwd(), scenarios_dir)
|
summary=summary
|
||||||
logger.info(f"开始执行API场景测试,目录: {scenarios_dir}")
|
)
|
||||||
orchestrator.run_scenarios_from_spec(
|
logger.info("Stage execution finished.")
|
||||||
scenarios_dir=scenarios_dir,
|
|
||||||
parsed_spec=parsed_spec,
|
# Handle output and reporting
|
||||||
summary=summary
|
output_dir = config_data.get('output_dir', './test_reports')
|
||||||
)
|
if not os.path.isabs(output_dir):
|
||||||
logger.info("API场景测试执行完毕。")
|
output_dir = os.path.join(APP_ROOT, output_dir)
|
||||||
|
|
||||||
summary.finalize_summary() # 最终确定摘要,计算总时长等
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
# summary.print_summary_to_console() # 后端服务通常不直接打印到控制台
|
summary_path = os.path.join(output_dir, "test_summary.json")
|
||||||
|
details_path = os.path.join(output_dir, "api_call_details.md")
|
||||||
|
|
||||||
# 可以在这里决定如何保存报告,例如保存到 output_dir (如果提供)
|
with open(summary_path, 'w', encoding='utf-8') as f:
|
||||||
output_dir_path_str = config_data.get('output_dir')
|
f.write(summary.to_json(pretty=True))
|
||||||
main_report_file_path_str = ""
|
logger.info(f"Test summary saved to {summary_path}")
|
||||||
api_calls_output_path_str = ""
|
|
||||||
api_calls_filename = "api_call_details.md"
|
save_api_call_details_to_file(orchestrator.get_api_call_details(), output_dir)
|
||||||
|
logger.info(f"API call details saved to {details_path}")
|
||||||
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)
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "测试执行完成。",
|
|
||||||
"summary": summary.to_dict(),
|
"summary": summary.to_dict(),
|
||||||
"report_file": main_report_file_path_str, # 报告文件路径(如果保存了)
|
"summary_report_path": f"/download/{os.path.basename(output_dir)}/test_summary.json",
|
||||||
# "api_calls_file": api_calls_output_path_str + "/" + api_calls_filename # API调用详情文件路径
|
"details_report_path": f"/download/{os.path.basename(output_dir)}/api_call_details.md"
|
||||||
}), 200
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"执行测试时发生错误: {e}\n{traceback.format_exc()}")
|
error_msg = f"An unexpected error occurred during testing: {traceback.format_exc()}"
|
||||||
return jsonify({"error": f"执行测试时发生内部错误: {str(e)}"}), 500
|
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'])
|
@app.route('/list-yapi-categories', methods=['POST'])
|
||||||
|
@login_required
|
||||||
def list_yapi_categories_endpoint():
|
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:
|
try:
|
||||||
data = request.json
|
filename = secure_filename(file.filename)
|
||||||
yapi_file = data.get('yapi_file_path')
|
unique_filename = f"{uuid.uuid4()}_{filename}"
|
||||||
if not yapi_file:
|
temp_spec_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
|
||||||
return jsonify({"error": "'yapi_file_path' is required"}), 400
|
file.save(temp_spec_path)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
parser = InputParser()
|
parser = InputParser()
|
||||||
parsed_yapi = parser.parse_yapi_spec(yapi_file)
|
parsed_yapi = parser.parse_yapi_spec(temp_spec_path)
|
||||||
if not parsed_yapi or not parsed_yapi.categories:
|
|
||||||
|
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
|
return jsonify({"error": "Failed to parse YAPI categories or no categories found"}), 500
|
||||||
|
|
||||||
categories_list = [
|
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
|
for cat in parsed_yapi.categories
|
||||||
]
|
]
|
||||||
return jsonify(categories_list), 200
|
return jsonify(categories_list), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"列出YAPI分类时出错: {e}\n{traceback.format_exc()}")
|
logger.error(f"Error fetching YAPI categories: {traceback.format_exc()}")
|
||||||
return jsonify({"error": f"处理YAPI分类列表时出错: {str(e)}"}), 500
|
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'])
|
@app.route('/list-swagger-tags', methods=['POST'])
|
||||||
|
@login_required
|
||||||
def list_swagger_tags_endpoint():
|
def list_swagger_tags_endpoint():
|
||||||
try:
|
if 'api_spec_file' not in request.files:
|
||||||
data = request.json
|
return jsonify({"error": "api_spec_file part is missing"}), 400
|
||||||
swagger_file = data.get('swagger_file_path')
|
|
||||||
if not swagger_file:
|
file = request.files['api_spec_file']
|
||||||
return jsonify({"error": "'swagger_file_path' is required"}), 400
|
if file.filename == '':
|
||||||
|
return jsonify({"error": "No file selected"}), 400
|
||||||
|
|
||||||
if not os.path.isabs(swagger_file):
|
temp_spec_path = None
|
||||||
swagger_file = os.path.join(os.getcwd(), swagger_file)
|
try:
|
||||||
if not os.path.exists(swagger_file):
|
filename = secure_filename(file.filename)
|
||||||
return jsonify({"error": f"Swagger file not found: {swagger_file}"}), 400
|
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()
|
parser = InputParser()
|
||||||
parsed_swagger = parser.parse_swagger_spec(swagger_file)
|
parsed_swagger = parser.parse_swagger_spec(temp_spec_path)
|
||||||
if not parsed_swagger or not parsed_swagger.tags:
|
|
||||||
|
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
|
return jsonify({"error": "Failed to parse Swagger tags or no tags found"}), 500
|
||||||
|
|
||||||
tags_list = [
|
tags_list = [
|
||||||
@ -211,9 +537,33 @@ def list_swagger_tags_endpoint():
|
|||||||
]
|
]
|
||||||
return jsonify(tags_list), 200
|
return jsonify(tags_list), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"列出Swagger标签时出错: {e}\n{traceback.format_exc()}")
|
logger.error(f"Error fetching Swagger tags: {traceback.format_exc()}")
|
||||||
return jsonify({"error": f"处理Swagger标签列表时出错: {str(e)}"}), 500
|
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__':
|
if __name__ == '__main__':
|
||||||
# 注意:在生产环境中,应使用Gunicorn或uWSGI等WSGI服务器运行Flask应用
|
# 注意:在生产环境中,应使用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)
|
app.run(debug=True, host='0.0.0.0', port=5050)
|
||||||
@ -15,4 +15,6 @@ prance[osv]>=23.0.0,<24.0.0
|
|||||||
# 测试框架 (可选, 推荐)
|
# 测试框架 (可选, 推荐)
|
||||||
# pytest>=7.0.0,<8.0.0
|
# pytest>=7.0.0,<8.0.0
|
||||||
# pytest-cov>=4.0.0,<5.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
7
schema.sql
Normal 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
|
||||||
|
);
|
||||||
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<div class="configuration-section">
|
<div class="configuration-section">
|
||||||
<h2>测试配置</h2>
|
<h2>测试配置</h2>
|
||||||
<form id="test-config-form">
|
<form id="test-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="base_url">API 基础URL (必填):</label>
|
<label for="base_url">API 基础URL (必填):</label>
|
||||||
<input type="text" id="base_url" name="base_url" required placeholder="例如:http://localhost:8080/api/v1">
|
<input type="text" id="base_url" name="base_url" required placeholder="例如:http://localhost:8080/api/v1">
|
||||||
@ -21,73 +21,85 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>API 定义源 (选择一个)</legend>
|
<legend>API 定义源 (选择一个)</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="yapi_file_path">YAPI 文件路径:</label>
|
<label for="api_spec_type">API 规范类型:</label>
|
||||||
<input type="text" id="yapi_file_path" name="yapi_file_path" placeholder="例如:./assets/doc/yapi_spec.json">
|
<select id="api_spec_type" name="api_spec_type">
|
||||||
<button type="button" class="action-button" onclick="fetchYapiCategories()">加载分类</button>
|
<option value="YAPI">YAPI (.json)</option>
|
||||||
<div id="yapi-categories-container" class="categories-tags-container"></div>
|
<option value="Swagger">Swagger/OpenAPI (.json, .yaml)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="swagger_file_path">Swagger/OpenAPI 文件路径:</label>
|
<label for="api_spec_file">上传 API 规范文件:</label>
|
||||||
<input type="text" id="swagger_file_path" name="swagger_file_path" placeholder="例如:./assets/doc/swagger_spec.json">
|
<input type="file" id="api_spec_file" name="api_spec_file" accept=".json,.yaml,.yml" required>
|
||||||
<button type="button" class="action-button" onclick="fetchSwaggerTags()">加载标签</button>
|
|
||||||
<div id="swagger-tags-container" class="categories-tags-container"></div>
|
|
||||||
</div>
|
</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>
|
</fieldset>
|
||||||
|
|
||||||
<div class="form-group">
|
<details>
|
||||||
<label for="custom_test_cases_dir">自定义测试用例目录:</label>
|
<summary>高级配置 (点击展开)</summary>
|
||||||
<input type="text" id="custom_test_cases_dir" name="custom_test_cases_dir" placeholder="例如:./custom_testcases">
|
<div class="form-group">
|
||||||
</div>
|
<label for="custom_test_cases_dir">自定义测试用例目录:</label>
|
||||||
<div class="form-group">
|
<input type="text" id="custom_test_cases_dir" name="custom_test_cases_dir" placeholder="例如:./custom_testcases" value="./custom_testcases">
|
||||||
<label for="scenarios_dir">自定义场景目录:</label>
|
</div>
|
||||||
<input type="text" id="scenarios_dir" name="scenarios_dir" placeholder="例如:./custom_scenarios">
|
<div class="form-group">
|
||||||
</div>
|
<label for="stages_dir">自定义阶段目录:</label>
|
||||||
<div class="form-group">
|
<input type="text" id="stages_dir" name="stages_dir" placeholder="例如:./custom_stages" value="./custom_stages">
|
||||||
<label for="output_dir">报告输出目录:</label>
|
</div>
|
||||||
<input type="text" id="output_dir" name="output_dir" placeholder="例如:./test_reports">
|
<div class="form-group">
|
||||||
</div>
|
<label for="output_dir">报告输出目录:</label>
|
||||||
|
<input type="text" id="output_dir" name="output_dir" placeholder="例如:./test_reports" value="./test_reports">
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<fieldset>
|
<details>
|
||||||
<legend>LLM 配置 (可选)</legend>
|
<summary>LLM 配置 (可选, 点击展开)</summary>
|
||||||
<div class="form-group">
|
<fieldset>
|
||||||
<label for="llm_api_key">LLM API Key:</label>
|
<legend>LLM 配置 (可选)</legend>
|
||||||
<input type="password" id="llm_api_key" name="llm_api_key" placeholder="留空则尝试读取环境变量">
|
<div class="form-group">
|
||||||
</div>
|
<label for="llm_api_key">LLM API Key:</label>
|
||||||
<div class="form-group">
|
<input type="password" id="llm_api_key" name="llm_api_key" placeholder="留空则尝试读取环境变量">
|
||||||
<label for="llm_base_url">LLM Base URL:</label>
|
</div>
|
||||||
<input type="text" id="llm_base_url" name="llm_base_url" placeholder="例如:https://dashscope.aliyuncs.com/compatible-mode/v1">
|
<div class="form-group">
|
||||||
</div>
|
<label for="llm_base_url">LLM Base URL:</label>
|
||||||
<div class="form-group">
|
<input type="text" id="llm_base_url" name="llm_base_url" placeholder="例如:https://dashscope.aliyuncs.com/compatible-mode/v1">
|
||||||
<label for="llm_model_name">LLM 模型名称:</label>
|
</div>
|
||||||
<input type="text" id="llm_model_name" name="llm_model_name" placeholder="例如:qwen-plus">
|
<div class="form-group">
|
||||||
</div>
|
<label for="llm_model_name">LLM 模型名称:</label>
|
||||||
<div class="form-group checkbox-group">
|
<input type="text" id="llm_model_name" name="llm_model_name" placeholder="例如:qwen-plus">
|
||||||
<input type="checkbox" id="use_llm_for_request_body" name="use_llm_for_request_body">
|
</div>
|
||||||
<label for="use_llm_for_request_body">使用LLM生成请求体</label>
|
<div class="form-group checkbox-group">
|
||||||
</div>
|
<input type="checkbox" id="use_llm_for_request_body" name="use_llm_for_request_body">
|
||||||
<div class="form-group checkbox-group">
|
<label for="use_llm_for_request_body">使用LLM生成请求体</label>
|
||||||
<input type="checkbox" id="use_llm_for_path_params" name="use_llm_for_path_params">
|
</div>
|
||||||
<label for="use_llm_for_path_params">使用LLM生成路径参数</label>
|
<div class="form-group checkbox-group">
|
||||||
</div>
|
<input type="checkbox" id="use_llm_for_path_params" name="use_llm_for_path_params">
|
||||||
<div class="form-group checkbox-group">
|
<label for="use_llm_for_path_params">使用LLM生成路径参数</label>
|
||||||
<input type="checkbox" id="use_llm_for_query_params" name="use_llm_for_query_params">
|
</div>
|
||||||
<label for="use_llm_for_query_params">使用LLM生成查询参数</label>
|
<div class="form-group checkbox-group">
|
||||||
</div>
|
<input type="checkbox" id="use_llm_for_query_params" name="use_llm_for_query_params">
|
||||||
<div class="form-group checkbox-group">
|
<label for="use_llm_for_query_params">使用LLM生成查询参数</label>
|
||||||
<input type="checkbox" id="use_llm_for_headers" name="use_llm_for_headers">
|
</div>
|
||||||
<label for="use_llm_for_headers">使用LLM生成头部参数</label>
|
<div class="form-group checkbox-group">
|
||||||
</div>
|
<input type="checkbox" id="use_llm_for_headers" name="use_llm_for_headers">
|
||||||
</fieldset>
|
<label for="use_llm_for_headers">使用LLM生成头部参数</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</details>
|
||||||
|
|
||||||
<button type="submit" class="submit-button">运行测试</button>
|
<button type="submit" class="submit-button">运行测试</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="results-section">
|
<div class="results-section">
|
||||||
<h2>测试状态与结果</h2>
|
<h2>测试日志与结果</h2>
|
||||||
<div id="status-area">等待配置并运行测试...</div>
|
<label for="log-output">实时日志:</label>
|
||||||
<pre id="results-output"></pre>
|
<textarea id="log-output" readonly style="width:100%"></textarea>
|
||||||
<div id="report-link-area"></div>
|
<div id="results-container">
|
||||||
|
<!-- 测试结果将在此处动态生成 -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
247
static/script.js
247
static/script.js
@ -1,140 +1,139 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const form = document.getElementById('test-config-form');
|
const form = document.getElementById('test-form');
|
||||||
const statusArea = document.getElementById('status-area');
|
const logOutput = document.getElementById('log-output');
|
||||||
const resultsOutput = document.getElementById('results-output');
|
const resultsContainer = document.getElementById('results-container');
|
||||||
const reportLinkArea = document.getElementById('report-link-area');
|
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) => {
|
form.addEventListener('submit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
statusArea.textContent = '正在运行测试,请稍候...';
|
logOutput.value = '正在开始测试...\n';
|
||||||
resultsOutput.textContent = '';
|
resultsContainer.innerHTML = '';
|
||||||
reportLinkArea.innerHTML = '';
|
|
||||||
|
// FormData will correctly handle all form fields, including the file upload
|
||||||
const formData = new FormData(form);
|
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 {
|
try {
|
||||||
const response = await fetch('/run-tests', {
|
const response = await fetch('/run-tests', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
// For FormData, the browser sets the Content-Type to multipart/form-data with the correct boundary.
|
||||||
'Content-Type': 'application/json'
|
// Do not set the 'Content-Type' header manually.
|
||||||
},
|
body: formData,
|
||||||
body: JSON.stringify(config)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (!response.ok) {
|
||||||
statusArea.textContent = `测试完成: ${result.message || '成功'}`;
|
// Try to parse the error, provide a fallback message.
|
||||||
resultsOutput.textContent = JSON.stringify(result.summary, null, 2);
|
const errorMessage = result.error || '运行测试时发生未知错误';
|
||||||
if (result.report_file) {
|
logOutput.value += `\n错误: ${errorMessage}`;
|
||||||
reportLinkArea.innerHTML = `<p>测试报告已保存到: <strong>${result.report_file}</strong></p>`;
|
throw new Error(errorMessage);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
statusArea.textContent = `测试失败: ${result.error || '未知错误'}`;
|
|
||||||
resultsOutput.textContent = JSON.stringify(result, null, 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
} catch (error) {
|
||||||
statusArea.textContent = '运行测试时发生网络错误或服务器内部错误。';
|
console.error('运行测试时捕获到错误:', error);
|
||||||
resultsOutput.textContent = error.toString();
|
logOutput.value += `\n\n发生严重错误: ${error.message}`;
|
||||||
console.error('运行测试出错:', error);
|
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() {
|
// The old functions fetchYapiCategories, fetchSwaggerTags, and renderCheckboxes are no longer needed
|
||||||
const yapiFilePath = document.getElementById('yapi_file_path').value;
|
// and should be removed if they exist elsewhere in this file.
|
||||||
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;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
1091
test_reports/summary_report.json
Normal file
1091
test_reports/summary_report.json
Normal file
File diff suppressed because it is too large
Load Diff
1091
test_reports/test_summary.json
Normal file
1091
test_reports/test_summary.json
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user