签入版本

This commit is contained in:
2026-09-12 18:24:41 +08:00
commit fb2b7d9538
852 changed files with 115466 additions and 0 deletions
View File
View File
+51
View File
@@ -0,0 +1,51 @@
"""集成冒烟测试:登录链路 + 各模块只读接口(需 MySQL/Redis 运行,默认跳过)。"""
import pytest
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
async def test_login_flow(client):
"""完整登录链路:/api/v1/captcha → 解析验证码 → /api/v1/login 获取 token。"""
import redis as redis_lib
from core.config import REDIS_AUTH, REDIS_HOST, REDIS_INDEX, REDIS_PASSWORD, REDIS_PORT
r = await client.get("/api/v1/captcha")
body = r.json()
assert body["code"] == 0
idkey = body.get("idkey")
assert idkey
# 从 Redis 读取验证码明文(带认证参数,与 app 连接一致)
redis_client = redis_lib.Redis(
host=REDIS_HOST, port=REDIS_PORT,
password=REDIS_PASSWORD if REDIS_AUTH else None,
db=REDIS_INDEX,
)
code = redis_client.get(idkey).decode()
resp = await client.post("/api/v1/login", json={
"username": "admin", "password": "123456",
"code": code, "key": idkey,
})
login = resp.json()
assert login["code"] == 0
assert login["data"]["access_token"]
@pytest.mark.asyncio
async def test_admin_read_endpoints(client, admin_headers):
"""管理员 token 可访问代表性只读接口。"""
for path in ("/api/v1/user/page", "/api/v1/dept/list", "/api/v1/menu/list",
"/api/v1/index/menu", "/api/v1/job/page"):
resp = await client.get(path, headers=admin_headers)
assert resp.json()["code"] == 0, f"{path} failed"
@pytest.mark.asyncio
async def test_unauthorized_blocked(client):
"""无 token 访问受保护接口应返回 401。"""
resp = await client.get("/api/v1/user/page")
assert resp.json()["code"] == 401
+80
View File
@@ -0,0 +1,80 @@
"""事务与操作日志集成测试:db_session_middleware 的 commit/rollback 分支、operation_log 独立落库。
需真实 MySQL/PostgreSQL/SQL Server/SQLite + Redis 运行(默认标记 integration 跳过)。
"""
import pytest
from sqlalchemy import text
from core.config import DB_PREFIX
from core.database import engine
pytestmark = pytest.mark.integration
def _params(*values):
"""位置参数 → 命名参数(p1/p2/...),无参数返回 None。"""
return {f"p{i + 1}": v for i, v in enumerate(values)} if values else None
def _query_count(table, where, *values):
with engine.connect() as conn:
return conn.execute(
text(f"SELECT COUNT(*) FROM {table} WHERE {where}"), _params(*values)
).scalar()
def _execute(sql, *values):
with engine.begin() as conn:
conn.execute(text(sql), _params(*values))
def _table(name):
return DB_PREFIX + name
_VALID_DEPT = {"name": "测试部门T", "code": "TESTT", "type": 1, "parentId": 0, "sort": 99, "status": 1}
@pytest.mark.asyncio
async def test_write_commit_persists(client, admin_headers):
"""业务成功的写操作应被 db_session_middleware 提交。"""
r = await client.post("/api/v1/dept/add", headers=admin_headers, json=_VALID_DEPT)
assert r.json()["code"] == 0
assert _query_count(_table("dept"), "name=:p1", "测试部门T") >= 1
# 清理测试数据
_execute(f"DELETE FROM {_table('dept')} WHERE name=:p1", "测试部门T")
@pytest.mark.asyncio
async def test_write_rollback_on_validation_error(client, admin_headers):
"""参数校验失败(code!=0)时请求事务应回滚,不产生脏数据。"""
before = _query_count(_table("dept"), "name=:p1", "测试部门X")
r = await client.post("/api/v1/dept/add", headers=admin_headers, json={"name": "测试部门X"}) # 缺必填字段
assert r.json()["code"] != 0
assert _query_count(_table("dept"), "name=:p1", "测试部门X") == before
@pytest.mark.asyncio
async def test_operation_log_persists_on_success(client, admin_headers):
"""成功的写操作后,operation_log 应通过独立会话落库(标题正确)。"""
before = _query_count(_table("operation_log"), "1=1")
r = await client.post("/api/v1/dept/add", headers=admin_headers, json=_VALID_DEPT)
assert r.json()["code"] == 0
after = _query_count(_table("operation_log"), "1=1")
assert after >= before + 1, "operation_log 未新增记录"
_execute(f"DELETE FROM {_table('dept')} WHERE name=:p1", "测试部门T")
@pytest.mark.asyncio
async def test_operation_log_persists_on_failure(client, admin_headers):
"""业务失败的写操作也应记录 operation_log(status=1),且独立会话不受请求回滚影响。"""
# 构造一个通过参数校验但业务失败的操作:部门编码重复(唯一性校验)
await client.post("/api/v1/dept/add", headers=admin_headers, json=_VALID_DEPT)
before = _query_count(_table("operation_log"), "1=1")
# 再次添加同名同编码 → 业务失败
r = await client.post("/api/v1/dept/add", headers=admin_headers, json=_VALID_DEPT)
assert r.json()["code"] != 0
after = _query_count(_table("operation_log"), "1=1")
assert after >= before + 1, "业务失败的写操作未记录 operation_log"
_execute(f"DELETE FROM {_table('dept')} WHERE name=:p1", "测试部门T")
+32
View File
@@ -0,0 +1,32 @@
"""集成测试 fixture:真实 FastAPI 应用 + ASGI client(需 MySQL/Redis 运行)。"""
import httpx
import pytest
import pytest_asyncio
from core import jwt as core_jwt
from core.app import create_app
@pytest.fixture
def app():
return create_app()
@pytest_asyncio.fixture
async def client(app):
# 手动运行 Redis lifespan(当前 httpx 版本不支持 ASGITransport lifespan 参数)
from core.redis_client import redis_lifespan
transport = httpx.ASGITransport(app=app)
async with redis_lifespan(app):
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
yield c
@pytest.fixture
def admin_headers():
"""userId=1 管理员 token(绕过 permission_required 权限校验)。"""
token = core_jwt.create_token(
{"userId": 1, "username": "admin", "realname": "admin"})
return {"Authorization": f"Bearer {token}"}