签入版本

This commit is contained in:
2026-09-16 12:00:58 +08:00
commit 20a2f846ff
842 changed files with 93035 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""核心层单元测试。"""
+138
View File
@@ -0,0 +1,138 @@
"""BaseService 模板方法测试。"""
import pytest
from core.base_service import BaseService
from tests.fixtures.factories import FakeHandler, FakeRepository
class FakeColumn:
"""伪列对象:模拟 SQLAlchemy 列的 asc/desc。"""
def __init__(self, name):
self.name = name
def asc(self):
return self
def desc(self):
return self
class DummyItem:
"""伪模型:类属性模拟列(支持 .asc()/.desc()),实例属性存值。"""
id = FakeColumn("id")
name = FakeColumn("name")
status = FakeColumn("status")
sort = FakeColumn("sort")
def __init__(self, id=None, name=None, status=1, sort=0):
self.id = id
self.name = name
self.status = status
self.sort = sort
def to_dict(self):
# 与 BaseModel.to_dict() 保持一致:返回 camelCase key
return {"id": self.id, "name": self.name, "status": self.status, "sort": self.sort}
class DummySchema:
"""伪 Pydantic 表单。"""
def __init__(self, data):
for k, v in data.items():
setattr(self, k, v)
self._data = data
@classmethod
def model_validate(cls, data):
return cls(data)
def model_dump(self):
return dict(self._data)
class DummyService(BaseService):
model = DummyItem
repo_cls = FakeRepository
create_schema = DummySchema
update_schema = DummySchema
status_schema = DummySchema
page_like_fields = ("name",)
page_eq_fields = ("status",)
page_order_by = (("sort", "asc"),)
serialize_maps = {"status": {1: "正常", 2: "停用"}}
@pytest.fixture
def service():
repo = FakeRepository([DummyItem(id=1, name="A", status=1), DummyItem(id=2, name="B", status=1)])
DummyService.repo_cls = lambda db=None: repo
return DummyService()
async def test_get_page(service):
handler = FakeHandler(query={"page": 1, "limit": 10})
await service.get_page(handler)
assert handler.result["code"] == 0
assert handler.result["data"]["total"] == 2
assert len(handler.result["data"]["records"]) == 2
assert handler.result["data"]["current"] == 1
assert handler.result["data"]["size"] == 10
# serialize_maps 枚举名(camelCase + Text 后缀)
assert handler.result["data"]["records"][0]["statusText"] == "正常"
async def test_get_detail(service):
assert service.get_detail(1)["name"] == "A"
assert service.get_detail(999) is None
async def test_add(service):
handler = FakeHandler(body={"name": "C", "status": 1, "sort": 3})
await service.add(handler)
assert handler.result["code"] == 0
assert any(item.name == "C" for item in service.repo.items)
async def test_update(service):
handler = FakeHandler(body={"id": 1, "name": "A2", "status": 1, "sort": 5})
await service.update(handler)
assert handler.result["code"] == 0
assert handler.result["msg"] == "更新成功"
async def test_update_requires_id(service):
handler = FakeHandler(body={"name": "A2", "status": 1})
await service.update(handler)
assert handler.result["code"] == 1
assert "记录ID" in handler.result["msg"]
async def test_delete(service):
handler = FakeHandler()
await service.delete(handler, "1,2")
assert handler.result["code"] == 0
assert service.repo._deleted == [1, 2]
async def test_delete_invalid_ids(service):
handler = FakeHandler()
await service.delete(handler, "abc,,0")
assert handler.result["code"] == 1
assert "记录ID" in handler.result["msg"]
async def test_update_status(service):
handler = FakeHandler(body={"id": 1, "status": 2})
await service.update_status(handler)
assert handler.result["code"] == 0
assert handler.result["msg"] == "设置成功"
async def test_update_status_missing_record(service):
handler = FakeHandler(body={"id": 999, "status": 2})
await service.update_status(handler)
assert handler.result["code"] == 1
assert handler.result["msg"] == "记录不存在"
+47
View File
@@ -0,0 +1,47 @@
"""加密工具测试。"""
from common.utils.crypto import (check_password, hash_password, is_bcrypt,
set_password, verify_password, verify_user_password)
def test_hash_and_verify_legacy():
"""无盐 bcrypt(兼容旧数据)。"""
hashed = hash_password("123456")
assert is_bcrypt(hashed)
assert verify_password("123456", hashed)
assert not verify_password("wrong", hashed)
def test_set_and_check_password():
"""盐方案:生成盐 + 双重加盐加密。"""
salt, hashed = set_password("123456")
assert salt
assert is_bcrypt(hashed)
assert check_password("123456", salt, hashed)
assert not check_password("wrong", salt, hashed)
assert not check_password("123456", "", hashed)
def test_salt_differences():
"""同一明文两次 set_password 盐不同,但均可校验通过。"""
salt1, hashed1 = set_password("123456")
salt2, hashed2 = set_password("123456")
assert salt1 != salt2
assert hashed1 != hashed2
assert check_password("123456", salt1, hashed1)
assert check_password("123456", salt2, hashed2)
def test_verify_user_password_prefers_salt():
"""统一校验:优先盐方案。"""
salt, hashed = set_password("123456")
assert verify_user_password("123456", salt, hashed)
assert not verify_user_password("wrong", salt, hashed)
def test_verify_user_password_falls_back_to_bcrypt():
"""统一校验:兼容旧无盐 bcrypt。"""
bcrypt_hash = hash_password("123456")
assert verify_user_password("123456", None, bcrypt_hash)
assert not verify_user_password("wrong", None, bcrypt_hash)
# 非 bcrypt 密文无法通过校验
assert not verify_user_password("123456", None, "not-a-hash")
+43
View File
@@ -0,0 +1,43 @@
"""JWT 加解密测试。"""
import asyncio
from core.jwt import create_token, parse_payload, refresh_token
def test_create_and_parse():
token = create_token({"userId": 1}, timeout=10)
assert isinstance(token, str) and token
result = parse_payload(token)
assert result["code"] == 0
assert result["data"]["userId"] == 1
def test_parse_invalid_token():
result = parse_payload("invalid.token.here")
# 新实现统一约定:code 0-成功 1-失败(旧实现返回 -1)
assert result["code"] == 1
assert result["data"] is None
def test_parse_empty_token():
result = parse_payload("")
assert result["code"] == 1
assert "不能为空" in result["msg"]
def test_refresh_token_rotation():
token = create_token({"userId": 7}, timeout=30)
result = asyncio.run(refresh_token(token, extend_minutes=60))
assert result["code"] == 0
assert result["data"]["expires_in"] == 60 * 60
new_token = result["data"]["access_token"]
parsed = parse_payload(new_token)
assert parsed["code"] == 0
assert parsed["data"]["userId"] == 7
# 轮换后新令牌不等于旧令牌
assert new_token != token
def test_refresh_invalid_token():
result = asyncio.run(refresh_token("invalid.token.here"))
assert result["code"] == 1
+30
View File
@@ -0,0 +1,30 @@
"""Repository 相关纯逻辑与序列化安全测试。"""
from common.utils.helpers import parse_id_list
def test_parse_id_list():
assert parse_id_list("1,2,3") == [1, 2, 3]
assert parse_id_list("1,,3,abc,-2") == [1, 3]
assert parse_id_list("") == []
assert parse_id_list(None) == []
def test_to_dict_excludes_sensitive_fields():
"""BaseModel.to_dict() 不得泄露 password/salt/is_delete。"""
from modules.user.models import User
user = User(username="admin", password="secret-hash", salt="s1", realname="管理员")
data = user.to_dict()
assert "password" not in data
assert "salt" not in data
assert "is_delete" not in data
assert data["username"] == "admin"
assert data["realname"] == "管理员"
def test_to_json_excludes_sensitive_fields():
from modules.user.models import User
user = User(username="admin", password="secret-hash", salt="s1")
data = user.to_json()
assert "password" not in data
assert "salt" not in data
assert "is_delete" not in data
+66
View File
@@ -0,0 +1,66 @@
"""统一响应测试。"""
from core import response as R
class FakeHandler:
"""模拟 Tornado handler 的 finish 方法。"""
def __init__(self):
self.result = None
def finish(self, result):
self.result = result
return self.result
def test_ok_basic():
handler = FakeHandler()
R.ok(handler, data={"a": 1}, msg="成功")
assert handler.result["code"] == 0
assert handler.result["data"] == {"a": 1}
assert handler.result["msg"] == "成功"
assert handler.result["ok"] is True
def test_ok_with_kwargs():
handler = FakeHandler()
R.ok(handler, data=[], count=10)
assert handler.result["count"] == 10
assert handler.result["ok"] is True
def test_failed_default_code():
"""failed 默认码为 1,并含 ok: False。"""
handler = FakeHandler()
R.failed(handler, msg="失败")
assert handler.result["code"] == 1
assert handler.result["data"] is None
assert handler.result["msg"] == "失败"
assert handler.result["ok"] is False
def test_failed_with_code_and_data():
handler = FakeHandler()
R.failed(handler, msg="未认证", code=R.Codes.UNAUTHORIZED, data={"detail": "x"})
assert handler.result["code"] == 401
assert handler.result["data"] == {"detail": "x"}
assert handler.result["ok"] is False
def test_response_generic():
"""通用响应生成器:允许自定义 ok 字段。"""
handler = FakeHandler()
R.response(handler, data=[1], msg="自定义", code=200, success=True, extra=1)
assert handler.result["code"] == 200
assert handler.result["ok"] is True
assert handler.result["extra"] == 1
def test_codes_constants():
assert R.Codes.SUCCESS == 0
assert R.Codes.FAILED == 1
assert R.Codes.UNAUTHORIZED == 401
assert R.Codes.FORBIDDEN == 403
assert R.Codes.NOT_FOUND == 404
assert R.Codes.VALIDATE_ERROR == 422
assert R.Codes.SERVER_ERROR == 500