- adj_factor 覆盖索引 (ts_code,trade_date) INCLUDE (adj_factor):根治堆碎片化 (单股 6516 行散 6516 块,满载时位图堆扫 1.5s+),Index Only Scan ~2ms; 因子查询全部改 2 列投影,lag() 窗口只取变点(6516→32 行) - preview 冷路径 2 会话 2 波并发,信息卡合并为 LEFT JOIN LATERAL 一条 - 鉴权会话 60s 进程内缓存(登出/全端登出即时失效),全站请求省 ~80ms - pvj/chipsj/stocksj/facetsj 存 model_dump_json 原串直返(与 response_model 字节一致),热路径 230-2190ms → 1-2ms;get_version 本地缓存+bump 即时可见 - 连接池 10+20;smoke_test 适配鉴权缓存
121 lines
4.7 KiB
Python
121 lines
4.7 KiB
Python
"""使用线上 PostgreSQL 跑鉴权与核心 API 自检,临时用户会自动清理。
|
||
|
||
用法:uv run --directory backend python smoke_test.py
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
import secrets
|
||
from datetime import datetime, timedelta, timezone
|
||
|
||
import httpx
|
||
from sqlalchemy import delete, select
|
||
|
||
from app.auth import drop_session_cache, hash_password, token_digest
|
||
from app.db import async_session, engine
|
||
from app.main import app
|
||
from app.models import AuthSession, Candle, User
|
||
|
||
|
||
async def main() -> None:
|
||
username = f"smoke_{secrets.token_hex(6)}"
|
||
password = secrets.token_urlsafe(24)
|
||
now = datetime.now(timezone.utc)
|
||
|
||
async with async_session() as db:
|
||
db.add(
|
||
User(
|
||
username=username,
|
||
password_hash=hash_password(password),
|
||
password_algo="argon2id",
|
||
is_active=True,
|
||
failed_login_count=0,
|
||
password_changed_at=now,
|
||
created_at=now,
|
||
updated_at=now,
|
||
)
|
||
)
|
||
await db.commit()
|
||
|
||
try:
|
||
transport = httpx.ASGITransport(app=app)
|
||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||
health = await client.get("/api/health")
|
||
protected = await client.post("/api/backtest", json={})
|
||
bad_login = await client.post(
|
||
"/api/auth/login", json={"username": username, "password": "wrong-password"}
|
||
)
|
||
login = await client.post(
|
||
"/api/auth/login", json={"username": username, "password": password}
|
||
)
|
||
print("health:", health.status_code)
|
||
print("protected without login:", protected.status_code)
|
||
print("bad login:", bad_login.status_code)
|
||
print("login:", login.status_code)
|
||
assert health.status_code == 200
|
||
assert protected.status_code == 401
|
||
assert bad_login.status_code == 401
|
||
assert login.status_code == 200
|
||
assert client.cookies.get("stock_session")
|
||
|
||
me = await client.get("/api/auth/me")
|
||
assert me.status_code == 200 and me.json()["username"] == username
|
||
|
||
daily = await client.post(
|
||
"/api/backtest",
|
||
json={
|
||
"symbol": "000001",
|
||
"strategy": "macd_cross",
|
||
"params": {"fast": 12, "slow": 26, "signal": 9},
|
||
"initial_cash": 100000.0,
|
||
"fast_mode": False,
|
||
},
|
||
)
|
||
assert daily.status_code == 200, daily.text
|
||
result = daily.json()
|
||
assert len(result["candles"]) > 100
|
||
print("backtest metrics:", json.dumps(result["metrics"], ensure_ascii=False))
|
||
|
||
token = client.cookies.get("stock_session")
|
||
assert token
|
||
async with async_session() as db:
|
||
auth_session = (
|
||
await db.execute(
|
||
select(AuthSession).where(AuthSession.token_hash == token_digest(token))
|
||
)
|
||
).scalar_one()
|
||
auth_session.expires_at = datetime.now(timezone.utc) - timedelta(seconds=1)
|
||
await db.commit()
|
||
# 鉴权会话有 60s 进程内缓存(auth.py),会掩盖库里的过期态;
|
||
# 清缓存模拟 TTL 已过,验证 require_user 对过期会话本身的判定
|
||
drop_session_cache(digest=token_digest(token))
|
||
assert (await client.get("/api/auth/me")).status_code == 401
|
||
print("expired session: 401")
|
||
|
||
login = await client.post(
|
||
"/api/auth/login", json={"username": username, "password": password}
|
||
)
|
||
assert login.status_code == 200
|
||
logout = await client.post("/api/auth/logout")
|
||
assert logout.status_code == 204
|
||
assert (await client.get("/api/auth/me")).status_code == 401
|
||
print("logout and revoke: ok")
|
||
|
||
async with async_session() as db:
|
||
has_market_data = bool(await db.scalar(select(Candle.id).limit(1)))
|
||
print("market data present:", has_market_data)
|
||
finally:
|
||
async with async_session() as db:
|
||
user_id = await db.scalar(select(User.id).where(User.username == username))
|
||
if user_id is not None:
|
||
await db.execute(delete(AuthSession).where(AuthSession.user_id == user_id))
|
||
await db.execute(delete(User).where(User.id == user_id))
|
||
await db.commit()
|
||
await engine.dispose()
|
||
print("temporary user cleaned")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|