93 lines
3.8 KiB
Python
93 lines
3.8 KiB
Python
|
|
"""日志查询模型单元测试。
|
|||
|
|
|
|||
|
|
守的是**时间范围校验**与**时区归一**:起始晚于结束时必须拒绝 —— 放过去
|
|||
|
|
的话 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
|