114 lines
4.0 KiB
Python
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()
|