签入版本
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""核心层单元测试。"""
|
||||
@@ -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"] == "记录不存在"
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user