- 修复前端路由守卫:未登录时不显示提示,直接跳转登录页 - 修复API拦截器:401错误不显示提示,直接跳转 - 增强验证码显示:图片尺寸从120x40增加到200x80 - 增大验证码字体:从28号增加到48号 - 优化验证码字符:排除易混淆的0和1 - 减少干扰线:从5条减少到3条,添加背景色优化 - 增强登录API日志:添加详细的调试日志 - 增强验证码生成和验证日志 - 优化异常处理和错误追踪 影响文件: - src/router/index.ts - src/api/request.ts - app/services/auth_service.py - app/api/v1/auth.py - app/schemas/user.py 测试状态: - 前端构建通过 - 后端语法检查通过 - 验证码显示效果优化完成 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
427 lines
14 KiB
Python
427 lines
14 KiB
Python
"""
|
|
接口集成测试
|
|
|
|
测试内容:
|
|
- 所有API接口功能测试
|
|
- 参数验证测试
|
|
- 错误处理测试
|
|
- 响应时间测试
|
|
- 并发测试
|
|
"""
|
|
|
|
import pytest
|
|
import time
|
|
import asyncio
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
# from fastapi.testclient import TestClient
|
|
|
|
|
|
# class TestAPIEndpoints:
|
|
# """测试所有API端点"""
|
|
#
|
|
# def test_health_check(self, client: TestClient):
|
|
# """测试健康检查接口"""
|
|
# response = client.get("/health")
|
|
# assert response.status_code == 200
|
|
# assert response.json()["status"] == "healthy"
|
|
#
|
|
# def test_api_root(self, client: TestClient):
|
|
# """测试API根路径"""
|
|
# response = client.get("/api/v1/")
|
|
# assert response.status_code == 200
|
|
# data = response.json()
|
|
# assert "version" in data
|
|
# assert "name" in data
|
|
|
|
|
|
# class TestParameterValidation:
|
|
# """测试参数验证"""
|
|
#
|
|
# def test_query_parameter_validation(self, client: TestClient, auth_headers):
|
|
# """测试查询参数验证"""
|
|
# # 无效的分页参数
|
|
# response = client.get(
|
|
# "/api/v1/assets?page=-1&page_size=0",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert response.status_code == 422
|
|
#
|
|
# # 超大的page_size
|
|
# response = client.get(
|
|
# "/api/v1/assets?page_size=10000",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert response.status_code == 422
|
|
#
|
|
# def test_path_parameter_validation(self, client: TestClient, auth_headers):
|
|
# """测试路径参数验证"""
|
|
# # 无效的ID
|
|
# response = client.get(
|
|
# "/api/v1/assets/abc",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert response.status_code == 422
|
|
#
|
|
# # 负数ID
|
|
# response = client.get(
|
|
# "/api/v1/assets/-1",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert response.status_code == 422
|
|
#
|
|
# def test_request_body_validation(self, client: TestClient, auth_headers):
|
|
# """测试请求体验证"""
|
|
# # 缺少必填字段
|
|
# response = client.post(
|
|
# "/api/v1/assets",
|
|
# headers=auth_headers,
|
|
# json={"asset_name": "测试"} # 缺少device_type_id
|
|
# )
|
|
# assert response.status_code == 422
|
|
#
|
|
# # 无效的数据类型
|
|
# response = client.post(
|
|
# "/api/v1/assets",
|
|
# headers=auth_headers,
|
|
# json={
|
|
# "asset_name": "测试",
|
|
# "device_type_id": "not_a_number", # 应该是数字
|
|
# "organization_id": 1
|
|
# }
|
|
# )
|
|
# assert response.status_code == 422
|
|
#
|
|
# # 超长字符串
|
|
# response = client.post(
|
|
# "/api/v1/assets",
|
|
# headers=auth_headers,
|
|
# json={
|
|
# "asset_name": "a" * 300, # 超过最大长度
|
|
# "device_type_id": 1,
|
|
# "organization_id": 1
|
|
# }
|
|
# )
|
|
# assert response.status_code == 422
|
|
#
|
|
# def test_enum_validation(self, client: TestClient, auth_headers):
|
|
# """测试枚举值验证"""
|
|
# # 无效的状态值
|
|
# response = client.get(
|
|
# "/api/v1/assets?status=invalid_status",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert response.status_code == 422
|
|
#
|
|
# def test_date_validation(self, client: TestClient, auth_headers):
|
|
# """测试日期格式验证"""
|
|
# # 无效的日期格式
|
|
# response = client.get(
|
|
# "/api/v1/assets?purchase_date_start=invalid-date",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert response.status_code == 422
|
|
#
|
|
# # 结束日期早于开始日期
|
|
# response = client.get(
|
|
# "/api/v1/assets?purchase_date_start=2024-12-31&purchase_date_end=2024-01-01",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert response.status_code == 400
|
|
|
|
|
|
# class TestErrorHandling:
|
|
# """测试错误处理"""
|
|
#
|
|
# def test_404_not_found(self, client: TestClient, auth_headers):
|
|
# """测试404错误"""
|
|
# response = client.get(
|
|
# "/api/v1/assets/999999",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert response.status_code == 404
|
|
# data = response.json()
|
|
# assert "message" in data
|
|
#
|
|
# def test_401_unauthorized(self, client: TestClient):
|
|
# """测试401未授权错误"""
|
|
# response = client.get("/api/v1/assets")
|
|
# assert response.status_code == 401
|
|
#
|
|
# def test_403_forbidden(self, client: TestClient, auth_headers):
|
|
# """测试403禁止访问"""
|
|
# # 使用普通用户token访问管理员接口
|
|
# response = client.delete(
|
|
# "/api/v1/assets/1",
|
|
# headers=auth_headers # 普通用户token
|
|
# )
|
|
# assert response.status_code == 403
|
|
#
|
|
# def test_409_conflict(self, client: TestClient, auth_headers):
|
|
# """测试409冲突错误"""
|
|
# # 尝试创建重复的资源
|
|
# asset_data = {
|
|
# "asset_name": "测试资产",
|
|
# "device_type_id": 1,
|
|
# "organization_id": 1,
|
|
# "serial_number": "UNIQUE-SN-001"
|
|
# }
|
|
#
|
|
# # 第一次创建成功
|
|
# client.post("/api/v1/assets", headers=auth_headers, json=asset_data)
|
|
#
|
|
# # 第二次创建应该返回409
|
|
# response = client.post("/api/v1/assets", headers=auth_headers, json=asset_data)
|
|
# assert response.status_code == 409
|
|
#
|
|
# def test_422_validation_error(self, client: TestClient, auth_headers):
|
|
# """测试422验证错误"""
|
|
# response = client.post(
|
|
# "/api/v1/assets",
|
|
# headers=auth_headers,
|
|
# json={}
|
|
# )
|
|
# assert response.status_code == 422
|
|
# data = response.json()
|
|
# assert "errors" in data
|
|
#
|
|
# def test_500_internal_error(self, client: TestClient, auth_headers):
|
|
# """测试500服务器错误"""
|
|
# # 这个测试需要mock一个会抛出异常的场景
|
|
# pass
|
|
#
|
|
# def test_error_response_format(self, client: TestClient, auth_headers):
|
|
# """测试错误响应格式"""
|
|
# response = client.get(
|
|
# "/api/v1/assets/999999",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert response.status_code == 404
|
|
#
|
|
# data = response.json()
|
|
# # 验证错误响应包含必要字段
|
|
# assert "code" in data
|
|
# assert "message" in data
|
|
# assert "timestamp" in data
|
|
|
|
|
|
# class TestResponseTime:
|
|
# """测试接口响应时间"""
|
|
#
|
|
# @pytest.mark.parametrize("endpoint,expected_max_time", [
|
|
# ("/api/v1/assets", 0.5), # 资产列表应该在500ms内返回
|
|
# ("/api/v1/assets/1", 0.3), # 资产详情应该在300ms内返回
|
|
# ("/api/v1/statistics/overview", 1.0), # 统计概览在1秒内返回
|
|
# ])
|
|
# def test_response_time_within_limit(self, client, auth_headers, endpoint, expected_max_time):
|
|
# """测试响应时间在限制内"""
|
|
# start_time = time.time()
|
|
#
|
|
# response = client.get(endpoint, headers=auth_headers)
|
|
#
|
|
# elapsed_time = time.time() - start_time
|
|
#
|
|
# assert response.status_code == 200
|
|
# assert elapsed_time < expected_max_time, \
|
|
# f"响应时间 {elapsed_time:.2f}s 超过限制 {expected_max_time}s"
|
|
#
|
|
# def test_concurrent_requests_performance(self, client, auth_headers):
|
|
# """测试并发请求性能"""
|
|
# urls = ["/api/v1/assets"] * 10
|
|
#
|
|
# start_time = time.time()
|
|
#
|
|
# with ThreadPoolExecutor(max_workers=5) as executor:
|
|
# futures = [
|
|
# executor.submit(
|
|
# client.get,
|
|
# url,
|
|
# headers=auth_headers
|
|
# )
|
|
# for url in urls
|
|
# ]
|
|
# responses = [f.result() for f in futures]
|
|
#
|
|
# elapsed_time = time.time() - start_time
|
|
#
|
|
# # 所有请求都应该成功
|
|
# assert all(r.status_code == 200 for r in responses)
|
|
#
|
|
# # 10个并发请求应该在3秒内完成
|
|
# assert elapsed_time < 3.0
|
|
#
|
|
# def test_large_list_response_time(self, client, auth_headers, db):
|
|
# """测试大数据量列表响应时间"""
|
|
# # 创建1000条测试数据
|
|
# # ... 创建数据
|
|
#
|
|
# start_time = time.time()
|
|
# response = client.get("/api/v1/assets?page=1&page_size=100", headers=auth_headers)
|
|
# elapsed_time = time.time() - start_time
|
|
#
|
|
# assert response.status_code == 200
|
|
# assert elapsed_time < 1.0 # 100条记录应该在1秒内返回
|
|
#
|
|
# def test_complex_query_response_time(self, client, auth_headers):
|
|
# """测试复杂查询响应时间"""
|
|
# params = {
|
|
# "keyword": "联想",
|
|
# "device_type_id": 1,
|
|
# "organization_id": 1,
|
|
# "status": "in_use",
|
|
# "purchase_date_start": "2024-01-01",
|
|
# "purchase_date_end": "2024-12-31",
|
|
# "page": 1,
|
|
# "page_size": 20
|
|
# }
|
|
#
|
|
# start_time = time.time()
|
|
# response = client.get("/api/v1/assets", params=params, headers=auth_headers)
|
|
# elapsed_time = time.time() - start_time
|
|
#
|
|
# assert response.status_code == 200
|
|
# assert elapsed_time < 1.0
|
|
|
|
|
|
# class TestConcurrentRequests:
|
|
# """测试并发请求"""
|
|
#
|
|
# def test_concurrent_asset_creation(self, client, auth_headers):
|
|
# """测试并发创建资产"""
|
|
# asset_data = {
|
|
# "asset_name": "并发测试资产",
|
|
# "device_type_id": 1,
|
|
# "organization_id": 1
|
|
# }
|
|
#
|
|
# def create_asset(i):
|
|
# data = asset_data.copy()
|
|
# data["asset_name"] = f"并发测试资产-{i}"
|
|
# return client.post("/api/v1/assets", headers=auth_headers, json=data)
|
|
#
|
|
# with ThreadPoolExecutor(max_workers=10) as executor:
|
|
# futures = [executor.submit(create_asset, i) for i in range(50)]
|
|
# responses = [f.result() for f in futures]
|
|
#
|
|
# # 所有请求都应该成功
|
|
# success_count = sum(1 for r in responses if r.status_code == 201)
|
|
# assert success_count == 50
|
|
#
|
|
# def test_concurrent_same_resource_update(self, client, auth_headers, test_asset):
|
|
# """测试并发更新同一资源"""
|
|
# def update_asset(i):
|
|
# return client.put(
|
|
# f"/api/v1/assets/{test_asset.id}",
|
|
# headers=auth_headers,
|
|
# json={"location": f"位置-{i}"}
|
|
# )
|
|
#
|
|
# with ThreadPoolExecutor(max_workers=5) as executor:
|
|
# futures = [executor.submit(update_asset, i) for i in range(10)]
|
|
# responses = [f.result() for f in futures]
|
|
#
|
|
# # 所有请求都应该成功(乐观锁会处理并发)
|
|
# assert all(r.status_code in [200, 409] for r in responses)
|
|
#
|
|
# @pytest.mark.slow
|
|
# def test_high_concurrent_load(self, client, auth_headers):
|
|
# """测试高并发负载"""
|
|
# def make_request():
|
|
# return client.get("/api/v1/assets", headers=auth_headers)
|
|
#
|
|
# # 模拟100个并发请求
|
|
# with ThreadPoolExecutor(max_workers=20) as executor:
|
|
# futures = [executor.submit(make_request) for _ in range(100)]
|
|
# responses = [f.result() for f in futures]
|
|
#
|
|
# success_count = sum(1 for r in responses if r.status_code == 200)
|
|
# success_rate = success_count / 100
|
|
#
|
|
# # 成功率应该大于95%
|
|
# assert success_rate > 0.95
|
|
#
|
|
# def test_rate_limiting(self, client):
|
|
# """测试请求频率限制"""
|
|
# # 登录接口限制10次/分钟
|
|
# responses = []
|
|
# for i in range(12):
|
|
# response = client.post(
|
|
# "/api/v1/auth/login",
|
|
# json={
|
|
# "username": "test",
|
|
# "password": "test",
|
|
# "captcha": "1234",
|
|
# "captcha_key": f"test-{i}"
|
|
# }
|
|
# )
|
|
# responses.append(response)
|
|
#
|
|
# # 应该有部分请求被限流
|
|
# rate_limited_count = sum(1 for r in responses if r.status_code == 429)
|
|
# assert rate_limited_count >= 1
|
|
|
|
|
|
# class TestDataIntegrity:
|
|
# """测试数据完整性"""
|
|
#
|
|
# def test_create_and_retrieve_asset(self, client, auth_headers):
|
|
# """测试创建后获取数据一致性"""
|
|
# # 创建资产
|
|
# asset_data = {
|
|
# "asset_name": "数据完整性测试",
|
|
# "device_type_id": 1,
|
|
# "organization_id": 1,
|
|
# "model": "测试型号"
|
|
# }
|
|
#
|
|
# create_response = client.post("/api/v1/assets", headers=auth_headers, json=asset_data)
|
|
# assert create_response.status_code == 201
|
|
# created_asset = create_response.json()["data"]
|
|
#
|
|
# # 获取资产
|
|
# get_response = client.get(
|
|
# f"/api/v1/assets/{created_asset['id']}",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert get_response.status_code == 200
|
|
# retrieved_asset = get_response.json()["data"]
|
|
#
|
|
# # 验证数据一致性
|
|
# assert retrieved_asset["asset_name"] == asset_data["asset_name"]
|
|
# assert retrieved_asset["model"] == asset_data["model"]
|
|
#
|
|
# def test_update_and_retrieve_asset(self, client, auth_headers, test_asset):
|
|
# """测试更新后获取数据一致性"""
|
|
# # 更新资产
|
|
# updated_data = {"asset_name": "更新后的名称"}
|
|
# client.put(
|
|
# f"/api/v1/assets/{test_asset.id}",
|
|
# headers=auth_headers,
|
|
# json=updated_data
|
|
# )
|
|
#
|
|
# # 获取资产
|
|
# response = client.get(
|
|
# f"/api/v1/assets/{test_asset.id}",
|
|
# headers=auth_headers
|
|
# )
|
|
# asset = response.json()["data"]
|
|
#
|
|
# # 验证更新生效
|
|
# assert asset["asset_name"] == updated_data["asset_name"]
|
|
#
|
|
# def test_delete_and_verify_asset(self, client, auth_headers, test_asset):
|
|
# """测试删除后无法获取"""
|
|
# # 删除资产
|
|
# delete_response = client.delete(
|
|
# f"/api/v1/assets/{test_asset.id}",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert delete_response.status_code == 200
|
|
#
|
|
# # 验证无法获取
|
|
# get_response = client.get(
|
|
# f"/api/v1/assets/{test_asset.id}",
|
|
# headers=auth_headers
|
|
# )
|
|
# assert get_response.status_code == 404
|