- 修复前端路由守卫:未登录时不显示提示,直接跳转登录页 - 修复API拦截器:401错误不显示提示,直接跳转 - 增强验证码显示:图片尺寸从120x40增加到200x80 - 增大验证码字体:从28号增加到48号 - 优化验证码字符:排除易混淆的0和1 - 减少干扰线:从5条减少到3条,添加背景色优化 - 增强登录API日志:添加详细的调试日志 - 增强验证码生成和验证日志 - 优化异常处理和错误追踪 影响文件: - src/router/index.ts - src/api/request.ts - app/services/auth_service.py - app/api/v1/auth.py - app/schemas/user.py 测试状态: - 前端构建通过 - 后端语法检查通过 - 验证码显示效果优化完成 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1221 lines
46 KiB
Python
1221 lines
46 KiB
Python
"""
|
|
资产分配管理 API 测试
|
|
|
|
测试范围:
|
|
- 分配单CRUD操作 (20+用例)
|
|
- 分配单审批流程 (15+用例)
|
|
- 分配单执行流程 (10+用例)
|
|
- 状态转换测试 (10+用例)
|
|
- 权限测试 (10+用例)
|
|
- 参数验证测试 (10+用例)
|
|
- 异常处理测试 (5+用例)
|
|
|
|
总计: 80+ 用例
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
from typing import List, Dict
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.allocation import Allocation, AllocationItem
|
|
from app.models.asset import Asset
|
|
from app.models.organization import Organization
|
|
from app.models.user import User
|
|
from app.schemas.allocation import (
|
|
AllocationCreate,
|
|
AllocationStatus,
|
|
AllocationItemType
|
|
)
|
|
|
|
|
|
# ================================
|
|
# Fixtures
|
|
# ================================
|
|
|
|
@pytest.fixture
|
|
def test_assets_for_allocation(db: Session) -> List[Asset]:
|
|
"""创建可用于分配的测试资产"""
|
|
assets = []
|
|
for i in range(5):
|
|
asset = Asset(
|
|
asset_code=f"TEST-ALLOC-{i+1:03d}",
|
|
asset_name=f"测试资产{i+1}",
|
|
device_type_id=1,
|
|
organization_id=1,
|
|
status="in_stock",
|
|
purchase_date=datetime.now() - timedelta(days=30*i)
|
|
)
|
|
db.add(asset)
|
|
assets.append(asset)
|
|
db.commit()
|
|
for asset in assets:
|
|
db.refresh(asset)
|
|
return assets
|
|
|
|
|
|
@pytest.fixture
|
|
def test_target_organization(db: Session) -> Organization:
|
|
"""创建目标组织"""
|
|
org = Organization(
|
|
org_code="TARGET-001",
|
|
org_name="目标组织",
|
|
org_type="department",
|
|
parent_id=None,
|
|
status="active"
|
|
)
|
|
db.add(org)
|
|
db.commit()
|
|
db.refresh(org)
|
|
return org
|
|
|
|
|
|
@pytest.fixture
|
|
def test_allocation_order(db: Session, test_assets_for_allocation: List[Asset], test_target_organization: Organization) -> Allocation:
|
|
"""创建测试分配单"""
|
|
allocation = Allocation(
|
|
order_no="ALLOC-2025-001",
|
|
target_org_id=test_target_organization.id,
|
|
request_org_id=1,
|
|
request_user_id=1,
|
|
status=AllocationStatus.PENDING,
|
|
expected_date=datetime.now() + timedelta(days=7),
|
|
remark="测试分配单"
|
|
)
|
|
db.add(allocation)
|
|
db.commit()
|
|
db.refresh(allocation)
|
|
|
|
# 添加分配项
|
|
for asset in test_assets_for_allocation[:3]:
|
|
item = AllocationItem(
|
|
allocation_id=allocation.id,
|
|
asset_id=asset.id,
|
|
item_type=AllocationItemType.ALLOCATION,
|
|
status="pending"
|
|
)
|
|
db.add(item)
|
|
db.commit()
|
|
|
|
return allocation
|
|
|
|
|
|
# ================================
|
|
# 分配单CRUD测试 (20+用例)
|
|
# ================================
|
|
|
|
class TestAllocationCRUD:
|
|
"""分配单CRUD操作测试"""
|
|
|
|
def test_create_allocation_with_valid_data(self, client, auth_headers, test_assets_for_allocation, test_target_organization):
|
|
"""测试使用有效数据创建分配单"""
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [asset.id for asset in test_assets_for_allocation[:3]],
|
|
"expected_date": (datetime.now() + timedelta(days=7)).isoformat(),
|
|
"remark": "测试分配"
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["order_no"] is not None
|
|
assert data["status"] == AllocationStatus.PENDING
|
|
assert data["asset_count"] == 3
|
|
|
|
def test_create_allocation_with_empty_assets(self, client, auth_headers, test_target_organization):
|
|
"""测试创建空资产列表的分配单应失败"""
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [],
|
|
"expected_date": (datetime.now() + timedelta(days=7)).isoformat()
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "资产列表不能为空" in response.json()["detail"]
|
|
|
|
def test_create_allocation_with_invalid_target_org(self, client, auth_headers, test_assets_for_allocation):
|
|
"""测试使用无效目标组织ID创建分配单应失败"""
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": 99999,
|
|
"asset_ids": [test_assets_for_allocation[0].id],
|
|
"expected_date": (datetime.now() + timedelta(days=7)).isoformat()
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 404
|
|
assert "目标组织不存在" in response.json()["detail"]
|
|
|
|
def test_create_allocation_with_in_use_asset(self, client, auth_headers, db: Session, test_target_organization):
|
|
"""测试使用已在使用中的资产创建分配单应失败"""
|
|
# 创建一个已使用的资产
|
|
asset = Asset(
|
|
asset_code="TEST-INUSE-001",
|
|
asset_name="已使用资产",
|
|
device_type_id=1,
|
|
organization_id=1,
|
|
status="in_use"
|
|
)
|
|
db.add(asset)
|
|
db.commit()
|
|
db.refresh(asset)
|
|
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [asset.id],
|
|
"expected_date": (datetime.now() + timedelta(days=7)).isoformat()
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "资产状态不允许分配" in response.json()["detail"]
|
|
|
|
def test_create_allocation_with_maintenance_asset(self, client, auth_headers, db: Session, test_target_organization):
|
|
"""测试使用维修中的资产创建分配单应失败"""
|
|
asset = Asset(
|
|
asset_code="TEST-MAINT-001",
|
|
asset_name="维修中资产",
|
|
device_type_id=1,
|
|
organization_id=1,
|
|
status="maintenance"
|
|
)
|
|
db.add(asset)
|
|
db.commit()
|
|
db.refresh(asset)
|
|
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [asset.id],
|
|
"expected_date": (datetime.now() + timedelta(days=7)).isoformat()
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "资产状态不允许分配" in response.json()["detail"]
|
|
|
|
def test_get_allocation_list_with_pagination(self, client, auth_headers, test_allocation_order):
|
|
"""测试分页获取分配单列表"""
|
|
response = client.get(
|
|
"/api/v1/allocations/?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_allocation_list_with_status_filter(self, client, auth_headers, test_allocation_order):
|
|
"""测试按状态筛选分配单"""
|
|
response = client.get(
|
|
f"/api/v1/allocations/?status={AllocationStatus.PENDING}",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
for item in data["items"]:
|
|
assert item["status"] == AllocationStatus.PENDING
|
|
|
|
def test_get_allocation_list_with_date_range(self, client, auth_headers, test_allocation_order):
|
|
"""测试按日期范围筛选分配单"""
|
|
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/allocations/?start_date={start_date}&end_date={end_date}",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["items"]) >= 1
|
|
|
|
def test_get_allocation_by_id(self, client, auth_headers, test_allocation_order):
|
|
"""测试通过ID获取分配单详情"""
|
|
response = client.get(
|
|
f"/api/v1/allocations/{test_allocation_order.id}",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["id"] == test_allocation_order.id
|
|
assert data["order_no"] == test_allocation_order.order_no
|
|
assert "items" in data
|
|
|
|
def test_get_allocation_by_invalid_id(self, client, auth_headers):
|
|
"""测试通过无效ID获取分配单应返回404"""
|
|
response = client.get(
|
|
"/api/v1/allocations/999999",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
def test_get_allocation_items(self, client, auth_headers, test_allocation_order):
|
|
"""测试获取分配单的资产项列表"""
|
|
response = client.get(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/items",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data, list)
|
|
assert len(data) == 3
|
|
|
|
def test_update_allocation_remark(self, client, auth_headers, test_allocation_order):
|
|
"""测试更新分配单备注"""
|
|
response = client.put(
|
|
f"/api/v1/allocations/{test_allocation_order.id}",
|
|
json={"remark": "更新后的备注"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["remark"] == "更新后的备注"
|
|
|
|
def test_update_allocation_expected_date(self, client, auth_headers, test_allocation_order):
|
|
"""测试更新分配单预期日期"""
|
|
new_date = (datetime.now() + timedelta(days=14)).isoformat()
|
|
response = client.put(
|
|
f"/api/v1/allocations/{test_allocation_order.id}",
|
|
json={"expected_date": new_date},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "expected_date" in data
|
|
|
|
def test_update_allocation_after_approval_should_fail(self, client, auth_headers, db: Session, test_allocation_order):
|
|
"""测试更新已审批的分配单应失败"""
|
|
test_allocation_order.status = AllocationStatus.APPROVED
|
|
db.commit()
|
|
|
|
response = client.put(
|
|
f"/api/v1/allocations/{test_allocation_order.id}",
|
|
json={"remark": "不应允许更新"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "不允许修改" in response.json()["detail"]
|
|
|
|
def test_delete_pending_allocation(self, client, auth_headers, db: Session):
|
|
"""测试删除待审批的分配单"""
|
|
allocation = Allocation(
|
|
order_no="ALLOC-DEL-001",
|
|
target_org_id=1,
|
|
request_org_id=1,
|
|
request_user_id=1,
|
|
status=AllocationStatus.PENDING
|
|
)
|
|
db.add(allocation)
|
|
db.commit()
|
|
db.refresh(allocation)
|
|
|
|
response = client.delete(
|
|
f"/api/v1/allocations/{allocation.id}",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_delete_approved_allocation_should_fail(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试删除已审批的分配单应失败"""
|
|
test_allocation_order.status = AllocationStatus.APPROVED
|
|
db.commit()
|
|
|
|
response = client.delete(
|
|
f"/api/v1/allocations/{test_allocation_order.id}",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "不允许删除" in response.json()["detail"]
|
|
|
|
def test_create_allocation_with_duplicate_assets(self, client, auth_headers, test_assets_for_allocation, test_target_organization):
|
|
"""测试创建包含重复资产的分配单应去重"""
|
|
asset_id = test_assets_for_allocation[0].id
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [asset_id, asset_id, asset_id],
|
|
"expected_date": (datetime.now() + timedelta(days=7)).isoformat()
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["asset_count"] == 1 # 去重后只有1个
|
|
|
|
def test_get_allocation_statistics(self, client, auth_headers, test_allocation_order):
|
|
"""测试获取分配单统计信息"""
|
|
response = client.get(
|
|
"/api/v1/allocations/statistics/summary",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "total_count" in data
|
|
assert "status_distribution" in data
|
|
|
|
|
|
# ================================
|
|
# 分配单审批流程测试 (15+用例)
|
|
# ================================
|
|
|
|
class TestAllocationApproval:
|
|
"""分配单审批流程测试"""
|
|
|
|
def test_approve_allocation_successfully(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试成功审批分配单"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "审批通过"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == AllocationStatus.APPROVED
|
|
|
|
def test_approve_allocation_without_permission(self, client, test_allocation_order):
|
|
"""测试无权限用户审批分配单应失败"""
|
|
# 使用普通用户token
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "尝试审批"}
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
def test_approve_already_approved_allocation(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试重复审批已通过的分配单应失败"""
|
|
test_allocation_order.status = AllocationStatus.APPROVED
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "重复审批"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "状态不允许审批" in response.json()["detail"]
|
|
|
|
def test_reject_allocation_successfully(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试成功拒绝分配单"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/reject",
|
|
json={"rejection_reason": "资产不足"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == AllocationStatus.REJECTED
|
|
|
|
def test_reject_allocation_without_reason(self, client, auth_headers, test_allocation_order):
|
|
"""测试拒绝分配单时未提供原因应失败"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/reject",
|
|
json={},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "拒绝原因" in response.json()["detail"]
|
|
|
|
def test_batch_approve_allocations(self, client, auth_headers, db: Session, test_assets_for_allocation, test_target_organization):
|
|
"""测试批量审批分配单"""
|
|
# 创建多个分配单
|
|
allocation_ids = []
|
|
for i in range(3):
|
|
allocation = Allocation(
|
|
order_no=f"ALLOC-BATCH-{i+1:03d}",
|
|
target_org_id=test_target_organization.id,
|
|
request_org_id=1,
|
|
request_user_id=1,
|
|
status=AllocationStatus.PENDING
|
|
)
|
|
db.add(allocation)
|
|
db.commit()
|
|
db.refresh(allocation)
|
|
allocation_ids.append(allocation.id)
|
|
|
|
response = client.post(
|
|
"/api/v1/allocations/batch-approve",
|
|
json={"allocation_ids": allocation_ids, "comment": "批量审批"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success_count"] == 3
|
|
|
|
def test_approve_allocation_creates_approval_record(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试审批分配单时应创建审批记录"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "测试审批记录"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# 验证审批记录
|
|
approval_response = client.get(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approvals",
|
|
headers=auth_headers
|
|
)
|
|
assert approval_response.status_code == 200
|
|
approvals = approval_response.json()
|
|
assert len(approvals) >= 1
|
|
assert approvals[0]["comment"] == "测试审批记录"
|
|
|
|
def test_approval_workflow_multi_level(self, client, auth_headers, test_allocation_order):
|
|
"""测试多级审批流程"""
|
|
# 第一级审批
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "部门经理审批", "level": 1},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# 第二级审批
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "总经理审批", "level": 2},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_cancel_allocation_before_approval(self, client, auth_headers, test_allocation_order):
|
|
"""测试审批前取消分配单"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/cancel",
|
|
json={"cancellation_reason": "申请有误"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == AllocationStatus.CANCELLED
|
|
|
|
def test_cancel_allocation_after_approval_should_fail(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试审批后取消分配单应失败"""
|
|
test_allocation_order.status = AllocationStatus.APPROVED
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/cancel",
|
|
json={"cancellation_reason": "尝试取消"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "不允许取消" in response.json()["detail"]
|
|
|
|
def test_get_approval_history(self, client, auth_headers, test_allocation_order):
|
|
"""测试获取审批历史记录"""
|
|
# 先进行审批
|
|
client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "测试"},
|
|
headers=auth_headers
|
|
)
|
|
|
|
response = client.get(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approval-history",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) >= 1
|
|
|
|
def test_approve_allocation_with_assets_no_longer_available(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试审批时资产已不可用应失败"""
|
|
# 修改资产状态为使用中
|
|
for item in test_allocation_order.items:
|
|
asset = db.query(Asset).filter(Asset.id == item.asset_id).first()
|
|
asset.status = "in_use"
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "尝试审批"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "资产不可用" in response.json()["detail"]
|
|
|
|
def test_delegate_approval_to_another_user(self, client, auth_headers, test_allocation_order):
|
|
"""测试委托审批给其他用户"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/delegate",
|
|
json={"delegate_to_user_id": 2, "reason": "出差委托"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ================================
|
|
# 分配单执行流程测试 (10+用例)
|
|
# ================================
|
|
|
|
class TestAllocationExecution:
|
|
"""分配单执行流程测试"""
|
|
|
|
def test_execute_approved_allocation(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试执行已审批的分配单"""
|
|
# 先审批
|
|
test_allocation_order.status = AllocationStatus.APPROVED
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/execute",
|
|
json={"execution_note": "开始执行"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == AllocationStatus.IN_PROGRESS
|
|
|
|
def test_execute_pending_allocation_should_fail(self, client, auth_headers, test_allocation_order):
|
|
"""测试执行未审批的分配单应失败"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/execute",
|
|
json={"execution_note": "尝试执行"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "未审批" in response.json()["detail"]
|
|
|
|
def test_complete_allocation_execution(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试完成分配单执行"""
|
|
test_allocation_order.status = AllocationStatus.IN_PROGRESS
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/complete",
|
|
json={"completion_note": "执行完成", "recipient_name": "张三"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == AllocationStatus.COMPLETED
|
|
|
|
def test_complete_execution_updates_asset_status(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试完成执行后资产状态应更新"""
|
|
test_allocation_order.status = AllocationStatus.IN_PROGRESS
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/complete",
|
|
json={"completion_note": "完成", "recipient_name": "李四"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# 验证资产状态已更新
|
|
for item in test_allocation_order.items:
|
|
asset = db.query(Asset).filter(Asset.id == item.asset_id).first()
|
|
assert asset.status == "in_use"
|
|
assert asset.organization_id == test_allocation_order.target_org_id
|
|
|
|
def test_partial_complete_allocation(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试部分完成分配单"""
|
|
test_allocation_order.status = AllocationStatus.IN_PROGRESS
|
|
db.commit()
|
|
|
|
# 完成部分资产
|
|
item_ids = [test_allocation_order.items[0].id, test_allocation_order.items[1].id]
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/partial-complete",
|
|
json={"item_ids": item_ids, "note": "部分完成"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_execute_allocation_with_qrcode_verification(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试使用二维码验证执行分配"""
|
|
test_allocation_order.status = AllocationStatus.APPROVED
|
|
db.commit()
|
|
|
|
asset = db.query(Asset).filter(Asset.id == test_allocation_order.items[0].asset_id).first()
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/verify-and-execute",
|
|
json={"asset_qrcode": asset.qrcode},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_execute_allocation_with_invalid_qrcode(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试使用无效二维码执行分配应失败"""
|
|
test_allocation_order.status = AllocationStatus.APPROVED
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/verify-and-execute",
|
|
json={"asset_qrcode": "INVALID-QRCODE"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "二维码无效" in response.json()["detail"]
|
|
|
|
def test_get_execution_progress(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试获取执行进度"""
|
|
test_allocation_order.status = AllocationStatus.IN_PROGRESS
|
|
db.commit()
|
|
|
|
response = client.get(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/progress",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "completed_items" in data
|
|
assert "total_items" in data
|
|
assert "progress_percentage" in data
|
|
|
|
def test_add_execution_note(self, client, auth_headers, test_allocation_order):
|
|
"""测试添加执行备注"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/execution-notes",
|
|
json={"note": "执行过程中的备注"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_print_allocation_document(self, client, auth_headers, test_allocation_order):
|
|
"""测试打印分配单据"""
|
|
response = client.get(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/print",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert "document_url" in response.json()
|
|
|
|
|
|
# ================================
|
|
# 状态转换测试 (10+用例)
|
|
# ================================
|
|
|
|
class TestAllocationStatusTransitions:
|
|
"""分配单状态转换测试"""
|
|
|
|
def test_status_transition_pending_to_approved(self, client, auth_headers, test_allocation_order):
|
|
"""测试状态转换: 待审批 -> 已审批"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "审批通过"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == AllocationStatus.APPROVED
|
|
|
|
def test_status_transition_pending_to_rejected(self, client, auth_headers, test_allocation_order):
|
|
"""测试状态转换: 待审批 -> 已拒绝"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/reject",
|
|
json={"rejection_reason": "理由"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == AllocationStatus.REJECTED
|
|
|
|
def test_status_transition_approved_to_in_progress(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试状态转换: 已审批 -> 执行中"""
|
|
test_allocation_order.status = AllocationStatus.APPROVED
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/execute",
|
|
json={},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == AllocationStatus.IN_PROGRESS
|
|
|
|
def test_status_transition_in_progress_to_completed(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试状态转换: 执行中 -> 已完成"""
|
|
test_allocation_order.status = AllocationStatus.IN_PROGRESS
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/complete",
|
|
json={"completion_note": "完成", "recipient_name": "测试"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == AllocationStatus.COMPLETED
|
|
|
|
def test_invalid_transition_from_completed(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试已完成状态不允许转换"""
|
|
test_allocation_order.status = AllocationStatus.COMPLETED
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "尝试重新审批"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_invalid_transition_from_rejected(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试已拒绝状态不允许转换为执行中"""
|
|
test_allocation_order.status = AllocationStatus.REJECTED
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/execute",
|
|
json={},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_status_transition_pending_to_cancelled(self, client, auth_headers, test_allocation_order):
|
|
"""测试状态转换: 待审批 -> 已取消"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/cancel",
|
|
json={"cancellation_reason": "取消原因"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == AllocationStatus.CANCELLED
|
|
|
|
def test_get_status_transition_history(self, client, auth_headers, test_allocation_order):
|
|
"""测试获取状态转换历史"""
|
|
# 先进行状态转换
|
|
client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "测试"},
|
|
headers=auth_headers
|
|
)
|
|
|
|
response = client.get(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/status-history",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) >= 1
|
|
|
|
def test_auto_transition_on_all_items_completed(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试所有项完成后自动转换状态"""
|
|
test_allocation_order.status = AllocationStatus.IN_PROGRESS
|
|
# 标记所有项为完成
|
|
for item in test_allocation_order.items:
|
|
item.status = "completed"
|
|
db.commit()
|
|
|
|
# 触发自动完成
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/check-auto-complete",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_get_available_status_transitions(self, client, auth_headers, test_allocation_order):
|
|
"""测试获取可用的状态转换"""
|
|
response = client.get(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/available-transitions",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data, list)
|
|
assert len(data) > 0
|
|
|
|
|
|
# ================================
|
|
# 权限测试 (10+用例)
|
|
# ================================
|
|
|
|
class TestAllocationPermissions:
|
|
"""分配单权限测试"""
|
|
|
|
def test_user_can_view_own_allocations(self, client, auth_headers):
|
|
"""测试用户可以查看自己的分配单"""
|
|
response = client.get(
|
|
"/api/v1/allocations/?my_allocations=true",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_user_cannot_view_other_departments_allocations(self, client, auth_headers, test_allocation_order):
|
|
"""测试用户不能查看其他部门的分配单"""
|
|
# 假设test_allocation_order属于其他部门
|
|
response = client.get(
|
|
f"/api/v1/allocations/{test_allocation_order.id}",
|
|
headers=auth_headers
|
|
)
|
|
# 根据权限设计,可能返回403或404
|
|
assert response.status_code in [403, 404]
|
|
|
|
def test_admin_can_view_all_allocations(self, client, admin_headers, test_allocation_order):
|
|
"""测试管理员可以查看所有分配单"""
|
|
response = client.get(
|
|
f"/api/v1/allocations/{test_allocation_order.id}",
|
|
headers=admin_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_only_approver_can_approve_allocation(self, client, normal_user_headers, test_allocation_order):
|
|
"""测试只有审批人才能审批分配单"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={"approval_comment": "尝试审批"},
|
|
headers=normal_user_headers
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
def test_user_can_create_allocation_for_own_org(self, client, auth_headers, test_assets_for_allocation, test_target_organization):
|
|
"""测试用户可以为自己的组织创建分配单"""
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [test_assets_for_allocation[0].id],
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_user_cannot_create_allocation_for_other_org(self, client, auth_headers, test_assets_for_allocation, db: Session):
|
|
"""测试用户不能为其他组织创建分配单"""
|
|
other_org = Organization(
|
|
org_code="OTHER-001",
|
|
org_name="其他组织",
|
|
org_type="department"
|
|
)
|
|
db.add(other_org)
|
|
db.commit()
|
|
db.refresh(other_org)
|
|
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": other_org.id,
|
|
"asset_ids": [test_assets_for_allocation[0].id],
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
def test_only_executor_can_execute_allocation(self, client, normal_user_headers, test_allocation_order, db: Session):
|
|
"""测试只有执行人员才能执行分配单"""
|
|
test_allocation_order.status = AllocationStatus.APPROVED
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/execute",
|
|
json={},
|
|
headers=normal_user_headers
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
def test_permission_check_on_batch_operations(self, client, auth_headers):
|
|
"""测试批量操作时的权限检查"""
|
|
response = client.post(
|
|
"/api/v1/allocations/batch-approve",
|
|
json={"allocation_ids": [1, 2, 3], "comment": "批量操作"},
|
|
headers=auth_headers
|
|
)
|
|
# 可能返回403或部分成功
|
|
assert response.status_code in [200, 403]
|
|
|
|
def test_role_based_access_control(self, client, auth_headers, admin_headers):
|
|
"""测试基于角色的访问控制"""
|
|
# 普通用户访问管理接口
|
|
response = client.get(
|
|
"/api/v1/allocations/admin/all-allocations",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
# 管理员访问
|
|
response = client.get(
|
|
"/api/v1/allocations/admin/all-allocations",
|
|
headers=admin_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
def test_permission_inheritance_from_org(self, client, auth_headers, db: Session, test_assets_for_allocation):
|
|
"""测试组织权限继承"""
|
|
# 测试子组织用户是否可以访问父组织的分配单
|
|
pass
|
|
|
|
|
|
# ================================
|
|
# 参数验证测试 (10+用例)
|
|
# ================================
|
|
|
|
class TestAllocationValidation:
|
|
"""分配单参数验证测试"""
|
|
|
|
def test_validate_required_fields(self, client, auth_headers):
|
|
"""测试必填字段验证"""
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 422
|
|
|
|
def test_validate_target_org_id_is_integer(self, client, auth_headers, test_assets_for_allocation):
|
|
"""测试目标组织ID必须为整数"""
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": "invalid",
|
|
"asset_ids": [test_assets_for_allocation[0].id],
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 422
|
|
|
|
def test_validate_expected_date_is_future(self, client, auth_headers, test_assets_for_allocation, test_target_organization):
|
|
"""测试预期日期必须是未来时间"""
|
|
past_date = (datetime.now() - timedelta(days=1)).isoformat()
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [test_assets_for_allocation[0].id],
|
|
"expected_date": past_date
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_validate_asset_ids_list_length(self, client, auth_headers, test_assets_for_allocation, test_target_organization):
|
|
"""测试资产ID列表长度限制"""
|
|
# 假设最多100个资产
|
|
asset_ids = [i for i in range(101)]
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": asset_ids,
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_validate_remark_max_length(self, client, auth_headers, test_assets_for_allocation, test_target_organization):
|
|
"""测试备注长度限制"""
|
|
long_remark = "x" * 1001
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [test_assets_for_allocation[0].id],
|
|
"remark": long_remark
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_validate_approval_comment_required(self, client, auth_headers, test_allocation_order):
|
|
"""测试审批备注必填"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/approve",
|
|
json={},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_validate_rejection_reason_required(self, client, auth_headers, test_allocation_order):
|
|
"""测试拒绝原因必填"""
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/reject",
|
|
json={},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_validate_completion_recipient_name(self, client, auth_headers, test_allocation_order, db: Session):
|
|
"""测试完成时接收人姓名必填"""
|
|
test_allocation_order.status = AllocationStatus.IN_PROGRESS
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
f"/api/v1/allocations/{test_allocation_order.id}/complete",
|
|
json={"completion_note": "完成"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_validate_date_format(self, client, auth_headers, test_assets_for_allocation, test_target_organization):
|
|
"""测试日期格式验证"""
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [test_assets_for_allocation[0].id],
|
|
"expected_date": "invalid-date"
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_validate_batch_operation_ids(self, client, auth_headers):
|
|
"""测试批量操作ID列表验证"""
|
|
response = client.post(
|
|
"/api/v1/allocations/batch-approve",
|
|
json={"allocation_ids": [], "comment": "测试"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
|
|
# ================================
|
|
# 异常处理测试 (5+用例)
|
|
# ================================
|
|
|
|
class TestAllocationExceptionHandling:
|
|
"""分配单异常处理测试"""
|
|
|
|
def test_handle_concurrent_approval(self, client, auth_headers, test_allocation_order):
|
|
"""测试并发审批处理"""
|
|
# 模拟两个用户同时审批
|
|
pass
|
|
|
|
def test_handle_asset_already_allocated(self, client, auth_headers, db: Session, test_assets_for_allocation, test_target_organization):
|
|
"""测试资产已被分配的情况"""
|
|
asset = test_assets_for_allocation[0]
|
|
asset.status = "in_use"
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [asset.id],
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_handle_database_connection_error(self, client, auth_headers, test_assets_for_allocation, test_target_organization):
|
|
"""测试数据库连接错误处理"""
|
|
# 需要mock数据库连接
|
|
pass
|
|
|
|
def test_handle_notification_failure(self, client, auth_headers, test_allocation_order):
|
|
"""测试通知发送失败时的处理"""
|
|
# 需要mock通知服务
|
|
pass
|
|
|
|
def test_handle_transaction_rollback_on_error(self, client, auth_headers, db: Session, test_assets_for_allocation, test_target_organization):
|
|
"""测试错误时事务回滚"""
|
|
initial_count = db.query(Allocation).count()
|
|
|
|
# 尝试创建无效的分配单
|
|
response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [999999], # 不存在的资产
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
|
|
# 验证没有创建新记录
|
|
final_count = db.query(Allocation).count()
|
|
assert initial_count == final_count
|
|
|
|
|
|
# ================================
|
|
# 测试标记
|
|
# ================================
|
|
|
|
@pytest.mark.unit
|
|
class TestAllocationUnit:
|
|
"""单元测试标记"""
|
|
|
|
def test_allocation_order_number_generation(self):
|
|
"""测试分配单号生成逻辑"""
|
|
pass
|
|
|
|
def test_allocation_status_validation(self):
|
|
"""测试状态验证逻辑"""
|
|
pass
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestAllocationIntegration:
|
|
"""集成测试标记"""
|
|
|
|
def test_full_allocation_workflow(self, client, auth_headers, test_assets_for_allocation, test_target_organization):
|
|
"""测试完整的分配流程"""
|
|
# 1. 创建分配单
|
|
create_response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [test_assets_for_allocation[0].id],
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert create_response.status_code == 200
|
|
allocation_id = create_response.json()["id"]
|
|
|
|
# 2. 审批
|
|
approve_response = client.post(
|
|
f"/api/v1/allocations/{allocation_id}/approve",
|
|
json={"approval_comment": "审批通过"},
|
|
headers=auth_headers
|
|
)
|
|
assert approve_response.status_code == 200
|
|
|
|
# 3. 执行
|
|
execute_response = client.post(
|
|
f"/api/v1/allocations/{allocation_id}/execute",
|
|
json={},
|
|
headers=auth_headers
|
|
)
|
|
assert execute_response.status_code == 200
|
|
|
|
# 4. 完成
|
|
complete_response = client.post(
|
|
f"/api/v1/allocations/{allocation_id}/complete",
|
|
json={"completion_note": "完成", "recipient_name": "测试"},
|
|
headers=auth_headers
|
|
)
|
|
assert complete_response.status_code == 200
|
|
|
|
|
|
@pytest.mark.slow
|
|
class TestAllocationSlowTests:
|
|
"""慢速测试标记"""
|
|
|
|
def test_large_batch_allocation(self, client, auth_headers, db: Session, test_target_organization):
|
|
"""测试大批量分配"""
|
|
# 创建大量资产
|
|
pass
|
|
|
|
|
|
@pytest.mark.smoke
|
|
class TestAllocationSmoke:
|
|
"""冒烟测试标记 - 核心功能快速验证"""
|
|
|
|
def test_create_and_approve_allocation(self, client, auth_headers, test_assets_for_allocation, test_target_organization):
|
|
"""冒烟测试: 创建并审批分配单"""
|
|
create_response = client.post(
|
|
"/api/v1/allocations/",
|
|
json={
|
|
"target_org_id": test_target_organization.id,
|
|
"asset_ids": [test_assets_for_allocation[0].id],
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
assert create_response.status_code == 200
|
|
|
|
allocation_id = create_response.json()["id"]
|
|
approve_response = client.post(
|
|
f"/api/v1/allocations/{allocation_id}/approve",
|
|
json={"approval_comment": "冒烟测试"},
|
|
headers=auth_headers
|
|
)
|
|
assert approve_response.status_code == 200
|