170 lines
6.4 KiB
Python
170 lines
6.4 KiB
Python
|
|
"""系统管理集成测试共用夹具(需要 MySQL 与 Redis)。
|
|||
|
|
|
|||
|
|
运行前置:``docker compose -f docker/docker-compose.dev.yml up -d``
|
|||
|
|
|
|||
|
|
数据卫生约定:
|
|||
|
|
|
|||
|
|
- 测试账号/角色统一 ``t_`` 前缀;工厂夹具清理自己创建的对象,另有
|
|||
|
|
autouse 兜底按前缀全量清理(用例经**接口**创建的数据不在工厂名下,
|
|||
|
|
失败中断时会残留并影响后续用例的 total 断言)
|
|||
|
|
- 操作日志的清理同时按 username 与 user_id —— 账号被删除后其 ``user_id``
|
|||
|
|
会被 SET NULL,只能靠冗余的 username 追溯,清理逻辑必须覆盖两种状态
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import uuid
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
from sqlalchemy import delete, select
|
|||
|
|
|
|||
|
|
from app.core.constants import ROLE_ADMIN, ROLE_USER
|
|||
|
|
from app.core.db import get_session_factory
|
|||
|
|
from app.core.security import create_access_token, hash_password
|
|||
|
|
from app.models import OperationLog, Role, User, UserRole
|
|||
|
|
|
|||
|
|
TEST_PASSWORD = "TestPwd@2026"
|
|||
|
|
# 模块级预计算一次:bcrypt(rounds=12)单次约 0.2–0.4 秒,逐用例重复哈希
|
|||
|
|
# 会让整个集成套件多花 20 秒以上;这些测试账号全部走直签 JWT,
|
|||
|
|
# 哈希值不被任何断言消费,复用一个哈希与每次重算等价
|
|||
|
|
_TEST_PASSWORD_HASH = hash_password(TEST_PASSWORD)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _purge_test_data() -> None:
|
|||
|
|
"""按 `t_` 前缀清理测试残留(先日志后账号;角色先断关联再删)。
|
|||
|
|
|
|||
|
|
``t\\_%`` 中的下划线做了转义 —— 不转义时 `_` 是 LIKE 的单字符通配,
|
|||
|
|
会把 tn... 这类无关账号一并卷入。
|
|||
|
|
"""
|
|||
|
|
factory = get_session_factory()
|
|||
|
|
with factory() as session:
|
|||
|
|
users = list(
|
|||
|
|
session.execute(select(User.id, User.username).where(User.username.like("t\\_%"))).all()
|
|||
|
|
)
|
|||
|
|
ids = [row[0] for row in users]
|
|||
|
|
names = [row[1] for row in users]
|
|||
|
|
if ids:
|
|||
|
|
session.execute(delete(OperationLog).where(OperationLog.user_id.in_(ids)))
|
|||
|
|
if names:
|
|||
|
|
session.execute(delete(OperationLog).where(OperationLog.username.in_(names)))
|
|||
|
|
session.execute(delete(User).where(User.id.in_(ids)))
|
|||
|
|
|
|||
|
|
role_ids = list(
|
|||
|
|
session.execute(select(Role.id).where(Role.role_code.like("t\\_%"))).scalars()
|
|||
|
|
)
|
|||
|
|
if role_ids:
|
|||
|
|
session.execute(delete(UserRole).where(UserRole.role_id.in_(role_ids)))
|
|||
|
|
session.execute(delete(Role).where(Role.id.in_(role_ids)))
|
|||
|
|
session.commit()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(autouse=True)
|
|||
|
|
def _purge_after_each_test():
|
|||
|
|
"""每个集成用例结束后兜底清理,避免残留影响后续用例的计数断言。"""
|
|||
|
|
yield
|
|||
|
|
_purge_test_data()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def make_user():
|
|||
|
|
"""独立测试账号工厂:``make_user(["admin"])`` → dict(id/username/password)。
|
|||
|
|
|
|||
|
|
不复用真实 admin —— 测试会改密码、提 token_version、删除账号,
|
|||
|
|
污染真实账号会让后续手工验证产生困惑。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def _factory(role_codes: list[str] | None = None, username: str | None = None) -> dict:
|
|||
|
|
codes = role_codes if role_codes is not None else [ROLE_USER]
|
|||
|
|
name = username or f"t_{uuid.uuid4().hex[:8]}"
|
|||
|
|
factory = get_session_factory()
|
|||
|
|
with factory() as session:
|
|||
|
|
roles = list(session.execute(select(Role).where(Role.role_code.in_(codes))).scalars())
|
|||
|
|
assert len(roles) == len(codes), f"角色不存在:{codes}"
|
|||
|
|
user = User(
|
|||
|
|
username=name,
|
|||
|
|
password=_TEST_PASSWORD_HASH,
|
|||
|
|
real_name="测试账号",
|
|||
|
|
status=1,
|
|||
|
|
)
|
|||
|
|
user.user_roles = [UserRole(role_id=role.id) for role in roles]
|
|||
|
|
session.add(user)
|
|||
|
|
session.commit()
|
|||
|
|
return {"id": user.id, "username": name, "password": TEST_PASSWORD}
|
|||
|
|
|
|||
|
|
return _factory
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def auth_headers():
|
|||
|
|
"""令牌直签工厂:``auth_headers(user)`` → Authorization 请求头。
|
|||
|
|
|
|||
|
|
绕开验证码与登录接口 —— 令牌只承载身份,授权判定以数据库中的角色为准
|
|||
|
|
(`require_admin` 每次查库),直签不影响测试有效性,省去清理 Redis
|
|||
|
|
限流键与黑名单。令牌版本变化(如重置密码、禁用)后旧令牌会失效,
|
|||
|
|
再次调用本工厂可换新令牌。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def _factory(user: dict) -> dict[str, str]:
|
|||
|
|
factory = get_session_factory()
|
|||
|
|
with factory() as session:
|
|||
|
|
db_user = session.get(User, user["id"])
|
|||
|
|
assert db_user is not None, "账号已被删除,无法签发令牌"
|
|||
|
|
roles = [link.role.role_code for link in db_user.user_roles if link.role]
|
|||
|
|
token, _, _ = create_access_token(
|
|||
|
|
user_id=db_user.id,
|
|||
|
|
token_version=db_user.token_version,
|
|||
|
|
roles=roles,
|
|||
|
|
)
|
|||
|
|
return {"Authorization": f"Bearer {token}"}
|
|||
|
|
|
|||
|
|
return _factory
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def make_role():
|
|||
|
|
"""独立测试角色工厂(ORM 直建,用后删除)。
|
|||
|
|
|
|||
|
|
清理时先删除 user_roles 关联(用例失败时可能残留绑定),再删角色。
|
|||
|
|
"""
|
|||
|
|
created: list[int] = []
|
|||
|
|
|
|||
|
|
def _factory(**overrides: object) -> dict:
|
|||
|
|
code = str(overrides.pop("role_code", f"t_role_{uuid.uuid4().hex[:6]}"))
|
|||
|
|
name = str(overrides.pop("role_name", "测试角色"))
|
|||
|
|
factory = get_session_factory()
|
|||
|
|
with factory() as session:
|
|||
|
|
role = Role(
|
|||
|
|
role_code=code,
|
|||
|
|
role_name=name,
|
|||
|
|
description=overrides.pop("description", None),
|
|||
|
|
sort_order=overrides.pop("sort_order", 99),
|
|||
|
|
status=overrides.pop("status", 1),
|
|||
|
|
**overrides,
|
|||
|
|
)
|
|||
|
|
session.add(role)
|
|||
|
|
session.commit()
|
|||
|
|
created.append(role.id)
|
|||
|
|
return {"id": role.id, "role_code": code, "role_name": name}
|
|||
|
|
|
|||
|
|
yield _factory
|
|||
|
|
|
|||
|
|
factory = get_session_factory()
|
|||
|
|
with factory() as session:
|
|||
|
|
session.execute(delete(UserRole).where(UserRole.role_id.in_(created)))
|
|||
|
|
session.execute(delete(Role).where(Role.id.in_(created)))
|
|||
|
|
session.commit()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def admin(make_user, auth_headers) -> dict:
|
|||
|
|
"""管理员账号(含请求头)。"""
|
|||
|
|
user = make_user([ROLE_ADMIN])
|
|||
|
|
user["headers"] = auth_headers(user)
|
|||
|
|
return user
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def normal(make_user, auth_headers) -> dict:
|
|||
|
|
"""普通用户账号(含请求头)。"""
|
|||
|
|
user = make_user([ROLE_USER])
|
|||
|
|
user["headers"] = auth_headers(user)
|
|||
|
|
return user
|