Fix API compatibility and add user/role/permission and asset import/export
This commit is contained in:
1220
backend_new/tests/api/test_allocations.py
Normal file
1220
backend_new/tests/api/test_allocations.py
Normal file
File diff suppressed because it is too large
Load Diff
426
backend_new/tests/api/test_api_integration.py
Normal file
426
backend_new/tests/api/test_api_integration.py
Normal 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
backend_new/tests/api/test_assets.py
Normal file
459
backend_new/tests/api/test_assets.py
Normal 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
backend_new/tests/api/test_auth.py
Normal file
356
backend_new/tests/api/test_auth.py
Normal 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]
|
||||
880
backend_new/tests/api/test_device_types.py
Normal file
880
backend_new/tests/api/test_device_types.py
Normal 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]
|
||||
891
backend_new/tests/api/test_maintenance.py
Normal file
891
backend_new/tests/api/test_maintenance.py
Normal 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
|
||||
1547
backend_new/tests/api/test_organizations.py
Normal file
1547
backend_new/tests/api/test_organizations.py
Normal file
File diff suppressed because it is too large
Load Diff
912
backend_new/tests/api/test_statistics.py
Normal file
912
backend_new/tests/api/test_statistics.py
Normal 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
backend_new/tests/api/test_transfers.py
Normal file
1010
backend_new/tests/api/test_transfers.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user