""" 内置登录端到端测试(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()