"""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"