add:flask app
This commit is contained in:
@@ -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 调用都是一个独立的、可折叠的部分,您可以轻松地展开您关心的失败或成功的请求,查看其所有细节。
|
||||||
|
|
||||||
|
---
|
||||||
@@ -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:
|
||||||
|
|||||||
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._discovery_errors: List[str] = []
|
||||||
|
|
||||||
|
if self.test_cases_dir:
|
||||||
self.discover_test_cases()
|
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,
|
||||||
@@ -2489,6 +2490,8 @@ class APITestOrchestrator:
|
|||||||
|
|
||||||
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,
|
||||||
summary: TestSummary,
|
summary: TestSummary,
|
||||||
@@ -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(summary.total_endpoints_defined + len(endpoints_to_test))
|
||||||
summary.set_total_endpoints_defined(current_total_defined + len(endpoints_to_test))
|
|
||||||
|
|
||||||
total_applicable_tcs_for_this_run = 0
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
+452
-102
@@ -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 not config_data.get('base_url'):
|
if file.filename == '':
|
||||||
return jsonify({"error": "'base_url' is required"}), 400
|
logger.error("No API spec file selected.")
|
||||||
if not config_data.get('yapi_file_path') and not config_data.get('swagger_file_path'):
|
return jsonify({"error": "No API spec file selected"}), 400
|
||||||
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)
|
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)
|
||||||
|
elif api_spec_type == "Swagger":
|
||||||
|
parsed_spec = parser.parse_swagger_spec(temp_spec_path)
|
||||||
|
|
||||||
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:
|
if not parsed_spec:
|
||||||
logger.error(f"解析YAPI文件失败: {yapi_path}")
|
error_msg = f"Failed to parse the uploaded {api_spec_type} file."
|
||||||
return jsonify({"error": f"Failed to parse YAPI file: {yapi_path}"}), 500
|
logger.error(error_msg)
|
||||||
|
return jsonify({"error": error_msg}), 400
|
||||||
|
|
||||||
elif config_data.get('swagger_file_path'):
|
logger.info("Successfully parsed API specification.")
|
||||||
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
|
|
||||||
|
|
||||||
# 执行测试用例
|
# 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):
|
|
||||||
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,
|
parsed_spec=parsed_spec,
|
||||||
summary=summary
|
summary=summary
|
||||||
)
|
)
|
||||||
logger.info("API场景测试执行完毕。")
|
logger.info("Stage execution finished.")
|
||||||
|
|
||||||
summary.finalize_summary() # 最终确定摘要,计算总时长等
|
# Handle output and reporting
|
||||||
# summary.print_summary_to_console() # 后端服务通常不直接打印到控制台
|
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)
|
||||||
|
|
||||||
# 可以在这里决定如何保存报告,例如保存到 output_dir (如果提供)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
output_dir_path_str = config_data.get('output_dir')
|
summary_path = os.path.join(output_dir, "test_summary.json")
|
||||||
main_report_file_path_str = ""
|
details_path = os.path.join(output_dir, "api_call_details.md")
|
||||||
api_calls_output_path_str = ""
|
|
||||||
api_calls_filename = "api_call_details.md"
|
|
||||||
|
|
||||||
if output_dir_path_str:
|
with open(summary_path, 'w', encoding='utf-8') as f:
|
||||||
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))
|
f.write(summary.to_json(pretty=True))
|
||||||
logger.info(f"主测试报告已保存到: {main_report_file_path}")
|
logger.info(f"Test summary saved to {summary_path}")
|
||||||
|
|
||||||
# 保存API调用详情
|
save_api_call_details_to_file(orchestrator.get_api_call_details(), output_dir)
|
||||||
api_calls_output_path_str = str(output_path)
|
logger.info(f"API call details saved to {details_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():
|
||||||
try:
|
if 'api_spec_file' not in request.files:
|
||||||
data = request.json
|
return jsonify({"error": "api_spec_file part is missing"}), 400
|
||||||
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):
|
file = request.files['api_spec_file']
|
||||||
yapi_file = os.path.join(os.getcwd(), yapi_file)
|
if file.filename == '':
|
||||||
if not os.path.exists(yapi_file):
|
return jsonify({"error": "No file selected"}), 400
|
||||||
return jsonify({"error": f"YAPI file not found: {yapi_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()
|
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:
|
|
||||||
return jsonify({"error": "'swagger_file_path' is required"}), 400
|
|
||||||
|
|
||||||
if not os.path.isabs(swagger_file):
|
file = request.files['api_spec_file']
|
||||||
swagger_file = os.path.join(os.getcwd(), swagger_file)
|
if file.filename == '':
|
||||||
if not os.path.exists(swagger_file):
|
return jsonify({"error": "No file selected"}), 400
|
||||||
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()
|
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)
|
||||||
@@ -16,3 +16,5 @@ 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
|
||||||
@@ -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
|
||||||
|
);
|
||||||
+29
-17
@@ -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,32 +21,41 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>高级配置 (点击展开)</summary>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="custom_test_cases_dir">自定义测试用例目录:</label>
|
<label for="custom_test_cases_dir">自定义测试用例目录:</label>
|
||||||
<input type="text" id="custom_test_cases_dir" name="custom_test_cases_dir" placeholder="例如:./custom_testcases">
|
<input type="text" id="custom_test_cases_dir" name="custom_test_cases_dir" placeholder="例如:./custom_testcases" value="./custom_testcases">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="scenarios_dir">自定义场景目录:</label>
|
<label for="stages_dir">自定义阶段目录:</label>
|
||||||
<input type="text" id="scenarios_dir" name="scenarios_dir" placeholder="例如:./custom_scenarios">
|
<input type="text" id="stages_dir" name="stages_dir" placeholder="例如:./custom_stages" value="./custom_stages">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="output_dir">报告输出目录:</label>
|
<label for="output_dir">报告输出目录:</label>
|
||||||
<input type="text" id="output_dir" name="output_dir" placeholder="例如:./test_reports">
|
<input type="text" id="output_dir" name="output_dir" placeholder="例如:./test_reports" value="./test_reports">
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>LLM 配置 (可选, 点击展开)</summary>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>LLM 配置 (可选)</legend>
|
<legend>LLM 配置 (可选)</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -78,16 +87,19 @@
|
|||||||
<label for="use_llm_for_headers">使用LLM生成头部参数</label>
|
<label for="use_llm_for_headers">使用LLM生成头部参数</label>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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>
|
||||||
|
|
||||||
|
|||||||
+115
-116
@@ -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>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchYapiCategories() {
|
// A simplified function to render categories/tags as a list
|
||||||
const yapiFilePath = document.getElementById('yapi_file_path').value;
|
function renderItemsList(container, items, type) {
|
||||||
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) {
|
if (!items || items.length === 0) {
|
||||||
container.innerHTML = '<p>未找到任何项。</p>';
|
container.innerHTML = '<p>未找到任何项。</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let html = items.map((item, index) => {
|
let html = `<h4>${type} ${type === 'YAPI' ? '分类' : '标签'}:</h4><ul>`;
|
||||||
const id = `${groupName}_${index}`;
|
items.forEach(item => {
|
||||||
return `<div>
|
html += `<li><strong>${item.name}</strong>: ${item.description || '无描述'}</li>`;
|
||||||
<input type="checkbox" id="${id}" name="${groupName}[]" value="${item.name}">
|
});
|
||||||
<label for="${id}">${item.name} ${item.description ? '(' + item.description + ')' : ''}</label>
|
html += '</ul>';
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
container.innerHTML = html;
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The old functions fetchYapiCategories, fetchSwaggerTags, and renderCheckboxes are no longer needed
|
||||||
|
// and should be removed if they exist elsewhere in this file.
|
||||||
+1765
-3134
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
Reference in New Issue
Block a user