Fix API compatibility and add user/role/permission and asset import/export
This commit is contained in:
474
backend_new/tests/services/test_asset_service.py
Normal file
474
backend_new/tests/services/test_asset_service.py
Normal file
@@ -0,0 +1,474 @@
|
||||
"""
|
||||
资产管理服务层测试
|
||||
|
||||
测试内容:
|
||||
- 资产创建业务逻辑
|
||||
- 资产分配业务逻辑
|
||||
- 状态机转换
|
||||
- 权限验证
|
||||
- 异常处理
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
# from app.services.asset_service import AssetService
|
||||
# from app.services.state_machine_service import StateMachineService
|
||||
# from app.core.exceptions import BusinessException, NotFoundException
|
||||
|
||||
|
||||
# class TestAssetService:
|
||||
# """测试资产管理服务"""
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def asset_service(self):
|
||||
# """创建AssetService实例"""
|
||||
# return AssetService()
|
||||
#
|
||||
# def test_create_asset_generates_code(self, db, asset_service):
|
||||
# """测试创建资产时自动生成编码"""
|
||||
# asset_data = {
|
||||
# "asset_name": "测试资产",
|
||||
# "device_type_id": 1,
|
||||
# "organization_id": 1
|
||||
# }
|
||||
#
|
||||
# asset = asset_service.create_asset(
|
||||
# db=db,
|
||||
# asset_in=AssetCreate(**asset_data),
|
||||
# creator_id=1
|
||||
# )
|
||||
#
|
||||
# assert asset.asset_code is not None
|
||||
# assert asset.asset_code.startswith("ASSET-")
|
||||
# assert len(asset.asset_code) == 19 # ASSET-YYYYMMDD-XXXX
|
||||
#
|
||||
# def test_create_asset_records_status_history(self, db, asset_service):
|
||||
# """测试创建资产时记录状态历史"""
|
||||
# asset_data = AssetCreate(
|
||||
# asset_name="测试资产",
|
||||
# device_type_id=1,
|
||||
# organization_id=1
|
||||
# )
|
||||
#
|
||||
# asset = asset_service.create_asset(
|
||||
# db=db,
|
||||
# asset_in=asset_data,
|
||||
# creator_id=1
|
||||
# )
|
||||
#
|
||||
# # 验证状态历史
|
||||
# history = db.query(AssetStatusHistory).filter(
|
||||
# AssetStatusHistory.asset_id == asset.id
|
||||
# ).all()
|
||||
#
|
||||
# assert len(history) == 1
|
||||
# assert history[0].old_status is None
|
||||
# assert history[0].new_status == "pending"
|
||||
# assert history[0].operation_type == "create"
|
||||
#
|
||||
# def test_create_asset_with_invalid_device_type(self, db, asset_service):
|
||||
# """测试使用无效设备类型创建资产"""
|
||||
# asset_data = AssetCreate(
|
||||
# asset_name="测试资产",
|
||||
# device_type_id=999999, # 不存在的设备类型
|
||||
# organization_id=1
|
||||
# )
|
||||
#
|
||||
# with pytest.raises(NotFoundException):
|
||||
# asset_service.create_asset(
|
||||
# db=db,
|
||||
# asset_in=asset_data,
|
||||
# creator_id=1
|
||||
# )
|
||||
#
|
||||
# def test_create_asset_with_invalid_organization(self, db, asset_service):
|
||||
# """测试使用无效网点创建资产"""
|
||||
# asset_data = AssetCreate(
|
||||
# asset_name="测试资产",
|
||||
# device_type_id=1,
|
||||
# organization_id=999999 # 不存在的网点
|
||||
# )
|
||||
#
|
||||
# with pytest.raises(NotFoundException):
|
||||
# asset_service.create_asset(
|
||||
# db=db,
|
||||
# asset_in=asset_data,
|
||||
# creator_id=1
|
||||
# )
|
||||
#
|
||||
# def test_create_asset_validates_required_dynamic_fields(self, db, asset_service):
|
||||
# """测试验证必填的动态字段"""
|
||||
# # 假设计算机类型要求CPU和内存必填
|
||||
# asset_data = AssetCreate(
|
||||
# asset_name="测试计算机",
|
||||
# device_type_id=1, # 计算机类型
|
||||
# organization_id=1,
|
||||
# dynamic_attributes={
|
||||
# # 缺少必填的cpu和memory字段
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# with pytest.raises(BusinessException):
|
||||
# asset_service.create_asset(
|
||||
# db=db,
|
||||
# asset_in=asset_data,
|
||||
# creator_id=1
|
||||
# )
|
||||
|
||||
|
||||
# class TestAssetAllocation:
|
||||
# """测试资产分配"""
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def asset_service(self):
|
||||
# return AssetService()
|
||||
#
|
||||
# def test_allocate_assets_success(self, db, asset_service, test_asset):
|
||||
# """测试成功分配资产"""
|
||||
# allocation_order = asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[test_asset.id],
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# assert allocation_order is not None
|
||||
# assert allocation_order.order_type == "allocation"
|
||||
# assert allocation_order.asset_count == 1
|
||||
#
|
||||
# # 验证资产状态未改变(等待审批)
|
||||
# db.refresh(test_asset)
|
||||
# assert test_asset.status == "in_stock"
|
||||
#
|
||||
# def test_allocate_assets_invalid_status(self, db, asset_service):
|
||||
# """测试分配状态不正确的资产"""
|
||||
# # 创建一个使用中的资产
|
||||
# in_use_asset = Asset(
|
||||
# asset_code="ASSET-20250124-0002",
|
||||
# asset_name="使用中的资产",
|
||||
# device_type_id=1,
|
||||
# organization_id=1,
|
||||
# status="in_use"
|
||||
# )
|
||||
# db.add(in_use_asset)
|
||||
# db.commit()
|
||||
#
|
||||
# with pytest.raises(BusinessException) as exc:
|
||||
# asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[in_use_asset.id],
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# assert "当前状态不允许分配" in str(exc.value)
|
||||
#
|
||||
# def test_allocate_assets_batch(self, db, asset_service):
|
||||
# """测试批量分配资产"""
|
||||
# # 创建多个资产
|
||||
# assets = []
|
||||
# for i in range(5):
|
||||
# asset = Asset(
|
||||
# asset_code=f"ASSET-20250124-{i:04d}",
|
||||
# asset_name=f"测试资产{i}",
|
||||
# device_type_id=1,
|
||||
# organization_id=1,
|
||||
# status="in_stock"
|
||||
# )
|
||||
# db.add(asset)
|
||||
# assets.append(asset)
|
||||
# db.commit()
|
||||
#
|
||||
# asset_ids = [a.id for a in assets]
|
||||
#
|
||||
# allocation_order = asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=asset_ids,
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# assert allocation_order.asset_count == 5
|
||||
#
|
||||
# def test_allocate_assets_to_same_organization(self, db, asset_service, test_asset):
|
||||
# """测试分配到当前所在网点"""
|
||||
# with pytest.raises(BusinessException):
|
||||
# asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[test_asset.id],
|
||||
# organization_id=test_asset.organization_id, # 相同网点
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# def test_allocate_duplicate_assets(self, db, asset_service):
|
||||
# """测试分配时包含重复资产"""
|
||||
# with pytest.raises(BusinessException):
|
||||
# asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[1, 1, 2], # 资产ID重复
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# def test_approve_allocation_order(self, db, asset_service):
|
||||
# """测试审批分配单"""
|
||||
# # 创建分配单
|
||||
# allocation_order = asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[1],
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# # 审批通过
|
||||
# asset_service.approve_allocation_order(
|
||||
# db=db,
|
||||
# order_id=allocation_order.id,
|
||||
# approval_status="approved",
|
||||
# approver_id=2,
|
||||
# remark="同意"
|
||||
# )
|
||||
#
|
||||
# # 验证资产状态已更新
|
||||
# asset = db.query(Asset).filter(Asset.id == 1).first()
|
||||
# assert asset.status == "in_use"
|
||||
#
|
||||
# # 验证分配单执行状态
|
||||
# db.refresh(allocation_order)
|
||||
# assert allocation_order.approval_status == "approved"
|
||||
# assert allocation_order.execute_status == "completed"
|
||||
#
|
||||
# def test_reject_allocation_order(self, db, asset_service):
|
||||
# """测试拒绝分配单"""
|
||||
# allocation_order = asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[1],
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# # 审批拒绝
|
||||
# asset_service.approve_allocation_order(
|
||||
# db=db,
|
||||
# order_id=allocation_order.id,
|
||||
# approval_status="rejected",
|
||||
# approver_id=2,
|
||||
# remark="不符合条件"
|
||||
# )
|
||||
#
|
||||
# # 验证资产状态未改变
|
||||
# asset = db.query(Asset).filter(Asset.id == 1).first()
|
||||
# assert asset.status == "in_stock"
|
||||
#
|
||||
# db.refresh(allocation_order)
|
||||
# assert allocation_order.approval_status == "rejected"
|
||||
# assert allocation_order.execute_status == "cancelled"
|
||||
|
||||
|
||||
# class TestStateMachine:
|
||||
# """测试状态机"""
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def state_machine(self):
|
||||
# return StateMachineService()
|
||||
#
|
||||
# def test_valid_state_transitions(self, state_machine):
|
||||
# """测试有效的状态转换"""
|
||||
# valid_transitions = [
|
||||
# ("pending", "in_stock"),
|
||||
# ("in_stock", "in_use"),
|
||||
# ("in_stock", "maintenance"),
|
||||
# ("in_use", "transferring"),
|
||||
# ("in_use", "maintenance"),
|
||||
# ("maintenance", "in_stock"),
|
||||
# ("transferring", "in_use"),
|
||||
# ("in_use", "pending_scrap"),
|
||||
# ("pending_scrap", "scrapped"),
|
||||
# ]
|
||||
#
|
||||
# for old_status, new_status in valid_transitions:
|
||||
# assert state_machine.can_transition(old_status, new_status) is True
|
||||
#
|
||||
# def test_invalid_state_transitions(self, state_machine):
|
||||
# """测试无效的状态转换"""
|
||||
# invalid_transitions = [
|
||||
# ("pending", "in_use"), # pending不能直接到in_use
|
||||
# ("in_stock", "pending"), # 不能回退到pending
|
||||
# ("scrapped", "in_stock"), # 报废后不能恢复
|
||||
# ("in_use", "pending_scrap"), # 应该先transferring
|
||||
# ]
|
||||
#
|
||||
# for old_status, new_status in invalid_transitions:
|
||||
# assert state_machine.can_transition(old_status, new_status) is False
|
||||
#
|
||||
# def test_record_state_change(self, db, state_machine, test_asset):
|
||||
# """测试记录状态变更"""
|
||||
# state_machine.record_state_change(
|
||||
# db=db,
|
||||
# asset_id=test_asset.id,
|
||||
# old_status="in_stock",
|
||||
# new_status="in_use",
|
||||
# operator_id=1,
|
||||
# operation_type="allocate",
|
||||
# remark="资产分配"
|
||||
# )
|
||||
#
|
||||
# history = db.query(AssetStatusHistory).filter(
|
||||
# AssetStatusHistory.asset_id == test_asset.id
|
||||
# ).first()
|
||||
#
|
||||
# assert history is not None
|
||||
# assert history.old_status == "in_stock"
|
||||
# assert history.new_status == "in_use"
|
||||
# assert history.operation_type == "allocate"
|
||||
# assert history.remark == "资产分配"
|
||||
#
|
||||
# def test_get_available_transitions(self, state_machine):
|
||||
# """测试获取可用的状态转换"""
|
||||
# transitions = state_machine.get_available_transitions("in_stock")
|
||||
#
|
||||
# assert "in_use" in transitions
|
||||
# assert "maintenance" in transitions
|
||||
# assert "pending_scrap" not in transitions
|
||||
#
|
||||
# def test_state_transition_with_invalid_permission(self, db, state_machine, test_asset):
|
||||
# """测试无权限的状态转换"""
|
||||
# # 普通用户不能直接报废资产
|
||||
# with pytest.raises(PermissionDeniedException):
|
||||
# state_machine.transition_state(
|
||||
# db=db,
|
||||
# asset_id=test_asset.id,
|
||||
# new_status="scrapped",
|
||||
# operator_id=999, # 无权限的用户
|
||||
# operation_type="scrap"
|
||||
# )
|
||||
|
||||
|
||||
# class TestAssetStatistics:
|
||||
# """测试资产统计"""
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def asset_service(self):
|
||||
# return AssetService()
|
||||
#
|
||||
# def test_get_asset_overview(self, db, asset_service):
|
||||
# """测试获取资产概览统计"""
|
||||
# # 创建测试数据
|
||||
# # ... 创建不同状态的资产
|
||||
#
|
||||
# stats = asset_service.get_asset_overview(db)
|
||||
#
|
||||
# assert stats["total_assets"] > 0
|
||||
# assert stats["total_value"] > 0
|
||||
# assert "assets_in_stock" in stats
|
||||
# assert "assets_in_use" in stats
|
||||
# assert "assets_maintenance" in stats
|
||||
# assert "assets_scrapped" in stats
|
||||
#
|
||||
# def test_get_organization_distribution(self, db, asset_service):
|
||||
# """测试获取网点分布统计"""
|
||||
# distribution = asset_service.get_organization_distribution(db)
|
||||
#
|
||||
# assert isinstance(distribution, list)
|
||||
# if len(distribution) > 0:
|
||||
# assert "org_name" in distribution[0]
|
||||
# assert "count" in distribution[0]
|
||||
# assert "value" in distribution[0]
|
||||
#
|
||||
# def test_get_device_type_distribution(self, db, asset_service):
|
||||
# """测试获取设备类型分布统计"""
|
||||
# distribution = asset_service.get_device_type_distribution(db)
|
||||
#
|
||||
# assert isinstance(distribution, list)
|
||||
# if len(distribution) > 0:
|
||||
# assert "type_name" in distribution[0]
|
||||
# assert "count" in distribution[0]
|
||||
#
|
||||
# def test_get_value_trend(self, db, asset_service):
|
||||
# """测试获取价值趋势"""
|
||||
# trend = asset_service.get_value_trend(
|
||||
# db=db,
|
||||
# start_date="2024-01-01",
|
||||
# end_date="2024-12-31",
|
||||
# group_by="month"
|
||||
# )
|
||||
#
|
||||
# assert isinstance(trend, list)
|
||||
# if len(trend) > 0:
|
||||
# assert "date" in trend[0]
|
||||
# assert "count" in trend[0]
|
||||
# assert "value" in trend[0]
|
||||
|
||||
|
||||
# 性能测试
|
||||
# class TestAssetServicePerformance:
|
||||
# """测试资产管理服务性能"""
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def asset_service(self):
|
||||
# return AssetService()
|
||||
#
|
||||
# @pytest.mark.slow
|
||||
# def test_large_asset_list_query_performance(self, db, asset_service):
|
||||
# """测试大量资产查询性能"""
|
||||
# # 创建1000个资产
|
||||
# assets = []
|
||||
# for i in range(1000):
|
||||
# asset = Asset(
|
||||
# asset_code=f"ASSET-20250124-{i:04d}",
|
||||
# asset_name=f"测试资产{i}",
|
||||
# device_type_id=1,
|
||||
# organization_id=1,
|
||||
# status="in_stock"
|
||||
# )
|
||||
# assets.append(asset)
|
||||
# db.bulk_save_objects(assets)
|
||||
# db.commit()
|
||||
#
|
||||
# import time
|
||||
# start_time = time.time()
|
||||
#
|
||||
# items, total = asset_service.get_assets(
|
||||
# db=db,
|
||||
# skip=0,
|
||||
# limit=20
|
||||
# )
|
||||
#
|
||||
# elapsed_time = time.time() - start_time
|
||||
#
|
||||
# assert len(items) == 20
|
||||
# assert total == 1000
|
||||
# assert elapsed_time < 0.5 # 查询应该在500ms内完成
|
||||
#
|
||||
# @pytest.mark.slow
|
||||
# def test_batch_allocation_performance(self, db, asset_service):
|
||||
# """测试批量分配性能"""
|
||||
# # 创建100个资产
|
||||
# asset_ids = []
|
||||
# for i in range(100):
|
||||
# asset = Asset(
|
||||
# asset_code=f"ASSET-20250124-{i:04d}",
|
||||
# asset_name=f"测试资产{i}",
|
||||
# device_type_id=1,
|
||||
# organization_id=1,
|
||||
# status="in_stock"
|
||||
# )
|
||||
# db.add(asset)
|
||||
# db.flush()
|
||||
# asset_ids.append(asset.id)
|
||||
# db.commit()
|
||||
#
|
||||
# import time
|
||||
# start_time = time.time()
|
||||
#
|
||||
# allocation_order = asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=asset_ids,
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# elapsed_time = time.time() - start_time
|
||||
#
|
||||
# assert allocation_order.asset_count == 100
|
||||
# assert elapsed_time < 2.0 # 批量分配应该在2秒内完成
|
||||
1042
backend_new/tests/services/test_asset_state_machine.py
Normal file
1042
backend_new/tests/services/test_asset_state_machine.py
Normal file
File diff suppressed because it is too large
Load Diff
762
backend_new/tests/services/test_auth_service.py
Normal file
762
backend_new/tests/services/test_auth_service.py
Normal file
@@ -0,0 +1,762 @@
|
||||
"""
|
||||
认证服务测试
|
||||
|
||||
测试内容:
|
||||
- 登录服务测试(15+用例)
|
||||
- Token管理测试(10+用例)
|
||||
- 密码管理测试(10+用例)
|
||||
- 验证码测试(5+用例)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.services.auth_service import auth_service
|
||||
from app.models.user import User
|
||||
from app.core.exceptions import (
|
||||
InvalidCredentialsException,
|
||||
UserLockedException,
|
||||
UserDisabledException
|
||||
)
|
||||
|
||||
|
||||
# ==================== 登录服务测试 ====================
|
||||
|
||||
class TestAuthServiceLogin:
|
||||
"""测试认证服务登录功能"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试登录成功"""
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert result.access_token is not None
|
||||
assert result.refresh_token is not None
|
||||
assert result.token_type == "Bearer"
|
||||
assert result.user.id == test_user.id
|
||||
assert result.user.username == test_user.username
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_wrong_password(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试密码错误"""
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
"wrongpassword",
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_user_not_found(
|
||||
self,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试用户不存在"""
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
"nonexistent",
|
||||
"password",
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_account_disabled(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试账户被禁用"""
|
||||
test_user.status = "disabled"
|
||||
await db_session.commit()
|
||||
|
||||
with pytest.raises(UserDisabledException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_account_locked(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试账户被锁定"""
|
||||
test_user.status = "locked"
|
||||
test_user.locked_until = datetime.utcnow() + timedelta(minutes=30)
|
||||
await db_session.commit()
|
||||
|
||||
with pytest.raises(UserLockedException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_auto_unlock_after_lock_period(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试锁定期后自动解锁"""
|
||||
test_user.status = "locked"
|
||||
test_user.locked_until = datetime.utcnow() - timedelta(minutes=1)
|
||||
await db_session.commit()
|
||||
|
||||
# 应该能登录成功并自动解锁
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert result.access_token is not None
|
||||
|
||||
# 验证用户已解锁
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.status == "active"
|
||||
assert test_user.locked_until is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_increases_fail_count(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试登录失败增加失败次数"""
|
||||
initial_count = test_user.login_fail_count
|
||||
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
"wrongpassword",
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.login_fail_count == initial_count + 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_locks_after_max_failures(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试达到最大失败次数后锁定"""
|
||||
test_user.login_fail_count = 4 # 差一次就锁定
|
||||
await db_session.commit()
|
||||
|
||||
with pytest.raises(UserLockedException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
"wrongpassword",
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.status == "locked"
|
||||
assert test_user.locked_until is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_resets_fail_count_on_success(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试登录成功重置失败次数"""
|
||||
test_user.login_fail_count = 3
|
||||
await db_session.commit()
|
||||
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.login_fail_count == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_updates_last_login_time(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试登录更新最后登录时间"""
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.last_login_at is not None
|
||||
assert test_user.last_login_at.date() == datetime.utcnow().date()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_case_sensitive_username(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试用户名大小写敏感"""
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username.upper(), # 大写
|
||||
"password",
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_with_admin_user(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_admin: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试管理员登录"""
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_admin.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert result.user.is_admin is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_generates_different_tokens(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试每次登录生成不同的Token"""
|
||||
result1 = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
result2 = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# Access token应该不同
|
||||
assert result1.access_token != result2.access_token
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_includes_user_roles(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_role,
|
||||
test_password: str
|
||||
):
|
||||
"""测试登录返回用户角色"""
|
||||
# 分配角色
|
||||
from app.models.user import UserRole
|
||||
user_role = UserRole(
|
||||
user_id=test_user.id,
|
||||
role_id=test_role.id
|
||||
)
|
||||
db_session.add(user_role)
|
||||
await db_session.commit()
|
||||
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 应该包含角色信息
|
||||
|
||||
|
||||
# ==================== Token管理测试 ====================
|
||||
|
||||
class TestTokenManagement:
|
||||
"""测试Token管理"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_success(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试刷新Token成功"""
|
||||
# 先登录
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 刷新Token
|
||||
result = await auth_service.refresh_token(
|
||||
db_session,
|
||||
login_result.refresh_token
|
||||
)
|
||||
|
||||
assert "access_token" in result
|
||||
assert "expires_in" in result
|
||||
assert result["access_token"] != login_result.access_token
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_invalid(
|
||||
self,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试无效的刷新Token"""
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.refresh_token(
|
||||
db_session,
|
||||
"invalid_refresh_token"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_for_disabled_user(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试为禁用用户刷新Token"""
|
||||
# 先登录
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 禁用用户
|
||||
test_user.status = "disabled"
|
||||
await db_session.commit()
|
||||
|
||||
# 尝试刷新Token
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.refresh_token(
|
||||
db_session,
|
||||
login_result.refresh_token
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_access_token_expiration(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试访问Token过期时间"""
|
||||
from app.core.config import settings
|
||||
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert login_result.expires_in == settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_contains_user_info(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试Token包含用户信息"""
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 解析Token
|
||||
from app.core.security import security_manager
|
||||
payload = security_manager.verify_token(
|
||||
login_result.access_token,
|
||||
token_type="access"
|
||||
)
|
||||
|
||||
assert int(payload.get("sub")) == test_user.id
|
||||
assert payload.get("username") == test_user.username
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_longer_lifespan(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试刷新Token比访问Token有效期更长"""
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 验证两种Token都存在
|
||||
assert login_result.access_token is not None
|
||||
assert login_result.refresh_token is not None
|
||||
assert login_result.access_token != login_result.refresh_token
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_refresh_tokens(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试多次刷新Token"""
|
||||
# 先登录
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 多次刷新
|
||||
refresh_token = login_result.refresh_token
|
||||
for _ in range(3):
|
||||
result = await auth_service.refresh_token(
|
||||
db_session,
|
||||
refresh_token
|
||||
)
|
||||
assert "access_token" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_type_is_bearer(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试Token类型为Bearer"""
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert login_result.token_type == "Bearer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_user_has_all_permissions(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_admin: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试管理员用户拥有所有权限"""
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_admin.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 管理员应该有所有权限标记
|
||||
|
||||
|
||||
# ==================== 密码管理测试 ====================
|
||||
|
||||
class TestPasswordManagement:
|
||||
"""测试密码管理"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_success(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试修改密码成功"""
|
||||
result = await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
test_password,
|
||||
"NewPassword123"
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_wrong_old_password(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试修改密码时旧密码错误"""
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
"wrongoldpassword",
|
||||
"NewPassword123"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_updates_hash(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试修改密码更新哈希值"""
|
||||
old_hash = test_user.password_hash
|
||||
|
||||
await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
test_password,
|
||||
"NewPassword123"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.password_hash != old_hash
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_resets_lock_status(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试修改密码重置锁定状态"""
|
||||
# 设置为锁定状态
|
||||
test_user.status = "locked"
|
||||
test_user.locked_until = datetime.utcnow() + timedelta(minutes=30)
|
||||
test_user.login_fail_count = 5
|
||||
await db_session.commit()
|
||||
|
||||
# 修改密码
|
||||
await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
test_password,
|
||||
"NewPassword123"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.login_fail_count == 0
|
||||
assert test_user.locked_until is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_password_by_admin(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试管理员重置密码"""
|
||||
result = await auth_service.reset_password(
|
||||
db_session,
|
||||
test_user.id,
|
||||
"AdminReset123"
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_password_non_existent_user(
|
||||
self,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试重置不存在的用户密码"""
|
||||
result = await auth_service.reset_password(
|
||||
db_session,
|
||||
999999,
|
||||
"NewPassword123"
|
||||
)
|
||||
|
||||
assert result is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_hash_strength(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试密码哈希强度(bcrypt)"""
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
hash1 = get_password_hash("password123")
|
||||
hash2 = get_password_hash("password123")
|
||||
|
||||
# 相同密码应该产生不同哈希(因为盐值不同)
|
||||
assert hash1 != hash2
|
||||
|
||||
# 但都应该能验证成功
|
||||
from app.core.security import security_manager
|
||||
assert security_manager.verify_password("password123", hash1)
|
||||
assert security_manager.verify_password("password123", hash2)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_password_login(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试用新密码登录"""
|
||||
new_password = "NewPassword123"
|
||||
|
||||
# 修改密码
|
||||
await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
test_password,
|
||||
new_password
|
||||
)
|
||||
|
||||
# 用新密码登录
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
new_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert result.access_token is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_old_password_not_work_after_change(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试修改密码后旧密码不能登录"""
|
||||
new_password = "NewPassword123"
|
||||
|
||||
# 修改密码
|
||||
await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
test_password,
|
||||
new_password
|
||||
)
|
||||
|
||||
# 用旧密码登录应该失败
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
|
||||
# ==================== 验证码测试 ====================
|
||||
|
||||
class TestCaptchaVerification:
|
||||
"""测试验证码验证"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_captcha_verification_bypassed_in_test(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试验证码在测试环境中被绕过"""
|
||||
# 当前的实现中,验证码验证总是返回True
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"any_captcha",
|
||||
"any_uuid"
|
||||
)
|
||||
|
||||
assert result.access_token is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_captcha_required_parameter(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试验证码参数存在"""
|
||||
# 应该传递验证码参数,即使测试环境不验证
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
# 验证码为空,测试环境应该允许
|
||||
assert result.access_token is not None
|
||||
Reference in New Issue
Block a user