fix: 修复多个关键问题

- 修复前端路由守卫:未登录时不显示提示,直接跳转登录页
- 修复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>
This commit is contained in:
Claude
2026-01-25 00:26:21 +08:00
commit e71181f0a3
150 changed files with 39549 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,426 @@
"""
接口集成测试
测试内容:
- 所有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

459
tests/api/test_assets.py Normal file
View File

@@ -0,0 +1,459 @@
"""
资产管理模块API测试
测试内容:
- 资产列表查询
- 资产详情查询
- 创建资产
- 更新资产
- 删除资产
- 批量导入
- 扫码查询
"""
import pytest
from datetime import date
# class TestAssetList:
# """测试资产列表"""
#
# def test_get_assets_success(self, client: TestClient, auth_headers):
# """测试获取资产列表成功"""
# response = client.get(
# "/api/v1/assets",
# headers=auth_headers
# )
# assert response.status_code == 200
# data = response.json()
# assert data["code"] == 200
# assert "items" in data["data"]
# assert "total" in data["data"]
# assert "page" in data["data"]
#
# def test_get_assets_with_pagination(self, client: TestClient, auth_headers):
# """测试分页查询"""
# response = client.get(
# "/api/v1/assets?page=1&page_size=10",
# headers=auth_headers
# )
# assert response.status_code == 200
# data = response.json()
# assert data["data"]["page"] == 1
# assert data["data"]["page_size"] == 10
# assert len(data["data"]["items"]) <= 10
#
# def test_get_assets_with_keyword(self, client: TestClient, auth_headers, test_asset):
# """测试关键词搜索"""
# response = client.get(
# f"/api/v1/assets?keyword={test_asset.asset_name}",
# headers=auth_headers
# )
# assert response.status_code == 200
# data = response.json()
# assert len(data["data"]["items"]) > 0
#
# def test_get_assets_with_device_type_filter(self, client: TestClient, auth_headers):
# """测试按设备类型筛选"""
# response = client.get(
# "/api/v1/assets?device_type_id=1",
# headers=auth_headers
# )
# assert response.status_code == 200
#
# def test_get_assets_with_status_filter(self, client: TestClient, auth_headers):
# """测试按状态筛选"""
# response = client.get(
# "/api/v1/assets?status=in_stock",
# headers=auth_headers
# )
# assert response.status_code == 200
#
# def test_get_assets_with_organization_filter(self, client: TestClient, auth_headers):
# """测试按网点筛选"""
# response = client.get(
# "/api/v1/assets?organization_id=1",
# headers=auth_headers
# )
# assert response.status_code == 200
#
# def test_get_assets_with_date_range(self, client: TestClient, auth_headers):
# """测试按采购日期范围筛选"""
# response = client.get(
# "/api/v1/assets?purchase_date_start=2024-01-01&purchase_date_end=2024-12-31",
# headers=auth_headers
# )
# assert response.status_code == 200
#
# def test_get_assets_with_sorting(self, client: TestClient, auth_headers):
# """测试排序"""
# response = client.get(
# "/api/v1/assets?sort_by=purchase_date&sort_order=desc",
# headers=auth_headers
# )
# assert response.status_code == 200
#
# def test_get_assets_unauthorized(self, client: TestClient):
# """测试未授权访问"""
# response = client.get("/api/v1/assets")
# assert response.status_code == 401
#
# @pytest.mark.parametrize("page,page_size", [
# (0, 20), # 页码从0开始
# (1, 0), # 每页0条
# (-1, 20), # 负页码
# (1, 1000), # 超大页码
# ])
# def test_get_assets_invalid_pagination(self, client: TestClient, auth_headers, page, page_size):
# """测试无效分页参数"""
# response = client.get(
# f"/api/v1/assets?page={page}&page_size={page_size}",
# headers=auth_headers
# )
# assert response.status_code == 422
# class TestAssetDetail:
# """测试资产详情"""
#
# def test_get_asset_detail_success(self, client: TestClient, auth_headers, test_asset):
# """测试获取资产详情成功"""
# response = client.get(
# f"/api/v1/assets/{test_asset.id}",
# headers=auth_headers
# )
# assert response.status_code == 200
# data = response.json()
# assert data["code"] == 200
# assert data["data"]["id"] == test_asset.id
# assert data["data"]["asset_code"] == test_asset.asset_code
# assert "status_history" in data["data"]
#
# def test_get_asset_detail_not_found(self, client: TestClient, auth_headers):
# """测试获取不存在的资产"""
# response = client.get(
# "/api/v1/assets/999999",
# headers=auth_headers
# )
# assert response.status_code == 404
# data = response.json()
# assert data["code"] == 30002 # 资产不存在
#
# def test_get_asset_detail_unauthorized(self, client: TestClient, test_asset):
# """测试未授权访问"""
# response = client.get(f"/api/v1/assets/{test_asset.id}")
# assert response.status_code == 401
# class TestCreateAsset:
# """测试创建资产"""
#
# def test_create_asset_success(self, client: TestClient, auth_headers, sample_asset_data):
# """测试创建资产成功"""
# response = client.post(
# "/api/v1/assets",
# headers=auth_headers,
# json=sample_asset_data
# )
# assert response.status_code == 201
# data = response.json()
# assert data["code"] == 200
# assert "asset_code" in data["data"]
# assert data["data"]["asset_code"].startswith("ASSET-")
# assert data["data"]["status"] == "pending"
#
# def test_create_asset_without_auth(self, client: TestClient, sample_asset_data):
# """测试未认证创建"""
# response = client.post("/api/v1/assets", json=sample_asset_data)
# assert response.status_code == 401
#
# def test_create_asset_missing_required_fields(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
#
# @pytest.mark.parametrize("field,value,error_msg", [
# ("asset_name", "", "资产名称不能为空"),
# ("asset_name", "a" * 201, "资产名称过长"),
# ("device_type_id", 0, "设备类型ID无效"),
# ("device_type_id", -1, "设备类型ID无效"),
# ("purchase_price", -100, "采购价格不能为负数"),
# ])
# def test_create_asset_invalid_field(self, client: TestClient, auth_headers, field, value, error_msg):
# """测试无效字段值"""
# data = {
# "asset_name": "测试资产",
# "device_type_id": 1,
# "organization_id": 1
# }
# data[field] = value
#
# response = client.post(
# "/api/v1/assets",
# headers=auth_headers,
# json=data
# )
# assert response.status_code in [400, 422]
#
# def test_create_asset_duplicate_serial_number(self, client: TestClient, auth_headers, sample_asset_data):
# """测试序列号重复"""
# # 第一次创建
# client.post("/api/v1/assets", headers=auth_headers, json=sample_asset_data)
#
# # 第二次使用相同序列号创建
# response = client.post("/api/v1/assets", headers=auth_headers, json=sample_asset_data)
# assert response.status_code == 409 # Conflict
#
# def test_create_asset_with_dynamic_attributes(self, client: TestClient, auth_headers):
# """测试带动态字段创建"""
# data = {
# "asset_name": "测试资产",
# "device_type_id": 1,
# "organization_id": 1,
# "dynamic_attributes": {
# "cpu": "Intel i5-10400",
# "memory": "16GB",
# "disk": "512GB SSD",
# "gpu": "GTX 1660Ti"
# }
# }
#
# response = client.post(
# "/api/v1/assets",
# headers=auth_headers,
# json=data
# )
# assert response.status_code == 201
#
# def test_create_asset_invalid_device_type(self, client: TestClient, auth_headers, sample_asset_data):
# """测试无效的设备类型"""
# sample_asset_data["device_type_id"] = 999999
# response = client.post(
# "/api/v1/assets",
# headers=auth_headers,
# json=sample_asset_data
# )
# assert response.status_code == 400
#
# def test_create_asset_invalid_organization(self, client: TestClient, auth_headers, sample_asset_data):
# """测试无效的网点"""
# sample_asset_data["organization_id"] = 999999
# response = client.post(
# "/api/v1/assets",
# headers=auth_headers,
# json=sample_asset_data
# )
# assert response.status_code == 400
# class TestUpdateAsset:
# """测试更新资产"""
#
# def test_update_asset_success(self, client: TestClient, auth_headers, test_asset):
# """测试更新资产成功"""
# response = client.put(
# f"/api/v1/assets/{test_asset.id}",
# headers=auth_headers,
# json={
# "asset_name": "更新后的资产名称",
# "location": "新位置"
# }
# )
# assert response.status_code == 200
#
# def test_update_asset_partial_fields(self, client: TestClient, auth_headers, test_asset):
# """测试部分字段更新"""
# response = client.put(
# f"/api/v1/assets/{test_asset.id}",
# headers=auth_headers,
# json={"location": "只更新位置"}
# )
# assert response.status_code == 200
#
# def test_update_asset_not_found(self, client: TestClient, auth_headers):
# """测试更新不存在的资产"""
# response = client.put(
# "/api/v1/assets/999999",
# headers=auth_headers,
# json={"asset_name": "新名称"}
# )
# assert response.status_code == 404
#
# def test_update_asset_status_forbidden(self, client: TestClient, auth_headers, test_asset):
# """测试禁止直接修改状态"""
# response = client.put(
# f"/api/v1/assets/{test_asset.id}",
# headers=auth_headers,
# json={"status": "in_use"} # 状态应该通过分配单修改
# )
# # 状态字段应该被忽略或返回错误
# assert response.status_code in [200, 400]
#
# def test_update_asset_unauthorized(self, client: TestClient, test_asset):
# """测试未授权更新"""
# response = client.put(
# f"/api/v1/assets/{test_asset.id}",
# json={"asset_name": "新名称"}
# )
# assert response.status_code == 401
# class TestDeleteAsset:
# """测试删除资产"""
#
# def test_delete_asset_success(self, client: TestClient, auth_headers, test_asset):
# """测试删除资产成功"""
# response = client.delete(
# f"/api/v1/assets/{test_asset.id}",
# headers=auth_headers
# )
# assert response.status_code == 200
#
# # 验证删除
# get_response = client.get(
# f"/api/v1/assets/{test_asset.id}",
# headers=auth_headers
# )
# assert get_response.status_code == 404
#
# def test_delete_asset_not_found(self, client: TestClient, auth_headers):
# """测试删除不存在的资产"""
# response = client.delete(
# "/api/v1/assets/999999",
# headers=auth_headers
# )
# assert response.status_code == 404
#
# def test_delete_asset_in_use(self, client: TestClient, auth_headers):
# """测试删除使用中的资产"""
# # 创建使用中的资产
# # ... 创建in_use状态的资产
#
# response = client.delete(
# "/api/v1/assets/1",
# headers=auth_headers
# )
# # 使用中的资产不能删除
# assert response.status_code == 400
#
# def test_delete_asset_without_permission(self, client: TestClient, auth_headers):
# """测试无权限删除"""
# # 使用普通用户token而非管理员
# response = client.delete(
# "/api/v1/assets/1",
# headers=auth_headers
# )
# assert response.status_code == 403
# class TestAssetImport:
# """测试批量导入资产"""
#
# def test_import_assets_success(self, client: TestClient, auth_headers):
# """测试导入成功"""
# # 准备测试Excel文件
# # ... 创建临时Excel文件
#
# with open("test_import.xlsx", "rb") as f:
# files = {"file": ("test_import.xlsx", f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
# response = client.post(
# "/api/v1/assets/import",
# headers=auth_headers,
# files=files
# )
#
# assert response.status_code == 200
# data = response.json()
# assert data["data"]["total"] > 0
# assert data["data"]["success"] > 0
#
# def test_import_assets_partial_failure(self, client: TestClient, auth_headers):
# """测试部分失败"""
# # 准备包含错误数据的Excel文件
#
# with open("test_import_partial_fail.xlsx", "rb") as f:
# files = {"file": ("test_import.xlsx", f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
# response = client.post(
# "/api/v1/assets/import",
# headers=auth_headers,
# files=files
# )
#
# assert response.status_code == 200
# data = response.json()
# assert data["data"]["failed"] > 0
# assert len(data["data"]["errors"]) > 0
#
# def test_import_assets_invalid_file_format(self, client: TestClient, auth_headers):
# """测试无效文件格式"""
# with open("test.txt", "rb") as f:
# files = {"file": ("test.txt", f, "text/plain")}
# response = client.post(
# "/api/v1/assets/import",
# headers=auth_headers,
# files=files
# )
#
# assert response.status_code == 400
#
# def test_import_assets_missing_columns(self, client: TestClient, auth_headers):
# """测试缺少必填列"""
# # 准备缺少必填列的Excel文件
#
# with open("test_missing_columns.xlsx", "rb") as f:
# files = {"file": ("test.xlsx", f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
# response = client.post(
# "/api/v1/assets/import",
# headers=auth_headers,
# files=files
# )
#
# assert response.status_code == 400
# class TestAssetScan:
# """测试扫码查询"""
#
# def test_scan_asset_success(self, client: TestClient, auth_headers, test_asset):
# """测试扫码查询成功"""
# response = client.get(
# f"/api/v1/assets/scan/{test_asset.asset_code}",
# headers=auth_headers
# )
# assert response.status_code == 200
# data = response.json()
# assert data["data"]["asset_code"] == test_asset.asset_code
#
# def test_scan_asset_invalid_code(self, client: TestClient, auth_headers):
# """测试无效的资产编码"""
# response = client.get(
# "/api/v1/assets/scan/INVALID-CODE",
# headers=auth_headers
# )
# assert response.status_code == 404
#
# def test_scan_asset_without_auth(self, client: TestClient, test_asset):
# """测试未认证扫码"""
# response = client.get(f"/api/v1/assets/scan/{test_asset.asset_code}")
# assert response.status_code == 401
# class TestAssetStatistics:
# """测试资产统计"""
#
# def test_get_asset_summary(self, client: TestClient, auth_headers):
# """测试获取资产汇总"""
# response = client.get(
# "/api/v1/assets",
# headers=auth_headers
# )
# assert response.status_code == 200
# data = response.json()
# assert "summary" in data["data"]
# assert "total_count" in data["data"]["summary"]
# assert "total_value" in data["data"]["summary"]
# assert "status_distribution" in data["data"]["summary"]

356
tests/api/test_auth.py Normal file
View File

@@ -0,0 +1,356 @@
"""
认证模块API测试
测试内容:
- 用户登录
- Token刷新
- 用户登出
- 修改密码
- 验证码获取
"""
import pytest
# from fastapi.testclient import TestClient
# from app.core.config import settings
# class TestAuthLogin:
# """测试用户登录"""
#
# def test_login_success(self, client: TestClient, test_user):
# """测试登录成功"""
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": "testuser",
# "password": "Test123",
# "captcha": "1234",
# "captcha_key": "test-uuid"
# }
# )
# assert response.status_code == 200
# data = response.json()
# assert data["code"] == 200
# assert "access_token" in data["data"]
# assert "refresh_token" in data["data"]
# assert data["data"]["token_type"] == "Bearer"
# assert "user" in data["data"]
#
# def test_login_wrong_password(self, client: TestClient):
# """测试密码错误"""
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": "testuser",
# "password": "WrongPassword",
# "captcha": "1234",
# "captcha_key": "test-uuid"
# }
# )
# assert response.status_code == 401
# data = response.json()
# assert data["code"] == 10001 # 用户名或密码错误
#
# def test_login_user_not_found(self, client: TestClient):
# """测试用户不存在"""
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": "nonexistent",
# "password": "Test123",
# "captcha": "1234",
# "captcha_key": "test-uuid"
# }
# )
# assert response.status_code == 401
#
# def test_login_missing_fields(self, client: TestClient):
# """测试缺少必填字段"""
# response = client.post(
# "/api/v1/auth/login",
# json={"username": "testuser"}
# )
# assert response.status_code == 422 # Validation error
#
# @pytest.mark.parametrize("username", [
# "", # 空字符串
# "ab", # 太短
# "a" * 51, # 太长
# ])
# def test_login_invalid_username(self, client: TestClient, username):
# """测试无效用户名"""
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": username,
# "password": "Test123",
# "captcha": "1234",
# "captcha_key": "test-uuid"
# }
# )
# assert response.status_code == 422
#
# @pytest.mark.parametrize("password", [
# "", # 空字符串
# "short", # 太短
# "nospecial123", # 缺少特殊字符
# "NOlower123!", # 缺少小写字母
# "noupper123!", # 缺少大写字母
# "NoNumber!!", # 缺少数字
# ])
# def test_login_invalid_password(self, client: TestClient, password):
# """测试无效密码"""
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": "testuser",
# "password": password,
# "captcha": "1234",
# "captcha_key": "test-uuid"
# }
# )
# # 某些情况可能是422(验证失败),某些情况可能是401(认证失败)
# assert response.status_code in [400, 422, 401]
#
# def test_login_account_locked(self, client: TestClient, db):
# """测试账户被锁定"""
# # 创建一个锁定的账户
# # ... 创建锁定用户逻辑
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": "lockeduser",
# "password": "Test123",
# "captcha": "1234",
# "captcha_key": "test-uuid"
# }
# )
# assert response.status_code == 403
#
# def test_login_account_disabled(self, client: TestClient, db):
# """测试账户被禁用"""
# # ... 创建禁用用户逻辑
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": "disableduser",
# "password": "Test123",
# "captcha": "1234",
# "captcha_key": "test-uuid"
# }
# )
# assert response.status_code == 403
# class TestTokenRefresh:
# """测试Token刷新"""
#
# def test_refresh_token_success(self, client: TestClient, test_user):
# """测试刷新Token成功"""
# # 先登录获取refresh_token
# login_response = client.post(
# "/api/v1/auth/login",
# json={
# "username": "testuser",
# "password": "Test123",
# "captcha": "1234",
# "captcha_key": "test-uuid"
# }
# )
# refresh_token = login_response.json()["data"]["refresh_token"]
#
# # 刷新Token
# response = client.post(
# "/api/v1/auth/refresh",
# json={"refresh_token": refresh_token}
# )
# assert response.status_code == 200
# data = response.json()
# assert data["code"] == 200
# assert "access_token" in data["data"]
# assert "expires_in" in data["data"]
#
# def test_refresh_token_invalid(self, client: TestClient):
# """测试无效的refresh_token"""
# response = client.post(
# "/api/v1/auth/refresh",
# json={"refresh_token": "invalid_token"}
# )
# assert response.status_code == 401
# data = response.json()
# assert data["code"] == 10004 # Token无效
#
# def test_refresh_token_expired(self, client: TestClient):
# """测试过期的refresh_token"""
# response = client.post(
# "/api/v1/auth/refresh",
# json={"refresh_token": "expired_token"}
# )
# assert response.status_code == 401
# data = response.json()
# assert data["code"] == 10003 # Token过期
# class TestAuthLogout:
# """测试用户登出"""
#
# def test_logout_success(self, client: TestClient, auth_headers):
# """测试登出成功"""
# response = client.post(
# "/api/v1/auth/logout",
# headers=auth_headers
# )
# assert response.status_code == 200
# data = response.json()
# assert data["code"] == 200
# assert data["message"] == "登出成功"
#
# def test_logout_without_auth(self, client: TestClient):
# """测试未认证登出"""
# response = client.post("/api/v1/auth/logout")
# assert response.status_code == 401
# class TestChangePassword:
# """测试修改密码"""
#
# def test_change_password_success(self, client: TestClient, auth_headers):
# """测试修改密码成功"""
# response = client.put(
# "/api/v1/auth/change-password",
# headers=auth_headers,
# json={
# "old_password": "Test123",
# "new_password": "NewTest456",
# "confirm_password": "NewTest456"
# }
# )
# assert response.status_code == 200
# data = response.json()
# assert data["code"] == 200
# assert data["message"] == "密码修改成功"
#
# def test_change_password_wrong_old_password(self, client: TestClient, auth_headers):
# """测试旧密码错误"""
# response = client.put(
# "/api/v1/auth/change-password",
# headers=auth_headers,
# json={
# "old_password": "WrongPassword",
# "new_password": "NewTest456",
# "confirm_password": "NewTest456"
# }
# )
# assert response.status_code == 400
#
# def test_change_password_mismatch(self, client: TestClient, auth_headers):
# """测试两次密码不一致"""
# response = client.put(
# "/api/v1/auth/change-password",
# headers=auth_headers,
# json={
# "old_password": "Test123",
# "new_password": "NewTest456",
# "confirm_password": "DifferentPass789"
# }
# )
# assert response.status_code == 400
#
# def test_change_password_weak_password(self, client: TestClient, auth_headers):
# """测试弱密码"""
# response = client.put(
# "/api/v1/auth/change-password",
# headers=auth_headers,
# json={
# "old_password": "Test123",
# "new_password": "weak",
# "confirm_password": "weak"
# }
# )
# assert response.status_code == 400
#
# def test_change_password_without_auth(self, client: TestClient):
# """测试未认证修改密码"""
# response = client.put(
# "/api/v1/auth/change-password",
# json={
# "old_password": "Test123",
# "new_password": "NewTest456",
# "confirm_password": "NewTest456"
# }
# )
# assert response.status_code == 401
# class TestCaptcha:
# """测试验证码"""
#
# def test_get_captcha_success(self, client: TestClient):
# """测试获取验证码成功"""
# response = client.get("/api/v1/auth/captcha")
# assert response.status_code == 200
# data = response.json()
# assert data["code"] == 200
# assert "captcha_key" in data["data"]
# assert "captcha_image" in data["data"]
# assert data["data"]["captcha_image"].startswith("data:image/png;base64,")
#
# @pytest.mark.parametrize("count", range(5))
# def test_get_captcha_multiple_times(self, client: TestClient, count):
# """测试多次获取验证码,每次应该不同"""
# response = client.get("/api/v1/auth/captcha")
# assert response.status_code == 200
# data = response.json()
# assert data["data"]["captcha_key"] is not None
# class TestRateLimiting:
# """测试请求频率限制"""
#
# def test_login_rate_limiting(self, client: TestClient):
# """测试登录接口频率限制"""
# # 登录接口限制10次/分钟
# for i in range(11):
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": "testuser",
# "password": "wrongpass",
# "captcha": "1234",
# "captcha_key": f"test-{i}"
# }
# )
#
# # 第11次应该被限流
# assert response.status_code == 429
# data = response.json()
# assert data["code"] == 429
# assert "retry_after" in data["data"]
# 测试SQL注入攻击
# class TestSecurity:
# """测试安全性"""
#
# def test_sql_injection_prevention(self, client: TestClient):
# """测试防止SQL注入"""
# malicious_inputs = [
# "admin' OR '1'='1",
# "admin'--",
# "admin'/*",
# "' OR 1=1--",
# "'; DROP TABLE users--"
# ]
#
# for malicious_input in malicious_inputs:
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": malicious_input,
# "password": "Test123",
# "captcha": "1234",
# "captcha_key": "test"
# }
# )
# # 应该返回认证失败,而不是数据库错误
# assert response.status_code in [401, 400, 422]

View File

@@ -0,0 +1,880 @@
"""
设备类型管理模块API测试
测试内容:
- 设备类型CRUD测试(15+用例)
- 动态字段配置测试(10+用例)
- 字段验证测试(10+用例)
- 参数验证测试(10+用例)
- 异常处理测试(5+用例)
"""
import pytest
from httpx import AsyncClient
from datetime import datetime
# ==================== 设备类型CRUD测试 ====================
class TestDeviceTypeCRUD:
"""测试设备类型CRUD操作"""
@pytest.mark.asyncio
async def test_create_device_type_success(
self,
client: AsyncClient,
admin_headers: dict,
sample_device_type_data: dict
):
"""测试创建设备类型成功"""
response = await client.post(
"/api/v1/device-types",
headers=admin_headers,
json=sample_device_type_data
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
assert data["data"]["type_code"] == sample_device_type_data["type_code"]
assert data["data"]["type_name"] == sample_device_type_data["type_name"]
assert "id" in data["data"]
@pytest.mark.asyncio
async def test_create_device_type_duplicate_code(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type
):
"""测试创建重复代码的设备类型"""
response = await client.post(
"/api/v1/device-types",
headers=admin_headers,
json={
"type_code": test_device_type.type_code,
"type_name": "另一个类型"
}
)
assert response.status_code in [400, 409]
@pytest.mark.asyncio
async def test_get_device_type_list(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type
):
"""测试获取设备类型列表"""
response = await client.get(
"/api/v1/device-types",
headers=admin_headers
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
assert len(data["data"]) >= 1
@pytest.mark.asyncio
async def test_get_device_type_by_id(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type
):
"""测试根据ID获取设备类型"""
response = await client.get(
f"/api/v1/device-types/{test_device_type.id}",
headers=admin_headers
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
assert data["data"]["id"] == test_device_type.id
assert data["data"]["type_code"] == test_device_type.type_code
@pytest.mark.asyncio
async def test_get_device_type_by_code(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type
):
"""测试根据代码获取设备类型"""
response = await client.get(
f"/api/v1/device-types/code/{test_device_type.type_code}",
headers=admin_headers
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
assert data["data"]["type_code"] == test_device_type.type_code
@pytest.mark.asyncio
async def test_get_device_type_with_fields(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type_with_fields
):
"""测试获取设备类型及其字段"""
response = await client.get(
f"/api/v1/device-types/{test_device_type_with_fields.id}",
headers=admin_headers
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
# 验证字段存在
# assert "fields" in data["data"]
@pytest.mark.asyncio
async def test_update_device_type_success(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type
):
"""测试更新设备类型成功"""
response = await client.put(
f"/api/v1/device-types/{test_device_type.id}",
headers=admin_headers,
json={
"type_name": "更新后的类型名称",
"description": "更新后的描述"
}
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
@pytest.mark.asyncio
async def test_update_device_type_status(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type
):
"""测试更新设备类型状态"""
response = await client.put(
f"/api/v1/device-types/{test_device_type.id}",
headers=admin_headers,
json={"status": "inactive"}
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_delete_device_type_success(
self,
client: AsyncClient,
admin_headers: dict,
db_session,
test_device_type
):
"""测试删除设备类型成功"""
response = await client.delete(
f"/api/v1/device-types/{test_device_type.id}",
headers=admin_headers
)
assert response.status_code == 200
# 验证软删除
get_response = await client.get(
f"/api/v1/device-types/{test_device_type.id}",
headers=admin_headers
)
# 应该返回404或显示已删除
assert get_response.status_code in [404, 200]
@pytest.mark.asyncio
async def test_delete_device_type_with_assets_forbidden(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type
):
"""测试删除有关联资产的设备类型(应该失败)"""
# 假设test_device_type有关联资产
# 实际测试中需要先创建资产
response = await client.delete(
f"/api/v1/device-types/{test_device_type.id}",
headers=admin_headers
)
# 如果有关联资产应该返回400或403
# assert response.status_code in [400, 403]
@pytest.mark.asyncio
async def test_filter_device_type_by_category(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type
):
"""测试按分类筛选设备类型"""
response = await client.get(
f"/api/v1/device-types?category={test_device_type.category}",
headers=admin_headers
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
# 验证筛选结果
# for item in data["data"]:
# assert item["category"] == test_device_type.category
@pytest.mark.asyncio
async def test_filter_device_type_by_status(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type
):
"""测试按状态筛选设备类型"""
response = await client.get(
f"/api/v1/device-types?status={test_device_type.status}",
headers=admin_headers
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
@pytest.mark.asyncio
async def test_get_device_type_not_found(
self,
client: AsyncClient,
admin_headers: dict
):
"""测试获取不存在的设备类型"""
response = await client.get(
"/api/v1/device-types/999999",
headers=admin_headers
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_update_device_type_not_found(
self,
client: AsyncClient,
admin_headers: dict
):
"""测试更新不存在的设备类型"""
response = await client.put(
"/api/v1/device-types/999999",
headers=admin_headers,
json={"type_name": "新名称"}
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_create_device_type_unauthorized(
self,
client: AsyncClient,
sample_device_type_data: dict
):
"""测试未授权创建设备类型"""
response = await client.post(
"/api/v1/device-types",
json=sample_device_type_data
)
assert response.status_code == 401
# ==================== 动态字段配置测试 ====================
class TestDynamicFieldConfig:
"""测试动态字段配置"""
@pytest.mark.asyncio
async def test_add_field_to_device_type(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType,
sample_field_data: dict
):
"""测试为设备类型添加字段"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json=sample_field_data
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
assert data["data"]["field_code"] == sample_field_data["field_code"]
@pytest.mark.asyncio
async def test_add_required_field(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType
):
"""测试添加必填字段"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": "required_field",
"field_name": "必填字段",
"field_type": "text",
"is_required": True,
"sort_order": 10
}
)
assert response.status_code == 200
data = response.json()
assert data["data"]["is_required"] is True
@pytest.mark.asyncio
async def test_add_select_field_with_options(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType
):
"""测试添加下拉选择字段"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": "status",
"field_name": "状态",
"field_type": "select",
"is_required": True,
"options": [
{"label": "启用", "value": "enabled"},
{"label": "禁用", "value": "disabled"}
],
"sort_order": 10
}
)
assert response.status_code == 200
data = response.json()
assert data["data"]["field_type"] == "select"
assert len(data["data"]["options"]) == 2
@pytest.mark.asyncio
async def test_add_number_field_with_validation(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType
):
"""测试添加数字字段并设置验证规则"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": "price",
"field_name": "价格",
"field_type": "number",
"is_required": False,
"validation_rules": {
"min": 0,
"max": 1000000
},
"sort_order": 10
}
)
assert response.status_code == 200
data = response.json()
assert data["data"]["field_type"] == "number"
assert "validation_rules" in data["data"]
@pytest.mark.asyncio
async def test_get_device_type_fields(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type_with_fields: DeviceType
):
"""测试获取设备类型的字段列表"""
response = await client.get(
f"/api/v1/device-types/{test_device_type_with_fields.id}/fields",
headers=admin_headers
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 200
assert len(data["data"]) >= 3 # 至少3个字段
@pytest.mark.asyncio
async def test_update_field_success(
self,
client: AsyncClient,
admin_headers: dict,
db_session,
test_device_type_with_fields: DeviceType
):
"""测试更新字段成功"""
# 获取第一个字段
fields_response = await client.get(
f"/api/v1/device-types/{test_device_type_with_fields.id}/fields",
headers=admin_headers
)
field_id = fields_response.json()["data"][0]["id"]
response = await client.put(
f"/api/v1/device-types/{test_device_type_with_fields.id}/fields/{field_id}",
headers=admin_headers,
json={
"field_name": "更新后的字段名",
"is_required": False
}
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_delete_field_success(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type_with_fields: DeviceType
):
"""测试删除字段成功"""
fields_response = await client.get(
f"/api/v1/device-types/{test_device_type_with_fields.id}/fields",
headers=admin_headers
)
field_id = fields_response.json()["data"][0]["id"]
response = await client.delete(
f"/api/v1/device-types/{test_device_type_with_fields.id}/fields/{field_id}",
headers=admin_headers
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_add_duplicate_field_code(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type_with_fields: DeviceType,
sample_field_data: dict
):
"""测试添加重复的字段代码"""
# 第一次添加
await client.post(
f"/api/v1/device-types/{test_device_type_with_fields.id}/fields",
headers=admin_headers,
json=sample_field_data
)
# 第二次添加相同代码
response = await client.post(
f"/api/v1/device-types/{test_device_type_with_fields.id}/fields",
headers=admin_headers,
json=sample_field_data
)
assert response.status_code in [400, 409]
@pytest.mark.asyncio
async def test_fields_sorted_by_order(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type_with_fields: DeviceType
):
"""测试字段按sort_order排序"""
response = await client.get(
f"/api/v1/device-types/{test_device_type_with_fields.id}/fields",
headers=admin_headers
)
assert response.status_code == 200
data = response.json()
fields = data["data"]
# 验证排序
for i in range(len(fields) - 1):
assert fields[i]["sort_order"] <= fields[i + 1]["sort_order"]
# ==================== 字段验证测试 ====================
class TestFieldValidation:
"""测试字段验证"""
@pytest.mark.asyncio
@pytest.mark.parametrize("field_code,field_name,expected_status", [
("", "字段名", 422), # 空字段代码
("a" * 51, "字段名", 422), # 字段代码过长
("valid_code", "", 422), # 空字段名称
("valid_code", "a" * 101, 422), # 字段名称过长
])
async def test_field_name_validation(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType,
field_code: str,
field_name: str,
expected_status: int
):
"""测试字段名称验证"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": field_code,
"field_name": field_name,
"field_type": "text",
"sort_order": 1
}
)
assert response.status_code == expected_status
@pytest.mark.asyncio
@pytest.mark.parametrize("field_type", [
"text", "textarea", "number", "date", "select",
"multiselect", "boolean", "email", "phone", "url"
])
async def test_valid_field_types(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType,
field_type: str
):
"""测试有效的字段类型"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": f"test_{field_type}",
"field_name": f"测试{field_type}",
"field_type": field_type,
"sort_order": 1
}
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_invalid_field_type(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType
):
"""测试无效的字段类型"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": "test",
"field_name": "测试",
"field_type": "invalid_type",
"sort_order": 1
}
)
assert response.status_code in [400, 422]
@pytest.mark.asyncio
async def test_select_field_without_options(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType
):
"""测试select类型字段缺少options"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": "test_select",
"field_name": "测试选择",
"field_type": "select",
"sort_order": 1
}
)
# select类型应该有options
assert response.status_code in [400, 422]
@pytest.mark.asyncio
async def test_validation_rules_json_format(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType
):
"""测试验证规则的JSON格式"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": "test_validation",
"field_name": "测试验证",
"field_type": "text",
"validation_rules": {
"min_length": 1,
"max_length": 100,
"pattern": "^[A-Za-z0-9]+$"
},
"sort_order": 1
}
)
assert response.status_code == 200
data = response.json()
assert "validation_rules" in data["data"]
@pytest.mark.asyncio
async def test_placeholder_and_help_text(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType
):
"""测试placeholder和help_text"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": "test_help",
"field_name": "测试帮助",
"field_type": "text",
"placeholder": "请输入...",
"help_text": "这是帮助文本",
"sort_order": 1
}
)
assert response.status_code == 200
data = response.json()
assert data["data"]["placeholder"] == "请输入..."
assert data["data"]["help_text"] == "这是帮助文本"
# ==================== 参数验证测试 ====================
class TestDeviceTypeParameterValidation:
"""测试设备类型参数验证"""
@pytest.mark.asyncio
@pytest.mark.parametrize("type_code,expected_status", [
("", 422), # 空代码
("AB", 422), # 太短
("a" * 51, 422), # 太长
("VALID_CODE", 200), # 有效
])
async def test_type_code_validation(
self,
client: AsyncClient,
admin_headers: dict,
type_code: str,
expected_status: int
):
"""测试类型代码验证"""
response = await client.post(
"/api/v1/device-types",
headers=admin_headers,
json={
"type_code": type_code,
"type_name": "测试类型",
"category": "IT设备"
}
)
assert response.status_code == expected_status
@pytest.mark.asyncio
@pytest.mark.parametrize("type_name,expected_status", [
("", 422), # 空名称
("a" * 201, 422), # 太长
("有效名称", 200), # 有效
])
async def test_type_name_validation(
self,
client: AsyncClient,
admin_headers: dict,
type_name: str,
expected_status: int
):
"""测试类型名称验证"""
response = await client.post(
"/api/v1/device-types",
headers=admin_headers,
json={
"type_code": "TEST_CODE",
"type_name": type_name
}
)
assert response.status_code == expected_status
@pytest.mark.asyncio
async def test_sort_order_validation(
self,
client: AsyncClient,
admin_headers: dict
):
"""测试排序验证"""
response = await client.post(
"/api/v1/device-types",
headers=admin_headers,
json={
"type_code": "TEST_SORT",
"type_name": "测试排序",
"sort_order": -1 # 负数
}
)
# 排序可以是负数,或者应该返回422
# assert response.status_code in [200, 422]
@pytest.mark.asyncio
@pytest.mark.parametrize("status", [
"active", "inactive", "invalid_status"
])
async def test_status_validation(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType,
status: str
):
"""测试状态验证"""
response = await client.put(
f"/api/v1/device-types/{test_device_type.id}",
headers=admin_headers,
json={"status": status}
)
# 有效状态应该是200,无效状态应该是422
if status in ["active", "inactive"]:
assert response.status_code == 200
else:
assert response.status_code in [400, 422]
# ==================== 异常处理测试 ====================
class TestDeviceTypeExceptionHandling:
"""测试异常处理"""
@pytest.mark.asyncio
async def test_concurrent_device_type_creation(
self,
client: AsyncClient,
admin_headers: dict
):
"""测试并发创建相同代码的设备类型"""
import asyncio
data = {
"type_code": "CONCURRENT_TEST",
"type_name": "并发测试"
}
# 并发创建
tasks = [
client.post("/api/v1/device-types", headers=admin_headers, json=data)
for _ in range(2)
]
responses = await asyncio.gather(*tasks)
# 应该只有一个成功,另一个失败
success_count = sum(1 for r in responses if r.status_code == 200)
assert success_count == 1
@pytest.mark.asyncio
async def test_update_non_existent_field(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType
):
"""测试更新不存在的字段"""
response = await client.put(
f"/api/v1/device-types/{test_device_type.id}/fields/999999",
headers=admin_headers,
json={"field_name": "更新"}
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_delete_non_existent_device_type(
self,
client: AsyncClient,
admin_headers: dict
):
"""测试删除不存在的设备类型"""
response = await client.delete(
"/api/v1/device-types/999999",
headers=admin_headers
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_field_with_invalid_json_validation(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType
):
"""测试字段包含无效的JSON验证规则"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": "test",
"field_name": "测试",
"field_type": "text",
"validation_rules": "invalid json string", # 应该是对象
"sort_order": 1
}
)
# 应该返回验证错误
assert response.status_code in [400, 422]
@pytest.mark.asyncio
async def test_field_with_invalid_options_format(
self,
client: AsyncClient,
admin_headers: dict,
test_device_type: DeviceType
):
"""测试select字段包含无效的options格式"""
response = await client.post(
f"/api/v1/device-types/{test_device_type.id}/fields",
headers=admin_headers,
json={
"field_code": "test",
"field_name": "测试",
"field_type": "select",
"options": "invalid options", # 应该是数组
"sort_order": 1
}
)
assert response.status_code in [400, 422]

View File

@@ -0,0 +1,891 @@
"""
维修管理 API 测试
测试范围:
- 维修记录CRUD测试 (20+用例)
- 维修状态管理测试 (15+用例)
- 维修费用测试 (10+用例)
- 维修历史测试 (5+用例)
总计: 50+ 用例
"""
import pytest
from datetime import datetime, timedelta
from typing import List
from decimal import Decimal
from sqlalchemy.orm import Session
from app.models.maintenance import Maintenance, MaintenancePart
from app.models.asset import Asset
from app.schemas.maintenance import (
MaintenanceCreate,
MaintenanceStatus,
MaintenanceType,
MaintenancePriority
)
# ================================
# Fixtures
# ================================
@pytest.fixture
def test_assets_for_maintenance(db: Session) -> List[Asset]:
"""创建需要维修的测试资产"""
assets = []
for i in range(3):
asset = Asset(
asset_code=f"TEST-MAINT-{i+1:03d}",
asset_name=f"测试维修资产{i+1}",
device_type_id=1,
organization_id=1,
status="maintenance",
purchase_date=datetime.now() - timedelta(days=365)
)
db.add(asset)
assets.append(asset)
db.commit()
for asset in assets:
db.refresh(asset)
return assets
@pytest.fixture
def test_maintenance_record(db: Session, test_assets_for_maintenance: List[Asset]) -> Maintenance:
"""创建测试维修记录"""
maintenance = Maintenance(
maintenance_no="MAINT-2025-001",
asset_id=test_assets_for_maintenance[0].id,
maintenance_type=MaintenanceType.PREVENTIVE,
priority=MaintenancePriority.MEDIUM,
status=MaintenanceStatus.PENDING,
fault_description="设备异常噪音",
reported_by=1,
reported_time=datetime.now(),
estimated_cost=Decimal("500.00"),
estimated_start_time=datetime.now() + timedelta(days=1),
estimated_completion_time=datetime.now() + timedelta(days=3)
)
db.add(maintenance)
db.commit()
db.refresh(maintenance)
return maintenance
@pytest.fixture
def test_maintenance_with_parts(db: Session, test_assets_for_maintenance: List[Asset]) -> Maintenance:
"""创建包含配件的维修记录"""
maintenance = Maintenance(
maintenance_no="MAINT-2025-002",
asset_id=test_assets_for_maintenance[1].id,
maintenance_type=MaintenanceType.CORRECTIVE,
priority=MaintenancePriority.HIGH,
status=MaintenanceStatus.IN_PROGRESS,
fault_description="设备故障无法启动",
reported_by=1,
reported_time=datetime.now(),
actual_start_time=datetime.now(),
estimated_cost=Decimal("1500.00")
)
db.add(maintenance)
db.commit()
db.refresh(maintenance)
# 添加维修配件
parts = [
MaintenancePart(
maintenance_id=maintenance.id,
part_name="电机",
part_code="PART-001",
quantity=1,
unit_price=Decimal("800.00")
),
MaintenancePart(
maintenance_id=maintenance.id,
part_name="轴承",
part_code="PART-002",
quantity=2,
unit_price=Decimal("100.00")
)
]
for part in parts:
db.add(part)
db.commit()
return maintenance
# ================================
# 维修记录CRUD测试 (20+用例)
# ================================
class TestMaintenanceCRUD:
"""维修记录CRUD操作测试"""
def test_create_maintenance_with_valid_data(self, client, auth_headers, test_assets_for_maintenance):
"""测试使用有效数据创建维修记录"""
asset = test_assets_for_maintenance[0]
response = client.post(
"/api/v1/maintenance/",
json={
"asset_id": asset.id,
"maintenance_type": "corrective",
"priority": "high",
"fault_description": "设备故障需要维修",
"reported_by": 1,
"estimated_cost": 1000.00,
"estimated_start_time": (datetime.now() + timedelta(hours=2)).isoformat(),
"estimated_completion_time": (datetime.now() + timedelta(days=2)).isoformat()
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["maintenance_no"] is not None
assert data["status"] == MaintenanceStatus.PENDING
assert data["asset_id"] == asset.id
def test_create_maintenance_with_invalid_asset_id(self, client, auth_headers):
"""测试使用无效资产ID创建维修记录应失败"""
response = client.post(
"/api/v1/maintenance/",
json={
"asset_id": 999999,
"maintenance_type": "corrective",
"priority": "medium",
"fault_description": "测试",
"reported_by": 1
},
headers=auth_headers
)
assert response.status_code == 404
assert "资产不存在" in response.json()["detail"]
def test_create_maintenance_without_fault_description(self, client, auth_headers, test_assets_for_maintenance):
"""测试创建维修记录时未提供故障描述应失败"""
asset = test_assets_for_maintenance[0]
response = client.post(
"/api/v1/maintenance/",
json={
"asset_id": asset.id,
"maintenance_type": "corrective",
"priority": "medium",
"reported_by": 1
},
headers=auth_headers
)
assert response.status_code == 400
assert "故障描述" in response.json()["detail"]
def test_create_maintenance_with_negative_cost(self, client, auth_headers, test_assets_for_maintenance):
"""测试创建负费用的维修记录应失败"""
asset = test_assets_for_maintenance[0]
response = client.post(
"/api/v1/maintenance/",
json={
"asset_id": asset.id,
"maintenance_type": "corrective",
"priority": "medium",
"fault_description": "测试",
"reported_by": 1,
"estimated_cost": -100.00
},
headers=auth_headers
)
assert response.status_code == 400
def test_create_maintenance_auto_updates_asset_status(self, client, auth_headers, db: Session, test_assets_for_maintenance):
"""测试创建维修记录时自动更新资产状态"""
asset = test_assets_for_maintenance[0]
original_status = asset.status
response = client.post(
"/api/v1/maintenance/",
json={
"asset_id": asset.id,
"maintenance_type": "corrective",
"priority": "medium",
"fault_description": "测试自动更新状态",
"reported_by": 1
},
headers=auth_headers
)
assert response.status_code == 200
# 验证资产状态已更新
db.refresh(asset)
assert asset.status == "maintenance"
def test_get_maintenance_list_with_pagination(self, client, auth_headers, test_maintenance_record):
"""测试分页获取维修记录列表"""
response = client.get(
"/api/v1/maintenance/?page=1&page_size=10",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "items" in data
assert "total" in data
assert len(data["items"]) >= 1
def test_get_maintenance_list_with_status_filter(self, client, auth_headers, test_maintenance_record):
"""测试按状态筛选维修记录"""
response = client.get(
f"/api/v1/maintenance/?status={MaintenanceStatus.PENDING}",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
for item in data["items"]:
assert item["status"] == MaintenanceStatus.PENDING
def test_get_maintenance_list_with_asset_filter(self, client, auth_headers, test_maintenance_record):
"""测试按资产筛选维修记录"""
response = client.get(
f"/api/v1/maintenance/?asset_id={test_maintenance_record.asset_id}",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert len(data["items"]) >= 1
def test_get_maintenance_list_with_type_filter(self, client, auth_headers, test_maintenance_record):
"""测试按维修类型筛选"""
response = client.get(
f"/api/v1/maintenance/?maintenance_type={test_maintenance_record.maintenance_type}",
headers=auth_headers
)
assert response.status_code == 200
def test_get_maintenance_list_with_priority_filter(self, client, auth_headers, test_maintenance_record):
"""测试按优先级筛选"""
response = client.get(
f"/api/v1/maintenance/?priority={test_maintenance_record.priority}",
headers=auth_headers
)
assert response.status_code == 200
def test_get_maintenance_list_with_date_range(self, client, auth_headers, test_maintenance_record):
"""测试按日期范围筛选"""
start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
end_date = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
response = client.get(
f"/api/v1/maintenance/?start_date={start_date}&end_date={end_date}",
headers=auth_headers
)
assert response.status_code == 200
def test_get_maintenance_by_id(self, client, auth_headers, test_maintenance_record):
"""测试通过ID获取维修记录详情"""
response = client.get(
f"/api/v1/maintenance/{test_maintenance_record.id}",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["id"] == test_maintenance_record.id
assert data["maintenance_no"] == test_maintenance_record.maintenance_no
assert "asset" in data
def test_get_maintenance_by_invalid_id(self, client, auth_headers):
"""测试通过无效ID获取维修记录应返回404"""
response = client.get(
"/api/v1/maintenance/999999",
headers=auth_headers
)
assert response.status_code == 404
def test_update_maintenance_fault_description(self, client, auth_headers, test_maintenance_record):
"""测试更新故障描述"""
response = client.put(
f"/api/v1/maintenance/{test_maintenance_record.id}",
json={"fault_description": "更新后的故障描述"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["fault_description"] == "更新后的故障描述"
def test_update_maintenance_priority(self, client, auth_headers, test_maintenance_record):
"""测试更新优先级"""
response = client.put(
f"/api/v1/maintenance/{test_maintenance_record.id}",
json={"priority": "urgent"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["priority"] == MaintenancePriority.URGENT
def test_update_maintenance_after_start_should_fail(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试维修开始后更新某些字段应失败"""
test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS
db.commit()
response = client.put(
f"/api/v1/maintenance/{test_maintenance_record.id}",
json={"maintenance_type": "preventive"},
headers=auth_headers
)
assert response.status_code == 400
assert "不允许修改" in response.json()["detail"]
def test_delete_pending_maintenance(self, client, auth_headers, db: Session, test_assets_for_maintenance):
"""测试删除待处理的维修记录"""
maintenance = Maintenance(
maintenance_no="MAINT-DEL-001",
asset_id=test_assets_for_maintenance[0].id,
maintenance_type=MaintenanceType.CORRECTIVE,
priority=MaintenancePriority.MEDIUM,
status=MaintenanceStatus.PENDING,
fault_description="待删除",
reported_by=1
)
db.add(maintenance)
db.commit()
db.refresh(maintenance)
response = client.delete(
f"/api/v1/maintenance/{maintenance.id}",
headers=auth_headers
)
assert response.status_code == 200
def test_delete_in_progress_maintenance_should_fail(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试删除进行中的维修记录应失败"""
test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS
db.commit()
response = client.delete(
f"/api/v1/maintenance/{test_maintenance_record.id}",
headers=auth_headers
)
assert response.status_code == 400
assert "不允许删除" in response.json()["detail"]
def test_create_maintenance_with_parts(self, client, auth_headers, test_assets_for_maintenance):
"""测试创建包含配件的维修记录"""
asset = test_assets_for_maintenance[0]
response = client.post(
"/api/v1/maintenance/",
json={
"asset_id": asset.id,
"maintenance_type": "corrective",
"priority": "high",
"fault_description": "需要更换配件",
"reported_by": 1,
"parts": [
{
"part_name": "电机",
"part_code": "PART-001",
"quantity": 1,
"unit_price": 800.00
},
{
"part_name": "轴承",
"part_code": "PART-002",
"quantity": 2,
"unit_price": 100.00
}
]
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "parts" in data
assert len(data["parts"]) == 2
# ================================
# 维修状态管理测试 (15+用例)
# ================================
class TestMaintenanceStatusManagement:
"""维修状态管理测试"""
def test_start_maintenance(self, client, auth_headers, test_maintenance_record):
"""测试开始维修"""
response = client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/start",
json={"start_note": "开始维修", "technician_id": 2},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["status"] == MaintenanceStatus.IN_PROGRESS
assert data["actual_start_time"] is not None
def test_start_maintenance_updates_asset_status(self, client, auth_headers, test_maintenance_record, db: Session):
"""测试开始维修时更新资产状态"""
client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/start",
json={"start_note": "开始维修"},
headers=auth_headers
)
asset = db.query(Asset).filter(Asset.id == test_maintenance_record.asset_id).first()
assert asset.status == "maintenance"
def test_pause_maintenance(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试暂停维修"""
test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS
db.commit()
response = client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/pause",
json={"pause_reason": "等待配件"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["status"] == MaintenanceStatus.PAUSED
def test_resume_maintenance(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试恢复维修"""
test_maintenance_record.status = MaintenanceStatus.PAUSED
db.commit()
response = client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/resume",
json={"resume_note": "配件已到,继续维修"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["status"] == MaintenanceStatus.IN_PROGRESS
def test_complete_maintenance(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试完成维修"""
test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS
db.commit()
response = client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/complete",
json={
"completion_note": "维修完成",
"actual_cost": 1200.00,
"technician_id": 2
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["status"] == MaintenanceStatus.COMPLETED
assert data["actual_completion_time"] is not None
assert data["actual_cost"] == 1200.00
def test_complete_maintenance_updates_asset_status(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试完成维修后恢复资产状态"""
test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS
db.commit()
client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/complete",
json={"completion_note": "完成", "actual_cost": 1000.00},
headers=auth_headers
)
asset = db.query(Asset).filter(Asset.id == test_maintenance_record.asset_id).first()
assert asset.status == "in_stock"
def test_cancel_maintenance(self, client, auth_headers, test_maintenance_record):
"""测试取消维修"""
response = client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/cancel",
json={"cancellation_reason": "资产报废"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["status"] == MaintenanceStatus.CANCELLED
def test_cancel_maintenance_updates_asset_status(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试取消维修后恢复资产状态"""
client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/cancel",
json={"cancellation_reason": "取消维修"},
headers=auth_headers
)
asset = db.query(Asset).filter(Asset.id == test_maintenance_record.asset_id).first()
assert asset.status == "in_stock"
def test_assign_technician(self, client, auth_headers, test_maintenance_record):
"""测试分配维修人员"""
response = client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/assign-technician",
json={"technician_id": 2, "assignment_note": "指派张工负责"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["technician_id"] == 2
def test_add_maintenance_progress_note(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试添加维修进度备注"""
test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS
db.commit()
response = client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/progress-notes",
json={"note": "已更换故障配件", "progress_percentage": 50},
headers=auth_headers
)
assert response.status_code == 200
def test_get_maintenance_progress_notes(self, client, auth_headers, test_maintenance_record):
"""测试获取维修进度备注"""
response = client.get(
f"/api/v1/maintenance/{test_maintenance_record.id}/progress-notes",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_update_maintenance_progress(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试更新维修进度"""
test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS
db.commit()
response = client.put(
f"/api/v1/maintenance/{test_maintenance_record.id}/progress",
json={"progress_percentage": 75},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["progress_percentage"] == 75
def test_invalid_status_transition(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试无效的状态转换"""
test_maintenance_record.status = MaintenanceStatus.COMPLETED
db.commit()
response = client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/start",
json={"start_note": "尝试重新开始"},
headers=auth_headers
)
assert response.status_code == 400
def test_get_maintenance_status_history(self, client, auth_headers, test_maintenance_record):
"""测试获取状态变更历史"""
response = client.get(
f"/api/v1/maintenance/{test_maintenance_record.id}/status-history",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_auto_calculate_duration(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试自动计算维修时长"""
test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS
test_maintenance_record.actual_start_time = datetime.now() - timedelta(days=2)
db.commit()
client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/complete",
json={"completion_note": "完成", "actual_cost": 1000.00},
headers=auth_headers
)
db.refresh(test_maintenance_record)
assert test_maintenance_record.duration_hours is not None
# ================================
# 维修费用测试 (10+用例)
# ================================
class TestMaintenanceCost:
"""维修费用测试"""
def test_record_initial_cost_estimate(self, client, auth_headers, test_assets_for_maintenance):
"""测试记录初始费用估算"""
asset = test_assets_for_maintenance[0]
response = client.post(
"/api/v1/maintenance/",
json={
"asset_id": asset.id,
"maintenance_type": "corrective",
"priority": "medium",
"fault_description": "测试费用估算",
"reported_by": 1,
"estimated_cost": 2000.00
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["estimated_cost"] == 2000.00
def test_update_cost_estimate(self, client, auth_headers, test_maintenance_record):
"""测试更新费用估算"""
response = client.put(
f"/api/v1/maintenance/{test_maintenance_record.id}",
json={"estimated_cost": 800.00},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["estimated_cost"] == 800.00
def test_record_actual_cost(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试记录实际费用"""
test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS
db.commit()
response = client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/record-cost",
json={"actual_cost": 1500.00, "cost_breakdown": {"parts": 1000.00, "labor": 500.00}},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["actual_cost"] == 1500.00
def test_calculate_total_parts_cost(self, client, auth_headers, test_maintenance_with_parts):
"""测试计算配件总费用"""
response = client.get(
f"/api/v1/maintenance/{test_maintenance_with_parts.id}/parts-cost",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["total_parts_cost"] == 1000.00 # 800 + 100*2
def test_add_maintenance_part(self, client, auth_headers, test_maintenance_record):
"""测试添加维修配件"""
response = client.post(
f"/api/v1/maintenance/{test_maintenance_record.id}/parts",
json={
"part_name": "传感器",
"part_code": "PART-003",
"quantity": 1,
"unit_price": 300.00
},
headers=auth_headers
)
assert response.status_code == 200
def test_update_maintenance_part(self, client, auth_headers, test_maintenance_with_parts):
"""测试更新维修配件"""
part = test_maintenance_with_parts.parts[0]
response = client.put(
f"/api/v1/maintenance/{test_maintenance_with_parts.id}/parts/{part.id}",
json={"quantity": 2, "unit_price": 750.00},
headers=auth_headers
)
assert response.status_code == 200
def test_delete_maintenance_part(self, client, auth_headers, test_maintenance_with_parts):
"""测试删除维修配件"""
part = test_maintenance_with_parts.parts[0]
response = client.delete(
f"/api/v1/maintenance/{test_maintenance_with_parts.id}/parts/{part.id}",
headers=auth_headers
)
assert response.status_code == 200
def test_get_maintenance_parts_list(self, client, auth_headers, test_maintenance_with_parts):
"""测试获取维修配件列表"""
response = client.get(
f"/api/v1/maintenance/{test_maintenance_with_parts.id}/parts",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert len(data) == 2
def test_cost_variance_analysis(self, client, auth_headers, db: Session, test_maintenance_record):
"""测试费用差异分析"""
test_maintenance_record.estimated_cost = Decimal("1000.00")
test_maintenance_record.actual_cost = Decimal("1200.00")
db.commit()
response = client.get(
f"/api/v1/maintenance/{test_maintenance_record.id}/cost-analysis",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "variance" in data
assert "variance_percentage" in data
def test_get_cost_statistics_by_asset(self, client, auth_headers, test_assets_for_maintenance):
"""测试获取资产维修费用统计"""
asset = test_assets_for_maintenance[0]
response = client.get(
f"/api/v1/maintenance/asset/{asset.id}/cost-statistics",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "total_cost" in data
assert "maintenance_count" in data
# ================================
# 维修历史测试 (5+用例)
# ================================
class TestMaintenanceHistory:
"""维修历史测试"""
def test_get_asset_maintenance_history(self, client, auth_headers, test_maintenance_record):
"""测试获取资产维修历史"""
response = client.get(
f"/api/v1/maintenance/asset/{test_maintenance_record.asset_id}/history",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) >= 1
def test_get_maintenance_history_with_date_range(self, client, auth_headers, test_maintenance_record):
"""测试按日期范围获取维修历史"""
start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
end_date = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
response = client.get(
f"/api/v1/maintenance/asset/{test_maintenance_record.asset_id}/history?start_date={start_date}&end_date={end_date}",
headers=auth_headers
)
assert response.status_code == 200
def test_get_maintenance_frequency_analysis(self, client, auth_headers, test_assets_for_maintenance):
"""测试获取维修频率分析"""
asset = test_assets_for_maintenance[0]
response = client.get(
f"/api/v1/maintenance/asset/{asset.id}/frequency-analysis",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "total_maintenance_count" in data
assert "average_days_between_maintenance" in data
def test_export_maintenance_history(self, client, auth_headers, test_maintenance_record):
"""测试导出维修历史"""
response = client.get(
f"/api/v1/maintenance/asset/{test_maintenance_record.asset_id}/export",
headers=auth_headers
)
assert response.status_code == 200
assert "export_url" in response.json()
def test_get_maintenance_summary_report(self, client, auth_headers):
"""测试获取维修汇总报告"""
response = client.get(
"/api/v1/maintenance/summary-report",
headers=auth_headers,
params={"start_date": "2025-01-01", "end_date": "2025-12-31"}
)
assert response.status_code == 200
data = response.json()
assert "total_maintenance_count" in data
assert "total_cost" in data
assert "by_type" in data
# ================================
# 测试标记
# ================================
@pytest.mark.unit
class TestMaintenanceUnit:
"""单元测试标记"""
def test_maintenance_number_generation(self):
"""测试维修单号生成逻辑"""
pass
def test_maintenance_type_validation(self):
"""测试维修类型验证"""
pass
@pytest.mark.integration
class TestMaintenanceIntegration:
"""集成测试标记"""
def test_full_maintenance_workflow(self, client, auth_headers, test_assets_for_maintenance):
"""测试完整维修流程"""
asset = test_assets_for_maintenance[0]
# 1. 创建维修记录
create_response = client.post(
"/api/v1/maintenance/",
json={
"asset_id": asset.id,
"maintenance_type": "corrective",
"priority": "high",
"fault_description": "完整流程测试",
"reported_by": 1,
"estimated_cost": 1000.00
},
headers=auth_headers
)
assert create_response.status_code == 200
maintenance_id = create_response.json()["id"]
# 2. 开始维修
start_response = client.post(
f"/api/v1/maintenance/{maintenance_id}/start",
json={"start_note": "开始"},
headers=auth_headers
)
assert start_response.status_code == 200
# 3. 完成维修
complete_response = client.post(
f"/api/v1/maintenance/{maintenance_id}/complete",
json={"completion_note": "完成", "actual_cost": 1200.00},
headers=auth_headers
)
assert complete_response.status_code == 200
@pytest.mark.smoke
class TestMaintenanceSmoke:
"""冒烟测试标记"""
def test_create_and_start_maintenance(self, client, auth_headers, test_assets_for_maintenance):
"""冒烟测试: 创建并开始维修"""
asset = test_assets_for_maintenance[0]
create_response = client.post(
"/api/v1/maintenance/",
json={
"asset_id": asset.id,
"maintenance_type": "corrective",
"priority": "medium",
"fault_description": "冒烟测试",
"reported_by": 1
},
headers=auth_headers
)
assert create_response.status_code == 200
maintenance_id = create_response.json()["id"]
start_response = client.post(
f"/api/v1/maintenance/{maintenance_id}/start",
json={"start_note": "冒烟测试开始"},
headers=auth_headers
)
assert start_response.status_code == 200

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,912 @@
"""
统计分析 API 测试
测试范围:
- 资产统计测试 (20+用例)
- 分布统计测试 (15+用例)
- 趋势统计测试 (10+用例)
- 缓存测试 (10+用例)
- 导出测试 (5+用例)
总计: 60+ 用例
"""
import pytest
from datetime import datetime, timedelta
from decimal import Decimal
from sqlalchemy.orm import Session
from app.models.asset import Asset
from app.models.organization import Organization
from app.models.maintenance import Maintenance
# ================================
# Fixtures
# ================================
@pytest.fixture
def test_assets_for_statistics(db: Session) -> list:
"""创建用于统计的测试资产"""
assets = []
# 不同状态的资产
statuses = ["in_stock", "in_use", "maintenance", "scrapped"]
for i, status in enumerate(statuses):
for j in range(3):
asset = Asset(
asset_code=f"STAT-{status[:3].upper()}-{j+1:03d}",
asset_name=f"统计测试资产{i}-{j}",
device_type_id=1,
organization_id=1,
status=status,
purchase_price=Decimal(str(10000 * (i + 1))),
purchase_date=datetime.now() - timedelta(days=30 * (i + 1))
)
db.add(asset)
assets.append(asset)
db.commit()
for asset in assets:
db.refresh(asset)
return assets
@pytest.fixture
def test_orgs_for_statistics(db: Session) -> list:
"""创建用于统计的测试组织"""
orgs = []
for i in range(3):
org = Organization(
org_code=f"STAT-ORG-{i+1:03d}",
org_name=f"统计测试组织{i+1}",
org_type="department",
status="active"
)
db.add(org)
orgs.append(org)
db.commit()
for org in orgs:
db.refresh(org)
return orgs
# ================================
# 资产统计测试 (20+用例)
# ================================
class TestAssetStatistics:
"""资产统计测试"""
def test_get_total_asset_count(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产总数"""
response = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "total_count" in data
assert data["total_count"] >= len(test_assets_for_statistics)
def test_get_asset_count_by_status(self, client, auth_headers, test_assets_for_statistics):
"""测试按状态统计资产数量"""
response = client.get(
"/api/v1/statistics/assets/by-status",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) > 0
assert all("status" in item and "count" in item for item in data)
def test_get_asset_count_by_type(self, client, auth_headers, test_assets_for_statistics):
"""测试按类型统计资产数量"""
response = client.get(
"/api/v1/statistics/assets/by-type",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert all("device_type" in item and "count" in item for item in data)
def test_get_asset_count_by_organization(self, client, auth_headers, test_assets_for_statistics):
"""测试按组织统计资产数量"""
response = client.get(
"/api/v1/statistics/assets/by-organization",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_get_total_asset_value(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产总价值"""
response = client.get(
"/api/v1/statistics/assets/total-value",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "total_value" in data
assert isinstance(data["total_value"], (int, float, str))
def test_get_asset_value_by_status(self, client, auth_headers, test_assets_for_statistics):
"""测试按状态统计资产价值"""
response = client.get(
"/api/v1/statistics/assets/value-by-status",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert all("status" in item and "total_value" in item for item in data)
def test_get_asset_value_by_type(self, client, auth_headers, test_assets_for_statistics):
"""测试按类型统计资产价值"""
response = client.get(
"/api/v1/statistics/assets/value-by-type",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_get_asset_purchase_statistics(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产采购统计"""
response = client.get(
"/api/v1/statistics/assets/purchase-statistics",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "total_purchase_count" in data
assert "total_purchase_value" in data
def test_get_asset_purchase_by_month(self, client, auth_headers, test_assets_for_statistics):
"""测试按月统计资产采购"""
response = client.get(
"/api/v1/statistics/assets/purchase-by-month",
params={"year": 2025},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_get_asset_depreciation_summary(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产折旧汇总"""
response = client.get(
"/api/v1/statistics/assets/depreciation-summary",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "total_depreciation" in data
def test_get_asset_age_distribution(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产使用年限分布"""
response = client.get(
"/api/v1/statistics/assets/age-distribution",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert all("age_range" in item and "count" in item for item in data)
def test_get_new_asset_statistics(self, client, auth_headers, test_assets_for_statistics):
"""测试获取新增资产统计"""
response = client.get(
"/api/v1/statistics/assets/new-assets",
params={"days": 30},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "count" in data
assert "total_value" in data
def test_get_scrapped_asset_statistics(self, client, auth_headers, test_assets_for_statistics):
"""测试获取报废资产统计"""
response = client.get(
"/api/v1/statistics/assets/scrapped-assets",
params={"start_date": "2025-01-01", "end_date": "2025-12-31"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "count" in data
def test_get_asset_utilization_rate(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产利用率"""
response = client.get(
"/api/v1/statistics/assets/utilization-rate",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "utilization_rate" in data
assert "in_use_count" in data
assert "total_count" in data
def test_get_asset_maintenance_rate(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产维修率"""
response = client.get(
"/api/v1/statistics/assets/maintenance-rate",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "maintenance_rate" in data
def test_get_asset_summary_dashboard(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产汇总仪表盘数据"""
response = client.get(
"/api/v1/statistics/assets/summary-dashboard",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "total_assets" in data
assert "total_value" in data
assert "utilization_rate" in data
assert "maintenance_rate" in data
def test_search_statistics(self, client, auth_headers, test_assets_for_statistics):
"""测试搜索统计"""
response = client.get(
"/api/v1/statistics/assets/search",
params={"keyword": "统计"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "count" in data
def test_get_asset_top_list_by_value(self, client, auth_headers, test_assets_for_statistics):
"""测试获取价值最高的资产列表"""
response = client.get(
"/api/v1/statistics/assets/top-by-value",
params={"limit": 10},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_get_statistics_by_custom_field(self, client, auth_headers, test_assets_for_statistics):
"""测试按自定义字段统计"""
response = client.get(
"/api/v1/statistics/assets/by-custom-field",
params={"field_name": "manufacturer"},
headers=auth_headers
)
assert response.status_code in [200, 400] # 可能不支持该字段
def test_get_multi_dimension_statistics(self, client, auth_headers, test_assets_for_statistics):
"""测试多维度统计"""
response = client.post(
"/api/v1/statistics/assets/multi-dimension",
json={
"dimensions": ["status", "device_type"],
"metrics": ["count", "total_value"]
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "data" in data
# ================================
# 分布统计测试 (15+用例)
# ================================
class TestDistributionStatistics:
"""分布统计测试"""
def test_get_geographic_distribution(self, client, auth_headers, test_orgs_for_statistics):
"""测试获取地理分布统计"""
response = client.get(
"/api/v1/statistics/distribution/geographic",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_get_organization_hierarchy_distribution(self, client, auth_headers, test_orgs_for_statistics):
"""测试获取组织层级分布"""
response = client.get(
"/api/v1/statistics/distribution/organization-hierarchy",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_get_department_distribution(self, client, auth_headers, test_orgs_for_statistics):
"""测试获取部门分布"""
response = client.get(
"/api/v1/statistics/distribution/by-department",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_get_asset_category_distribution(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产类别分布"""
response = client.get(
"/api/v1/statistics/distribution/by-category",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_get_asset_value_distribution(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产价值分布"""
response = client.get(
"/api/v1/statistics/distribution/value-ranges",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert all("range" in item and "count" in item for item in data)
def test_get_asset_location_distribution(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产位置分布"""
response = client.get(
"/api/v1/statistics/distribution/by-location",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_get_asset_brand_distribution(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产品牌分布"""
response = client.get(
"/api/v1/statistics/distribution/by-brand",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
def test_get_asset_supplier_distribution(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产供应商分布"""
response = client.get(
"/api/v1/statistics/distribution/by-supplier",
headers=auth_headers
)
assert response.status_code == 200
def test_get_asset_status_distribution_pie_chart(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产状态分布饼图数据"""
response = client.get(
"/api/v1/statistics/distribution/status-pie-chart",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "labels" in data
assert "data" in data
assert isinstance(data["labels"], list)
assert isinstance(data["data"], list)
def test_get_organization_asset_tree(self, client, auth_headers, test_orgs_for_statistics):
"""测试获取组织资产树"""
response = client.get(
"/api/v1/statistics/distribution/org-asset-tree",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "tree" in data
def test_get_cross_tabulation(self, client, auth_headers, test_assets_for_statistics):
"""测试交叉统计表"""
response = client.post(
"/api/v1/statistics/distribution/cross-tabulation",
json={
"row_field": "status",
"column_field": "device_type_id"
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "rows" in data
assert "columns" in data
assert "data" in data
def test_get_distribution_heatmap_data(self, client, auth_headers, test_assets_for_statistics):
"""测试获取分布热力图数据"""
response = client.get(
"/api/v1/statistics/distribution/heatmap",
params={"dimension": "organization_asset"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "heatmap_data" in data
def test_get_asset_concentration_index(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产集中度指数"""
response = client.get(
"/api/v1/statistics/distribution/concentration-index",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "index" in data
def test_get_distribution_comparison(self, client, auth_headers, test_assets_for_statistics):
"""测试分布对比分析"""
response = client.post(
"/api/v1/statistics/distribution/comparison",
json={
"dimension": "status",
"period1": {"start": "2025-01-01", "end": "2025-06-30"},
"period2": {"start": "2024-01-01", "end": "2024-06-30"}
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "period1" in data
assert "period2" in data
def test_get_distribution_trend(self, client, auth_headers, test_assets_for_statistics):
"""测试分布趋势"""
response = client.get(
"/api/v1/statistics/distribution/trend",
params={"dimension": "status", "months": 12},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "trend_data" in data
# ================================
# 趋势统计测试 (10+用例)
# ================================
class TestTrendStatistics:
"""趋势统计测试"""
def test_get_asset_growth_trend(self, client, auth_headers, test_assets_for_statistics):
"""测试获取资产增长趋势"""
response = client.get(
"/api/v1/statistics/trends/asset-growth",
params={"period": "monthly", "months": 12},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "trend" in data
assert isinstance(data["trend"], list)
def test_get_value_change_trend(self, client, auth_headers, test_assets_for_statistics):
"""测试获取价值变化趋势"""
response = client.get(
"/api/v1/statistics/trends/value-change",
params={"period": "monthly", "months": 12},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "trend" in data
def test_get_utilization_trend(self, client, auth_headers, test_assets_for_statistics):
"""测试获取利用率趋势"""
response = client.get(
"/api/v1/statistics/trends/utilization",
params={"period": "weekly", "weeks": 12},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "trend" in data
def test_get_maintenance_cost_trend(self, client, auth_headers, test_assets_for_statistics):
"""测试获取维修费用趋势"""
response = client.get(
"/api/v1/statistics/trends/maintenance-cost",
params={"period": "monthly", "months": 12},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "trend" in data
def test_get_allocation_trend(self, client, auth_headers, test_assets_for_statistics):
"""测试获取分配趋势"""
response = client.get(
"/api/v1/statistics/trends/allocation",
params={"period": "monthly", "months": 12},
headers=auth_headers
)
assert response.status_code == 200
def test_get_transfer_trend(self, client, auth_headers, test_assets_for_statistics):
"""测试获取调拨趋势"""
response = client.get(
"/api/v1/statistics/trends/transfer",
params={"period": "monthly", "months": 12},
headers=auth_headers
)
assert response.status_code == 200
def test_get_scrap_rate_trend(self, client, auth_headers, test_assets_for_statistics):
"""测试获取报废率趋势"""
response = client.get(
"/api/v1/statistics/trends/scrap-rate",
params={"period": "monthly", "months": 12},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "trend" in data
def test_get_forecast_data(self, client, auth_headers, test_assets_for_statistics):
"""测试获取预测数据"""
response = client.get(
"/api/v1/statistics/trends/forecast",
params={
"metric": "asset_count",
"method": "linear_regression",
"forecast_periods": 6
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "forecast" in data
assert "confidence_interval" in data
def test_get_year_over_year_comparison(self, client, auth_headers, test_assets_for_statistics):
"""测试获取同比数据"""
response = client.get(
"/api/v1/statistics/trends/year-over-year",
params={"metric": "total_value"},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "current_year" in data
assert "previous_year" in data
assert "growth_rate" in data
def test_get_moving_average(self, client, auth_headers, test_assets_for_statistics):
"""测试获取移动平均"""
response = client.get(
"/api/v1/statistics/trends/moving-average",
params={"metric": "asset_count", "window": 7},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "moving_average" in data
# ================================
# 缓存测试 (10+用例)
# ================================
class TestStatisticsCache:
"""统计缓存测试"""
def test_cache_is_working(self, client, auth_headers, test_assets_for_statistics):
"""测试缓存是否生效"""
# 第一次请求
response1 = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
assert response1.status_code == 200
# 第二次请求应该从缓存读取
response2 = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
assert response2.status_code == 200
def test_cache_key_generation(self, client, auth_headers, test_assets_for_statistics):
"""测试缓存键生成"""
response = client.get(
"/api/v1/statistics/assets/by-status",
headers=auth_headers
)
assert response.status_code == 200
def test_cache_invalidation_on_asset_change(self, client, auth_headers, db: Session, test_assets_for_statistics):
"""测试资产变更时缓存失效"""
# 获取初始统计
response1 = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
count1 = response1.json()["total_count"]
# 创建新资产
new_asset = Asset(
asset_code="CACHE-TEST-001",
asset_name="缓存测试资产",
device_type_id=1,
organization_id=1,
status="in_stock"
)
db.add(new_asset)
db.commit()
# 再次获取统计
response2 = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
count2 = response2.json()["total_count"]
# 验证缓存已更新
assert count2 == count1 + 1
def test_cache_expiration(self, client, auth_headers, test_assets_for_statistics):
"""测试缓存过期"""
response = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
assert response.status_code == 200
def test_clear_cache(self, client, auth_headers, test_assets_for_statistics):
"""测试清除缓存"""
response = client.post(
"/api/v1/statistics/cache/clear",
json={"cache_keys": ["assets:total-count"]},
headers=auth_headers
)
assert response.status_code == 200
def test_cache_statistics(self, client, auth_headers):
"""测试获取缓存统计"""
response = client.get(
"/api/v1/statistics/cache/stats",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "hit_count" in data
assert "miss_count" in data
def test_warm_up_cache(self, client, auth_headers):
"""测试缓存预热"""
response = client.post(
"/api/v1/statistics/cache/warm-up",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "warmed_up_keys" in data
def test_cache_with_different_parameters(self, client, auth_headers, test_assets_for_statistics):
"""测试不同参数使用不同缓存"""
response1 = client.get(
"/api/v1/statistics/assets/purchase-by-month?year=2024",
headers=auth_headers
)
response2 = client.get(
"/api/v1/statistics/assets/purchase-by-month?year=2025",
headers=auth_headers
)
assert response1.status_code == 200
assert response2.status_code == 200
def test_distributed_cache_consistency(self, client, auth_headers, test_assets_for_statistics):
"""测试分布式缓存一致性"""
response = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
assert response.status_code == 200
def test_cache_performance(self, client, auth_headers, test_assets_for_statistics):
"""测试缓存性能"""
import time
# 未缓存请求
start = time.time()
response1 = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
uncached_time = time.time() - start
# 缓存请求
start = time.time()
response2 = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
cached_time = time.time() - start
# 缓存请求应该更快
# 注意: 这个断言可能因为网络延迟等因素不稳定
# assert cached_time < uncached_time
# ================================
# 导出测试 (5+用例)
# ================================
class TestStatisticsExport:
"""统计导出测试"""
def test_export_statistics_to_excel(self, client, auth_headers, test_assets_for_statistics):
"""测试导出统计数据到Excel"""
response = client.post(
"/api/v1/statistics/export/excel",
json={
"report_type": "asset_summary",
"filters": {"status": "in_use"},
"columns": ["asset_code", "asset_name", "purchase_price"]
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "download_url" in data
def test_export_statistics_to_pdf(self, client, auth_headers, test_assets_for_statistics):
"""测试导出统计数据到PDF"""
response = client.post(
"/api/v1/statistics/export/pdf",
json={
"report_type": "asset_distribution",
"include_charts": True
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "download_url" in data
def test_export_statistics_to_csv(self, client, auth_headers, test_assets_for_statistics):
"""测试导出统计数据到CSV"""
response = client.post(
"/api/v1/statistics/export/csv",
json={
"query": "assets_by_status",
"parameters": {}
},
headers=auth_headers
)
assert response.status_code in [200, 202] # 可能异步处理
def test_scheduled_export(self, client, auth_headers):
"""测试定时导出"""
response = client.post(
"/api/v1/statistics/export/schedule",
json={
"report_type": "monthly_report",
"schedule": "0 0 1 * *", # 每月1号
"recipients": ["admin@example.com"],
"format": "excel"
},
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "schedule_id" in data
def test_get_export_history(self, client, auth_headers):
"""测试获取导出历史"""
response = client.get(
"/api/v1/statistics/export/history",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
# ================================
# 测试标记
# ================================
@pytest.mark.unit
class TestStatisticsUnit:
"""单元测试标记"""
def test_calculation_accuracy(self):
"""测试计算准确性"""
pass
def test_rounding_rules(self):
"""测试舍入规则"""
pass
@pytest.mark.integration
class TestStatisticsIntegration:
"""集成测试标记"""
def test_full_statistics_workflow(self, client, auth_headers, test_assets_for_statistics):
"""测试完整统计流程"""
# 1. 获取基础统计
response1 = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
assert response1.status_code == 200
# 2. 获取详细统计
response2 = client.get(
"/api/v1/statistics/assets/by-status",
headers=auth_headers
)
assert response2.status_code == 200
# 3. 导出报告
response3 = client.post(
"/api/v1/statistics/export/excel",
json={"report_type": "asset_summary"},
headers=auth_headers
)
assert response3.status_code == 200
@pytest.mark.slow
class TestStatisticsSlowTests:
"""慢速测试标记"""
def test_large_dataset_statistics(self, client, auth_headers):
"""测试大数据集统计"""
pass
@pytest.mark.smoke
class TestStatisticsSmoke:
"""冒烟测试标记"""
def test_basic_statistics_endpoints(self, client, auth_headers):
"""冒烟测试: 基础统计接口"""
endpoints = [
"/api/v1/statistics/assets/total-count",
"/api/v1/statistics/assets/by-status",
"/api/v1/statistics/assets/total-value"
]
for endpoint in endpoints:
response = client.get(endpoint, headers=auth_headers)
assert response.status_code == 200
@pytest.mark.performance
class TestStatisticsPerformance:
"""性能测试标记"""
def test_query_response_time(self, client, auth_headers):
"""测试查询响应时间"""
import time
start = time.time()
response = client.get(
"/api/v1/statistics/assets/total-count",
headers=auth_headers
)
elapsed = time.time() - start
assert response.status_code == 200
assert elapsed < 1.0 # 响应时间应小于1秒
def test_concurrent_statistics_requests(self, client, auth_headers):
"""测试并发统计请求"""
pass

1010
tests/api/test_transfers.py Normal file

File diff suppressed because it is too large Load Diff