Files
chongming-core/test/test_auth_builtin.py

114 lines
4.0 KiB
Python

"""
内置登录端到端测试(auth 域拆分验证)
验证拆分目标:POST /api/v1/auth/login 内置在网关,不依赖 auth 插件——
本测试只挂载内置 auth 路由(不加载任何插件),登录/签发/响应 shape 全部可用。
同时校验与拆分前 plugins.auth.login 的兼容性:
- 路径 /api/v1/auth/login、响应字段 {result, token, message, timestamp}
- 失败文案逐字一致(用户名错误 / 密码错误)
- token 可被 chongming_jwt 解密回读,claims 含 is_superuser
(网关 admin API 判超管的契约)
"""
import asyncio
import time
from pathlib import Path
import orjson
import pytest
from chongming_auth import open_database, seed_admin
from chongming_config import get_config
from chongming_jwt import JWT
from fastapi import FastAPI
from fastapi.testclient import TestClient
from chongming.app.routes.auth import router as auth_router
_SECURITY = get_config()["security"]
SECRET, ALGORITHM = _SECURITY["secret_key"], _SECURITY["algorithm"]
PREFIX = get_config()["default"]["prefix"]
@pytest.fixture()
def client(tmp_path: Path):
"""最小 app:仅内置 auth 路由 + 临时 auth 库(模拟 auth 插件完全缺席)。"""
async def _build():
db = await open_database({"data_dir": str(tmp_path)})
await seed_admin(db)
return db
db = asyncio.run(_build())
app = FastAPI()
app.include_router(auth_router)
app.state.auth_db = db
with TestClient(app) as c:
yield c
asyncio.run(db.dispose())
def test_login_success_shape_and_claims(client):
resp = client.post(
f"{PREFIX}/auth/login", json={"username": "admin", "password": "admin"}
)
assert resp.status_code == 200
data = resp.json()
# 响应 shape 与拆分前 gen_models 产物一致
assert set(data.keys()) == {"result", "token", "message", "timestamp"}
assert data["result"] == "success"
assert data["message"] == "登录成功"
assert isinstance(data["timestamp"], float)
assert data["timestamp"] == pytest.approx(time.time(), abs=60)
# token 可解密且 claims 契约完整(is_superuser 是 admin API 的判超管依据)
payload = orjson.loads(JWT(SECRET, ALGORITHM).decode(data["token"]))
assert payload["sub"]
assert payload["username"] == "admin"
assert payload["is_superuser"] is True
def test_login_failure_messages_compat(client):
"""失败文案与拆分前 plugins.auth.login 逐字一致。"""
for body, expected_msg in [
({"username": "nouser", "password": "x"}, "用户名错误"),
({"username": "admin", "password": "wrong"}, "密码错误"),
]:
resp = client.post(f"{PREFIX}/auth/login", json=body)
assert resp.status_code == 200
data = resp.json()
assert data["result"] == "fail"
assert data["token"] == ""
assert data["message"] == expected_msg
assert isinstance(data["timestamp"], float)
def test_login_works_without_auth_plugin(client):
"""拆分核心目标:无 auth 插件时登录照常(fixture 未加载任何插件)。"""
resp = client.post(
f"{PREFIX}/auth/login", json={"username": "admin", "password": "admin"}
)
assert resp.status_code == 200
assert resp.json()["result"] == "success"
def test_login_updates_last_login(client, tmp_path):
"""登录成功后 last_login_at 落库(顺带修复拆分前未 commit 的旧问题)。"""
client.post(f"{PREFIX}/auth/login", json={"username": "admin", "password": "admin"})
row = asyncio.run(_read_admin_row(tmp_path))
assert row.last_login_at is not None
async def _read_admin_row(data_dir: Path):
from chongming_auth import Users
from sqlalchemy import select
db = await open_database({"data_dir": str(data_dir)})
try:
async with db.get_session() as sess:
return (
await sess.execute(select(Users).filter_by(username="admin"))
).scalar_one()
finally:
await db.dispose()