"""日志查询模型单元测试。 守的是**时间范围校验**与**时区归一**:起始晚于结束时必须拒绝 —— 放过去 的话 SQL 条件互斥、查询恒为空,使用者会以为是"没有数据"而不是"条件写反了"; 带时区的输入必须转换为本地 naive 时间 —— 不转换时 aware/naive 混用比较 抛 TypeError(穿透成 500),或偏移被驱动静默丢弃(查询窗口偏 8 小时)。 """ from datetime import UTC, datetime import pytest from pydantic import ValidationError from app.schemas.log import LoginLogQuery, OperationLogQuery class TestTimeRange: """start_time ≤ end_time 校验(两类日志共用基类)。""" def test_合法范围通过(self) -> None: query = OperationLogQuery( start_time=datetime(2026, 10, 1, 0, 0, 0), end_time=datetime(2026, 10, 10, 0, 0, 0) ) assert query.start_time is not None def test_起始晚于结束被拒(self) -> None: with pytest.raises(ValidationError): OperationLogQuery(start_time=datetime(2026, 10, 2), end_time=datetime(2026, 10, 1)) def test_登录日志同样校验(self) -> None: with pytest.raises(ValidationError): LoginLogQuery(start_time=datetime(2026, 10, 2), end_time=datetime(2026, 10, 1)) def test_边界相等通过(self) -> None: moment = datetime(2026, 10, 1, 12, 0, 0) assert OperationLogQuery(start_time=moment, end_time=moment).start_time == moment def test_仅传一端不校验(self) -> None: assert OperationLogQuery(start_time=datetime(2026, 10, 1)).end_time is None assert OperationLogQuery(end_time=datetime(2026, 10, 1)).start_time is None class TestQueryFields: """筛选字段边界。""" def test_成功标记只能为0或1(self) -> None: assert OperationLogQuery(success=0).success == 0 with pytest.raises(ValidationError): OperationLogQuery(success=2) def test_分页别名与偏移(self) -> None: query = OperationLogQuery(pageNum=2, pageSize=50) assert query.offset == 50 def test_操作日志筛选字段(self) -> None: query = OperationLogQuery(module="用户管理", action="删除", username="adm") assert (query.module, query.action, query.username) == ("用户管理", "删除", "adm") def test_登录日志按IP精确筛选(self) -> None: assert LoginLogQuery(ip="10.0.0.1").ip == "10.0.0.1" class TestTimezoneNormalization: """时区归一。 回归背景:混合 aware/naive 比较曾抛 TypeError 穿透成 500;aware 直传 曾被驱动静默丢弃偏移(查询窗口整体偏 8 小时且无提示)。 """ def test_utc后缀被转换为本地时间(self) -> None: """Z 后缀 = UTC 零点 = 东八区本地 08:00(与库列的本地时间口径对齐)。""" query = OperationLogQuery(start_time="2026-10-01T00:00:00Z") assert query.start_time == datetime(2026, 10, 1, 8, 0, 0) def test_混合aware与naive不再抛比较错误(self) -> None: query = OperationLogQuery( start_time=datetime(2026, 10, 1, 0, 0, 0), end_time=datetime(2026, 10, 2, 0, 0, 0, tzinfo=UTC), ) assert query.end_time is not None assert query.end_time.tzinfo is None # 已摘除 def test_归一后范围校验仍按本地口径生效(self) -> None: with pytest.raises(ValidationError): OperationLogQuery( start_time="2026-10-02T00:00:00Z", # 本地 10-02 08:00 end_time="2026-10-01 12:00:00", # 本地 10-01 12:00,早于起始 ) def test_naive时间视为本地原样保留(self) -> None: moment = datetime(2026, 10, 1, 12, 0, 0) assert OperationLogQuery(start_time=moment).start_time == moment