1. 删除了 test/unit/external_systems/framework/ 下的废弃空测试目录 2. 修复多处测试断言逻辑、参数传递与测试数据构造 3. 新增日志级别、缓存令牌、流事件等DTO单元测试 4. 补充路由绑定、会话仓储、outbox仓储的测试覆盖 5. 更新测试用例中的异常类型、参数校验与业务逻辑断言
274 lines
8.4 KiB
Python
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"
|