ForcePilot/backend/test/unit/channels/contract/dtos/test_audit.py
Kris 3ae9c0bd21 test: 批量修复与新增单元测试用例,清理废弃测试目录
1.  删除了 test/unit/external_systems/framework/ 下的废弃空测试目录
2.  修复多处测试断言逻辑、参数传递与测试数据构造
3.  新增日志级别、缓存令牌、流事件等DTO单元测试
4.  补充路由绑定、会话仓储、outbox仓储的测试覆盖
5.  更新测试用例中的异常类型、参数校验与业务逻辑断言
2026-07-11 21:43:16 +08:00

274 lines
8.4 KiB
Python

"""audit.py DTO 单元测试。
覆盖 ``AuditLogId`` / ``AuditOperationType`` / ``AuditTxStrategy`` /
``AuditEntry`` / ``AuditQuery`` / ``AuditLogStats`` / ``RetentionPolicy`` /
``RetentionPolicyUpdateCmd`` 的字段赋值、默认值、不可变语义与
``__post_init__`` 校验逻辑。
"""
from __future__ import annotations
import dataclasses
from datetime import datetime
import pytest
from yuxi.channels.contract.dtos.audit import (
AuditEntry,
AuditLogId,
AuditLogStats,
AuditOperationType,
AuditQuery,
AuditTxStrategy,
RetentionPolicy,
RetentionPolicyUpdateCmd,
)
from yuxi.channels.contract.dtos.common import Operator, OperatorRole
from yuxi.channels.contract.errors import ValidationError
pytestmark = pytest.mark.unit
def _make_operator() -> Operator:
return Operator(user_id="admin-1", role=OperatorRole.ADMIN_USER)
@pytest.mark.unit
class TestAuditLogId:
"""审计日志 ID DTO 测试。"""
def test_value_is_assigned(self):
# Act
log_id = AuditLogId(value="log-001")
# Assert
assert log_id.value == "log-001"
def test_is_frozen(self):
# Arrange
log_id = AuditLogId(value="log-001")
# Act / Assert
with pytest.raises(dataclasses.FrozenInstanceError):
log_id.value = "other" # type: ignore[misc]
@pytest.mark.unit
class TestAuditOperationType:
"""审计操作类型枚举测试。"""
def test_str_value(self):
# Assert
assert AuditOperationType.PLUGIN_LOADED == "plugin_loaded"
assert AuditOperationType.ACCOUNT_CREATED == "account_created"
def test_route_binding_values_contain_dot(self):
# Assert
assert AuditOperationType.ROUTE_BINDING_LIST == "route_binding.list"
assert AuditOperationType.ROUTE_BINDING_CREATE == "route_binding.create"
@pytest.mark.unit
class TestAuditTxStrategy:
"""审计事务策略枚举测试。"""
def test_str_values(self):
# Assert
assert AuditTxStrategy.SHARED == "shared"
assert AuditTxStrategy.INDEPENDENT == "independent"
@pytest.mark.unit
class TestAuditEntry:
"""审计条目 DTO 测试。"""
def test_required_fields_are_assigned(self):
# Arrange
ts = datetime(2024, 1, 1, 12, 0, 0)
# Act
entry = AuditEntry(
operator="admin-1",
operation=AuditOperationType.PLUGIN_STARTED,
target="plugin:wechat",
result="success",
timestamp=ts,
)
# Assert
assert entry.operator == "admin-1"
assert entry.operation == AuditOperationType.PLUGIN_STARTED
assert entry.params_summary is None
assert entry.trace_id is None
def test_is_frozen(self):
# Arrange
entry = AuditEntry(
operator="sys",
operation=AuditOperationType.CONFIG_CHANGED,
target="cfg",
result="success",
timestamp=datetime(2024, 1, 1),
)
# Act / Assert
with pytest.raises(dataclasses.FrozenInstanceError):
entry.operator = "other" # type: ignore[misc]
@pytest.mark.unit
class TestAuditQuery:
"""审计查询 DTO 测试。"""
def test_defaults(self):
# Act
query = AuditQuery()
# Assert
assert query.limit == 100
assert query.offset == 0
assert query.operation_type is None
def test_is_frozen(self):
# Arrange
query = AuditQuery()
# Act / Assert
with pytest.raises(dataclasses.FrozenInstanceError):
query.limit = 50 # type: ignore[misc]
def test_start_must_be_before_end(self):
# Arrange
start = datetime(2024, 1, 2)
end = datetime(2024, 1, 1)
# Act / Assert
with pytest.raises(ValidationError) as exc_info:
AuditQuery(start_time=start, end_time=end)
assert exc_info.value.field == "time_range"
def test_limit_must_be_positive(self):
# Act / Assert
with pytest.raises(ValidationError) as exc_info:
AuditQuery(limit=0)
assert exc_info.value.field == "limit"
def test_offset_must_be_non_negative(self):
# Act / Assert
with pytest.raises(ValidationError) as exc_info:
AuditQuery(offset=-1)
assert exc_info.value.field == "offset"
def test_start_only_without_end_is_valid(self):
# Act
query = AuditQuery(start_time=datetime(2024, 1, 1))
# Assert
assert query.start_time is not None
@pytest.mark.unit
class TestAuditLogStats:
"""审计日志统计聚合结果 DTO 测试。"""
def test_fields_are_assigned(self):
# Act
stats = AuditLogStats(
total=100,
by_operation_type={"plugin_loaded": 50},
by_result={"success": 90, "failed": 10},
time_range_start=datetime(2024, 1, 1),
time_range_end=datetime(2024, 1, 31),
)
# Assert
assert stats.total == 100
assert stats.time_range_start is not None
def test_time_range_can_be_none(self):
# Act
stats = AuditLogStats(
total=0,
by_operation_type={},
by_result={},
time_range_start=None,
time_range_end=None,
)
# Assert
assert stats.time_range_start is None
@pytest.mark.unit
class TestRetentionPolicy:
"""审计保留策略 DTO 测试。"""
def test_fields_are_assigned(self):
# Act
policy = RetentionPolicy(
default_retention_days=90,
by_operation_type={"plugin_loaded": 30},
auto_archive_enabled=True,
auto_archive_before_days=80,
)
# Assert
assert policy.default_retention_days == 90
assert policy.updated_at is None
@pytest.mark.unit
class TestRetentionPolicyUpdateCmd:
"""审计保留策略更新命令 DTO 测试。"""
def test_defaults(self):
# Act
cmd = RetentionPolicyUpdateCmd(operator=_make_operator())
# Assert
assert cmd.default_retention_days == 90
assert cmd.auto_archive_enabled is True
assert cmd.auto_archive_before_days == 80
def test_is_frozen(self):
# Arrange
cmd = RetentionPolicyUpdateCmd(operator=_make_operator())
# Act / Assert
with pytest.raises(dataclasses.FrozenInstanceError):
cmd.default_retention_days = 10 # type: ignore[misc]
def test_default_retention_must_be_positive(self):
# Act / Assert
with pytest.raises(ValidationError) as exc_info:
RetentionPolicyUpdateCmd(operator=_make_operator(), default_retention_days=0)
assert exc_info.value.field == "default_retention_days"
def test_auto_archive_before_must_be_positive(self):
# Act / Assert
with pytest.raises(ValidationError) as exc_info:
RetentionPolicyUpdateCmd(operator=_make_operator(), auto_archive_before_days=0)
assert exc_info.value.field == "auto_archive_before_days"
def test_auto_archive_before_must_be_less_than_default(self):
# Act / Assert
with pytest.raises(ValidationError) as exc_info:
RetentionPolicyUpdateCmd(
operator=_make_operator(),
default_retention_days=30,
auto_archive_before_days=30,
)
assert exc_info.value.field == "auto_archive_before_days"
def test_by_operation_type_invalid_key_raises(self):
# Act / Assert
with pytest.raises(ValidationError) as exc_info:
RetentionPolicyUpdateCmd(
operator=_make_operator(),
by_operation_type={"invalid_key": 30},
)
assert exc_info.value.field == "by_operation_type"
def test_by_operation_type_non_positive_value_raises(self):
# Act / Assert
with pytest.raises(ValidationError) as exc_info:
RetentionPolicyUpdateCmd(
operator=_make_operator(),
by_operation_type={"plugin_loaded": 0},
)
assert exc_info.value.field == "by_operation_type"
def test_by_operation_type_bool_value_raises(self):
# Act / Assert
with pytest.raises(ValidationError) as exc_info:
RetentionPolicyUpdateCmd(
operator=_make_operator(),
by_operation_type={"plugin_loaded": True},
)
assert exc_info.value.field == "by_operation_type"