feat: add initial frontend setup with styles, types, and testing framework

- Created a global CSS file for styling the frontend with responsive design.
- Introduced TypeScript types for various entities including Research, Alpha, and Account.
- Implemented Playwright tests for account management and data synchronization workflows.
- Configured TypeScript with strict settings and included necessary libraries.
- Set up Vite as the build tool with React plugin and API proxy configuration.
- Added a Python script to initialize environment variables securely.
This commit is contained in:
yuxuanhui
2026-09-07 14:54:20 +08:00
commit 3cd280d068
53 changed files with 10888 additions and 0 deletions
View File
+135
View File
@@ -0,0 +1,135 @@
"""Isolated browser acceptance server: temporary database, synthetic upstream, no real network."""
import tempfile
from contextlib import asynccontextmanager
from datetime import datetime, timedelta, timezone
import httpx
from cryptography.fernet import Fernet
from app.config import Settings
from app.main import create_app
from app.models import Base
from app.worldquant import WqClient
TEST_PASSWORD = "browser-test-password"
def sample(index):
super_alpha = index % 7 == 0
language = "PYTHON" if index % 11 == 0 and not super_alpha else "FASTEXPR"
names = [
"短期价格反转",
"成交量与价格背离",
"分析师预期修正",
"行业中性动量",
"低波动质量信号",
"资金流向偏离",
"组合选择实验",
]
return {
"id": f"TEST{index:04}",
"name": names[index % len(names)] + f" · {index:03}",
"type": "SUPER" if super_alpha else "REGULAR",
"stage": "OS" if index % 3 == 0 else "IS",
"status": "ACTIVE" if index % 3 == 0 else "UNSUBMITTED",
"hidden": index % 5 == 0,
"regular": None
if super_alpha
else {
"code": "def alpha(data):\n return -data.close.rank()"
if language == "PYTHON"
else "group_neutralize(-rank(ts_delta(close, 5)), industry)"
},
"selection": {"code": "self_correlation < 0.5"} if super_alpha else None,
"combo": {"code": "alpha"} if super_alpha else None,
"settings": {
"region": ["USA", "CHN", "EUR"][index % 3],
"universe": "TOP3000",
"language": language,
"delay": 1,
"decay": 4,
"neutralization": "INDUSTRY",
"truncation": 0.08,
"pasteurization": "ON",
},
"is": {
"sharpe": None if index % 13 == 0 else round(0.8 + (index % 25) * 0.1, 2),
"fitness": round(0.3 + (index % 20) * 0.06, 2),
"turnover": round(0.08 + (index % 12) * 0.015, 3),
"returns": 0.09,
"margin": 0.0002,
"drawdown": 0.11,
"checks": [{"name": "LOW_SHARPE", "result": "PASS", "value": 2.1, "limit": 1.58}],
},
"os": {"sharpe": 1.1} if index % 3 == 0 else None,
"dateCreated": (datetime(2025, 1, 1, tzinfo=timezone.utc) + timedelta(days=index)).isoformat(),
}
def create_test_app():
temporary = tempfile.TemporaryDirectory(prefix="wq-browser-test-")
settings = Settings(
_env_file=None,
database_url=f"sqlite+aiosqlite:///{temporary.name}/test.db",
admin_password=TEST_PASSWORD,
encryption_key=Fernet.generate_key().decode(),
public_origin="http://127.0.0.1:5179",
)
records = [sample(i) for i in range(620)]
def upstream(request):
path = request.url.path
if path == "/authentication" and request.method == "POST":
return httpx.Response(
201, json={"user": {"id": "TEST_USER"}}, headers={"Set-Cookie": "mock=only; Path=/"}
)
if request.method != "GET":
raise AssertionError("Browser acceptance attempted an upstream mutation")
if path == "/users/self":
return httpx.Response(
200, json={"id": "TEST_USER", "name": "模拟研究员", "email": "test@example.com"}
)
if path == "/users/self/alphas":
unsubmitted = "status" in request.url.params
hidden = request.url.params.get("hidden") == "true"
matched = [
r for r in records if (r["status"] == "UNSUBMITTED") == unsubmitted and r["hidden"] == hidden
]
offset, limit = (
int(request.url.params.get("offset", 0)),
int(request.url.params.get("limit", 100)),
)
return httpx.Response(
200, json={"count": len(matched), "results": matched[offset : offset + limit]}
)
if path.endswith("/recordsets/pnl"):
return httpx.Response(
200,
json={
"schema": {"properties": [{"name": "date"}, {"name": "pnl"}]},
"records": [
[
(datetime(2025, 1, 1) + timedelta(days=i)).date().isoformat(),
i * 250 + (i % 13) * 85,
]
for i in range(180)
],
},
)
record = next((r for r in records if path == f"/alphas/{r['id']}"), None)
return httpx.Response(200, json=record) if record else httpx.Response(404)
application = create_app(settings, WqClient(settings, transport=httpx.MockTransport(upstream)))
original_lifespan = application.router.lifespan_context
@asynccontextmanager
async def lifespan(app):
async with app.state.engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
async with original_lifespan(app):
yield
temporary.cleanup()
application.router.lifespan_context = lifespan
return application
+71
View File
@@ -0,0 +1,71 @@
import httpx
import pytest
from cryptography.fernet import Fernet
from app.config import Settings
from app.main import create_app
from app.models import Base
from app.worldquant import WqClient
ADMIN_PASSWORD = "test-admin-password-only"
@pytest.fixture
def settings(tmp_path):
return Settings(
_env_file=None,
database_url=f"sqlite+aiosqlite:///{tmp_path}/test.db",
admin_password=ADMIN_PASSWORD,
encryption_key=Fernet.generate_key().decode(),
enable_runner=False,
public_origin="http://testserver",
)
@pytest.fixture
async def app(settings):
def unexpected_request(request):
raise AssertionError("Unit tests must never access the real platform")
application = create_app(settings, WqClient(settings, transport=httpx.MockTransport(unexpected_request)))
async with application.state.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with application.router.lifespan_context(application):
yield application
@pytest.fixture
async def client(app):
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://testserver", headers={"X-WQ-Request": "1"}
) as client:
yield client
@pytest.fixture
async def logged_in(client):
response = await client.post("/api/v1/auth/login", json={"username": "admin", "password": ADMIN_PASSWORD})
assert response.status_code == 200
return client
def alpha(alpha_id="alpha1", **changes):
raw = {
"id": alpha_id,
"name": "Reversal",
"type": "REGULAR",
"stage": "IS",
"status": "UNSUBMITTED",
"hidden": False,
"regular": {"code": "-rank(ts_delta(close, 5))"},
"settings": {"region": "USA", "universe": "TOP3000", "language": "FASTEXPR", "delay": 1},
"is": {
"sharpe": 1.5,
"fitness": 0.8,
"turnover": 0.2,
"returns": 0.04,
"checks": [{"name": "LOW_SHARPE", "result": "FAIL", "value": 1.5, "limit": 1.58}],
},
"dateCreated": "2025-01-01T00:00:00Z",
}
return {**raw, **changes}
+198
View File
@@ -0,0 +1,198 @@
"""Run against an isolated acceptance Compose project; never use a personal data stack.
From the repository root:
python3 backend/tests/docker_acceptance.py --env-file .local/docker-test.env
Requires images already built and services started with -p wq-alpha-acceptance.
"""
import argparse
import http.cookiejar
import json
import os
import subprocess
import time
import urllib.error
import urllib.request
from pathlib import Path
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--env-file", required=True, type=Path)
parser.add_argument("--project", default="wq-alpha-acceptance")
args = parser.parse_args()
if not args.project.startswith("wq-alpha-acceptance"):
raise SystemExit("Only an isolated wq-alpha-acceptance* project is allowed")
values = dict(
line.split("=", 1)
for line in args.env_file.read_text().splitlines()
if "=" in line and not line.startswith("#")
)
compose = ["docker", "compose", "--env-file", str(args.env_file), "-p", args.project]
def run(arguments, data=None):
result = subprocess.run(compose + arguments, input=data, capture_output=True, check=True)
return result.stdout
# Assert that we are addressing the isolated project rather than an unrelated localhost app.
published = run(["port", "web", "80"]).decode().strip()
assert published == f"127.0.0.1:{values.get('LOCAL_PORT', '8080')}"
base = f"http://localhost:{values.get('LOCAL_PORT', '8080')}"
opener = urllib.request.build_opener(
urllib.request.ProxyHandler({}), urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())
)
def request(path, method="GET", payload=None):
body = json.dumps(payload).encode() if payload is not None else None
return opener.open(
urllib.request.Request(
base + path,
data=body,
method=method,
headers={"Content-Type": "application/json", "X-WQ-Request": "1", "Origin": base},
),
timeout=10,
)
assert json.load(request("/api/v1/health")) == {"status": "ok"}
html = request("/")
assert "frame-ancestors 'none'" in html.headers["Content-Security-Policy"]
assert b'<div id="root">' in html.read()
try:
request("/api/v1/alphas")
raise AssertionError("Unauthenticated data was exposed")
except urllib.error.HTTPError as error:
assert error.code == 401
json.load(
request(
"/api/v1/auth/login",
"POST",
{"username": values.get("ADMIN_USERNAME", "admin"), "password": values["ADMIN_PASSWORD"]},
)
)
code = b"""import asyncio
from app.config import Settings
from app.db import create_database
from app.alphas import upsert_alpha
async def seed():
engine, sessions = create_database(Settings().database_url)
async with sessions() as db:
await upsert_alpha(db, {"id":"DOCKER_ACCEPTANCE", "name":"Synthetic acceptance record", "type":"REGULAR", "stage":"IS", "status":"UNSUBMITTED", "regular":{"code":"rank(close)"}, "settings":{"region":"USA", "language":"FASTEXPR"}, "is":{"sharpe":2.0}})
await db.commit()
await engine.dispose()
asyncio.run(seed())
"""
run(["exec", "-T", "backend", "python", "-"], code)
json.load(
request(
"/api/v1/alphas/DOCKER_ACCEPTANCE/research",
"PATCH",
{
"note": "persistent local note",
"tags": ["docker-verified"],
"state": "candidate",
"favorite": True,
},
)
)
print("PASS: PostgreSQL migration, login, API authorization, local research write")
run(["up", "-d", "--force-recreate", "--wait"])
# A persisted server session and all database rows survive container replacement.
detail = json.load(request("/api/v1/alphas/DOCKER_ACCEPTANCE"))
assert detail["research"]["note"] == "persistent local note"
run(["exec", "-T", "backend", "python", "-"], code.replace(b'"sharpe":2.0', b'"sharpe":3.0'))
detail = json.load(request("/api/v1/alphas/DOCKER_ACCEPTANCE"))
assert detail["sharpe"] == 3 and detail["research"]["state"] == "candidate"
assert detail["research"]["tags"] == ["docker-verified"]
print("PASS: container replacement preserves session and data; snapshot update preserves research")
dump = run(["exec", "-T", "db", "pg_dump", "-U", "wq", "-d", "wq", "-Fc", "--no-owner"])
backup = Path(".local/docker-acceptance.dump")
fd = os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "wb") as output:
output.write(dump)
# Only this script's scratch restore database is replaced on repeated acceptance runs.
run(["exec", "-T", "db", "dropdb", "-U", "wq", "--if-exists", "wq_acceptance_restore"])
run(["exec", "-T", "db", "createdb", "-U", "wq", "wq_acceptance_restore"])
run(
[
"exec",
"-T",
"db",
"pg_restore",
"-U",
"wq",
"-d",
"wq_acceptance_restore",
"--no-owner",
"--exit-on-error",
],
dump,
)
rows = (
run(
[
"exec",
"-T",
"db",
"psql",
"-U",
"wq",
"-d",
"wq_acceptance_restore",
"-At",
"-c",
"SELECT note || '|' || state FROM research WHERE alpha_id = 'DOCKER_ACCEPTANCE'",
]
)
.decode()
.strip()
)
assert rows == "persistent local note|candidate"
print("PASS: PostgreSQL custom-format backup restores records into independent database")
config = json.loads(run(["-f", "compose.public.yaml", "config", "--format", "json"]))
assert config["services"]["backend"]["environment"]["COOKIE_SECURE"] == "true"
assert config["services"]["backend"]["environment"]["PUBLIC_ORIGIN"].startswith("https://")
assert "ports" not in config["services"]["db"] and "ports" not in config["services"]["backend"]
run(
[
"exec",
"-T",
"web",
"caddy",
"validate",
"--config",
"/etc/caddy/Caddyfile",
"--adapter",
"caddyfile",
]
)
print("PASS: Caddy configuration and public HTTPS/Secure-cookie configuration checks")
# Keep the backend process alive while its database disappears and returns.
run(["stop", "db"])
run(["up", "-d", "--wait", "db"])
probe = b"""import asyncio
from app.config import Settings
from app.db import create_database
from app.jobs import create_job
async def enqueue():
engine, sessions = create_database(Settings().database_url)
async with sessions() as db:
job = await create_job(db, "profile")
print(job.id)
await engine.dispose()
asyncio.run(enqueue())
"""
job_id = run(["exec", "-T", "backend", "python", "-"], probe).decode().strip()
deadline = time.monotonic() + 15
while time.monotonic() < deadline:
status = json.load(request(f"/api/v1/sync-jobs/{job_id}"))["status"]
if status == "waiting_connection":
break
time.sleep(0.25)
else:
raise AssertionError("Scheduler did not resume after database restart")
print("PASS: live backend resumes task processing after database stop/start")
if __name__ == "__main__":
main()
+232
View File
@@ -0,0 +1,232 @@
import csv
import io
from datetime import timedelta
import pytest
from sqlalchemy import select
from app.alphas import upsert_alpha
from app.models import Account, Admin, LoginSession, Research, now
from app.security import cipher
from tests.conftest import ADMIN_PASSWORD, alpha
PREFIX = "/api/v1"
async def seed(app, count=1):
async with app.state.sessions() as db:
for i in range(count):
await upsert_alpha(db, alpha(f"a{i:04}", name=f"Signal {i:04}", **{"is": {"sharpe": i / 100}}))
await db.commit()
async def test_login_logout_expiry_and_password_hash(app, client):
assert (await client.get(f"{PREFIX}/alphas")).status_code == 401
wrong = await client.post(f"{PREFIX}/auth/login", json={"username": "admin", "password": "wrong"})
assert wrong.status_code == 401
login = await client.post(f"{PREFIX}/auth/login", json={"username": "admin", "password": ADMIN_PASSWORD})
assert login.status_code == 200
assert "HttpOnly" in login.headers["set-cookie"] and "SameSite=strict" in login.headers["set-cookie"]
token = client.cookies.get("wq_session")
async with app.state.sessions() as db:
admin = await db.get(Admin, 1)
session = (await db.scalars(select(LoginSession))).one()
assert admin.password_hash.startswith("$argon2id$") and ADMIN_PASSWORD not in admin.password_hash
assert session.token_hash != token
assert (await client.post(f"{PREFIX}/auth/logout")).status_code == 200
client.cookies.set("wq_session", token)
assert (await client.get(f"{PREFIX}/auth/me")).status_code == 401
await client.post(f"{PREFIX}/auth/login", json={"username": "admin", "password": ADMIN_PASSWORD})
async with app.state.sessions() as db:
session = (await db.scalars(select(LoginSession))).one()
session.expires_at = now() - timedelta(seconds=1)
await db.commit()
assert (await client.get(f"{PREFIX}/auth/me")).status_code == 401
async def test_login_rate_limit_and_csrf(client):
for _ in range(5):
assert (
await client.post(f"{PREFIX}/auth/login", json={"username": "admin", "password": "wrong"})
).status_code == 401
assert (
await client.post(f"{PREFIX}/auth/login", json={"username": "admin", "password": ADMIN_PASSWORD})
).status_code == 429
assert (
await client.post(f"{PREFIX}/auth/login", headers={"X-WQ-Request": ""}, json={})
).status_code == 403
assert (
await client.post(f"{PREFIX}/auth/login", headers={"Origin": "https://evil.example"}, json={})
).status_code == 403
async def test_public_session_cookie_is_secure(app, client):
app.state.settings.cookie_secure = True
response = await client.post(
f"{PREFIX}/auth/login", json={"username": "admin", "password": ADMIN_PASSWORD}
)
assert response.status_code == 200
assert "; Secure" in response.headers["set-cookie"] and "; HttpOnly" in response.headers["set-cookie"]
async def test_credentials_encrypted_and_never_returned(app, logged_in):
secret = "private-worldquant-password"
r = await logged_in.put(
f"{PREFIX}/account/credentials", json={"email": "person@example.com", "password": secret}
)
assert r.status_code == 200 and r.json()["configured"] is True
assert "password" not in r.text and secret not in r.text
async with app.state.sessions() as db:
account = await db.get(Account, 1)
assert secret not in account.password_encrypted
assert cipher(app.state.settings).decrypt(account.password_encrypted.encode()).decode() == secret
account.wq_user_id = "person"
await db.commit()
assert (
await logged_in.put(
f"{PREFIX}/account/credentials", json={"email": "other@example.com", "password": secret}
)
).status_code == 409
invalid = await logged_in.put(
f"{PREFIX}/account/credentials", json={"email": "invalid", "password": secret}
)
assert invalid.status_code == 422 and secret not in invalid.text
response = await logged_in.get(f"{PREFIX}/account")
assert response.json()["profile"] == {} and "password" not in response.text
async def test_preferences_and_openapi(app, logged_in):
values = {"display_name": "My Lab", "theme": "dark", "timezone": "UTC", "page_size": 100}
r = await logged_in.patch(f"{PREFIX}/account/preferences", json=values)
assert r.status_code == 200 and all(r.json()[k] == v for k, v in values.items())
values["timezone"] = "invalid/timezone"
assert (await logged_in.patch(f"{PREFIX}/account/preferences", json=values)).status_code == 422
contract = app.openapi()
assert contract["paths"]["/api/v1/alphas"]["get"]["security"] == [{"APIKeyCookie": []}]
assert "text/csv" in contract["paths"]["/api/v1/alphas/export"]["get"]["responses"]["200"]["content"]
assert contract["paths"]["/api/v1/alphas"]["get"]["responses"]["422"]["content"]["application/json"][
"schema"
]["$ref"].endswith("/ErrorOutput")
async def test_local_research_survives_snapshot_and_bulk_is_atomic(app, logged_in):
await seed(app, 3)
r = await logged_in.patch(
f"{PREFIX}/alphas/a0000/research",
json={"note": "keep hypothesis", "tags": ["a", "a"], "state": "candidate", "favorite": True},
)
assert r.status_code == 200
async with app.state.sessions() as db:
await upsert_alpha(db, alpha("a0000", **{"is": {"sharpe": 9.25}}))
await db.commit()
detail = (await logged_in.get(f"{PREFIX}/alphas/a0000")).json()
assert detail["sharpe"] == 9.25 and detail["fitness"] is None
assert detail["research"]["note"] == "keep hypothesis" and detail["research"]["favorite"]
assert detail["research"]["tags"] == ["a"] and detail["research"]["state"] == "candidate"
invalid = await logged_in.patch(
f"{PREFIX}/alphas/research/bulk", json={"alpha_ids": ["a0000", "missing"], "add_tags": ["bad"]}
)
assert invalid.status_code == 404
assert (await logged_in.get(f"{PREFIX}/alphas?tag=bad")).json()["total"] == 0
r = await logged_in.patch(
f"{PREFIX}/alphas/research/bulk",
json={
"alpha_ids": ["a0000", "a0001"],
"add_tags": ["new"],
"remove_tags": ["a"],
"state": "optimizing",
},
)
assert r.json()["updated"] == 2
result = (await logged_in.get(f"{PREFIX}/alphas?tag=new&research_state=optimizing")).json()
assert result["total"] == 2
assert (await logged_in.get(f"{PREFIX}/alphas?tag=ne")).json()["total"] == 0
await logged_in.patch(f"{PREFIX}/alphas/a0000/research", json={"note": "updated"})
detail = (await logged_in.get(f"{PREFIX}/alphas/a0000")).json()
assert detail["research"]["favorite"] and detail["research"]["tags"] == ["new"]
async def test_filter_pagination_sort_and_export_over_500(app, logged_in):
await seed(app, 603)
query = "region=USA&sharpe_min=0.02&sharpe_max=6&sort=sharpe&direction=desc"
page = (await logged_in.get(f"{PREFIX}/alphas?{query}&offset=10&limit=25")).json()
assert page["total"] == 599 and len(page["items"]) == 25
assert page["items"][0]["id"] == "a0590"
exported = await logged_in.get(f"{PREFIX}/alphas/export?{query}&offset=10&limit=25")
rows = list(csv.DictReader(io.StringIO(exported.text.lstrip("\ufeff"))))
assert len(rows) == 599 and rows[0]["id"] == "a0600" and rows[-1]["id"] == "a0002"
assert (await logged_in.get(f"{PREFIX}/alphas?q=Signal%200001")).json()["total"] == 1
assert (await logged_in.get(f"{PREFIX}/alphas?q=%25")).json()["total"] == 0
for query in (
"sort=raw",
"sharpe_min=nan",
"sharpe_min=inf",
"sharpe_min=5&sharpe_max=2",
"limit=1000",
"created_from=2026-01-01&created_to=2025-01-01T00:00:00Z",
):
assert (await logged_in.get(f"{PREFIX}/alphas?{query}")).status_code == 422
async def test_export_formula_injection_and_detail_variants(app, logged_in):
async with app.state.sessions() as db:
await upsert_alpha(
db,
alpha(
"super1",
name="=DANGEROUS()",
type="SUPER",
regular=None,
selection={"code": "self_correlation < 0.5"},
combo={"code": "alpha"},
settings={"language": "FASTEXPR"},
**{"is": {"sharpe": None, "fitness": float("nan")}},
),
)
await upsert_alpha(
db,
alpha(
"python1", regular={"code": "def alpha():\n return close"}, settings={"language": "PYTHON"}
),
)
await db.commit()
row = await db.get(Research, "super1")
row.note = " @formula()"
await db.commit()
detail = (await logged_in.get(f"{PREFIX}/alphas/super1")).json()
assert detail["selection"] == "self_correlation < 0.5" and detail["combo"] == "alpha"
assert detail["expression"] is None and detail["sharpe"] is None and detail["fitness"] is None
assert (await logged_in.get(f"{PREFIX}/alphas/python1")).json()["language"] == "PYTHON"
csv_data = (await logged_in.get(f"{PREFIX}/alphas/export?q=super1")).text
row = list(csv.DictReader(io.StringIO(csv_data.lstrip("\ufeff"))))[0]
assert row["name"] == "'=DANGEROUS()" and row["note"] == "' @formula()"
assert (await logged_in.get(f"{PREFIX}/alphas/super1/pnl")).json() == {
"cached": False,
"points": [],
"fetched_at": None,
}
async def test_job_api_deduplicates_and_retries(app, logged_in):
assert (await logged_in.post(f"{PREFIX}/sync-jobs", json={"kind": "full_sync"})).status_code == 409
await logged_in.put(
f"{PREFIX}/account/credentials", json={"email": "person@example.com", "password": "only-testing"}
)
connect = await logged_in.post(f"{PREFIX}/account/connect")
assert connect.status_code == 202
first = (await logged_in.post(f"{PREFIX}/sync-jobs", json={"kind": "full_sync"})).json()
again = (await logged_in.post(f"{PREFIX}/sync-jobs", json={"kind": "full_sync"})).json()
assert first["id"] == again["id"]
await logged_in.post(f"{PREFIX}/sync-jobs/{first['id']}/cancel")
assert (await logged_in.get(f"{PREFIX}/sync-jobs/{first['id']}")).json()["status"] == "cancelled"
assert (await logged_in.post(f"{PREFIX}/sync-jobs/{first['id']}/retry")).json()["status"] == "queued"
assert (
await logged_in.post(
f"{PREFIX}/sync-jobs", json={"kind": "alpha_refresh", "alpha_ids": ["../authentication"]}
)
).status_code == 422
@pytest.mark.parametrize("endpoint", ["account", "alphas", "alphas/export", "alphas/a1/pnl", "sync-jobs"])
async def test_every_data_endpoint_requires_session(client, endpoint):
assert (await client.get(f"{PREFIX}/{endpoint}")).status_code == 401
+251
View File
@@ -0,0 +1,251 @@
import asyncio
from sqlalchemy import func, select
from sqlalchemy.exc import OperationalError
from app.jobs import Runner, create_job
from app.models import Account, Alpha, Job, JobItem, Pnl, Research
from app.security import cipher
from app.worldquant import VerificationRequired, WqError
from tests.conftest import alpha
class FakePlatform:
def __init__(self):
self.verification_url = None
self.on_retry = None
self.page_calls = []
self.fail_page = True
self.fail_id = True
self.require_verification = False
self.block = None
async def authenticate(self, *args, **kwargs):
if self.require_verification:
self.verification_url = "https://api.worldquantbrain.com/authentication/persona/test"
raise VerificationRequired(self.verification_url)
async def verify(self):
self.require_verification = False
self.verification_url = None
async def profile(self):
return {"id": "user1", "email": "test@example.com", "password": "never-save", "unknown": 1}
async def alphas(self, submission, hidden, offset, before):
self.page_calls.append((submission, hidden, offset, before))
if self.block:
self.block.set()
await asyncio.Future()
if submission == "UNSUBMITTED" and not hidden:
if offset == 0:
return {"results": [alpha("a1"), alpha("a2")], "count": 4}
if self.fail_page:
raise WqError("模拟第二页网络故障", "network_error")
return {"results": [alpha("a2", **{"is": {"sharpe": 7.0}}), alpha("a3")], "count": 4}
if submission == "SUBMITTED" and hidden:
return {"results": [alpha("hidden1", hidden=True, stage="OS", status="ACTIVE")], "next": None}
return {"results": [], "next": None}
async def alpha(self, alpha_id):
if alpha_id == "bad" and self.fail_id:
raise WqError("模拟无权访问", "access_denied")
return alpha(alpha_id, **{"is": {"sharpe": 8.0}})
async def pnl(self, alpha_id):
return {
"schema": {"properties": [{"name": "date"}, {"name": "pnl"}]},
"records": [["2025-01-01", 100]],
}
def disconnect(self):
self.verification_url = None
async def close(self):
pass
async def ready_runner(app):
async with app.state.sessions() as db:
account = await db.get(Account, 1)
account.email = "test@example.com"
account.password_encrypted = cipher(app.state.settings).encrypt(b"test-password").decode()
account.connection_status = "connected"
await db.commit()
return Runner(app.state.sessions, app.state.settings, FakePlatform())
async def job_for(runner, kind="full_sync", ids=None):
async with runner.sessions() as db:
return (await create_job(db, kind, {"alpha_ids": ids or []})).id
async def result(runner, job_id):
async with runner.sessions() as db:
return await db.get(Job, job_id)
async def test_pages_commit_resume_dedupe_and_preserve_research(app):
runner = await ready_runner(app)
job_id = await job_for(runner)
await runner.execute(job_id)
failed = await result(runner, job_id)
assert failed.status == "failed" and failed.processed == 2
assert failed.checkpoint == {"partition": 0, "offset": 2}
async with runner.sessions() as db:
research = await db.get(Research, "a2")
research.note, research.tags, research.state = "keep me", ["keep"], "candidate"
await db.commit()
runner.client.fail_page = False
runner.client.page_calls.clear()
await runner.execute(job_id)
finished = await result(runner, job_id)
assert finished.status == "completed" and finished.processed == 4 and finished.total == 4
assert runner.client.page_calls[0][:3] == ("UNSUBMITTED", False, 2)
assert len({call[3] for call in runner.client.page_calls}) == 1
async with runner.sessions() as db:
assert await db.scalar(select(func.count()).select_from(Alpha)) == 4
assert (await db.get(Alpha, "a2")).sharpe == 7.0
assert (await db.get(Alpha, "hidden1")).hidden is True
research = await db.get(Research, "a2")
assert (research.note, research.tags, research.state) == ("keep me", ["keep"], "candidate")
assert await db.scalar(select(func.count()).select_from(JobItem)) == 4
# A later full scan updates existing IDs, without deleting unseen local records.
second = await job_for(runner)
await runner.execute(second)
assert (await result(runner, second)).processed == 4
async def test_partial_ids_retry_only_errors_and_pnl_cache(app):
runner = await ready_runner(app)
job_id = await job_for(runner, "alpha_refresh", ["good", "bad"])
await runner.execute(job_id)
partial = await result(runner, job_id)
assert (partial.status, partial.processed, partial.failed, partial.total) == (
"completed_with_errors",
1,
1,
2,
)
runner.client.fail_id = False
await runner.execute(job_id)
complete = await result(runner, job_id)
assert (complete.status, complete.processed, complete.failed) == ("completed", 2, 0)
pnl_job = await job_for(runner, "pnl_refresh", ["good", "missing"])
await runner.execute(pnl_job)
assert (await result(runner, pnl_job)).failed == 1
async with runner.sessions() as db:
pnl = await db.get(Pnl, "good")
assert pnl.points == [{"date": "2025-01-01", "value": 100.0}]
async def test_human_verification_waits_then_resumes_pending_jobs(app):
runner = await ready_runner(app)
runner.client.require_verification = True
job_id = await job_for(runner, "alpha_refresh", ["good"])
await runner.execute(job_id)
assert (await result(runner, job_id)).status == "waiting_auth"
async with runner.sessions() as db:
account = await db.get(Account, 1)
assert account.connection_status == "verification_required" and account.verification_url
verification = await job_for(runner, "verify")
await runner.execute(verification)
assert (await result(runner, verification)).status == "completed"
assert (await result(runner, job_id)).status == "queued"
async with runner.sessions() as db:
account = await db.get(Account, 1)
assert (
account.wq_user_id == "user1"
and "password" not in account.profile
and "unknown" not in account.profile
)
await runner.execute(job_id)
assert (await result(runner, job_id)).status == "completed"
async def wait_status(runner, job_id, expected):
async with asyncio.timeout(5):
while (await result(runner, job_id)).status not in expected:
await asyncio.sleep(0.02)
async def test_user_cancel_interrupts_request_and_disconnect_pauses(app):
runner = await ready_runner(app)
runner.client.block = asyncio.Event()
job_id = await job_for(runner)
await runner.start()
await asyncio.wait_for(runner.client.block.wait(), 3)
async with runner.sessions() as db:
job = await db.get(Job, job_id)
job.cancel_requested = True
await db.commit()
await runner.cancel(job_id)
await wait_status(runner, job_id, {"cancelled"})
runner.client.block.clear()
pending = await job_for(runner)
runner.wake.set()
await asyncio.wait_for(runner.client.block.wait(), 3)
await runner.disconnect()
assert (await result(runner, pending)).status == "waiting_connection"
async with runner.sessions() as db:
assert (await db.get(Account, 1)).connection_status == "disconnected"
await runner.stop()
async def test_restart_recovers_running_job_and_keeps_checkpoint(app):
runner = await ready_runner(app)
runner.client.block = asyncio.Event()
job_id = await job_for(runner)
await runner.start()
await asyncio.wait_for(runner.client.block.wait(), 3)
await runner.stop()
assert (await result(runner, job_id)).status == "queued"
# Simulate abrupt process loss after a committed page, before the running flag was cleared.
async with runner.sessions() as db:
job = await db.get(Job, job_id)
job.status, job.checkpoint = "running", {"partition": 0, "offset": 2}
await db.commit()
resumed = Runner(runner.sessions, runner.settings, FakePlatform())
resumed.client.fail_page = False
await resumed.start()
await wait_status(resumed, job_id, {"completed", "failed"})
await resumed.stop()
assert (await result(resumed, job_id)).status == "completed"
assert resumed.client.page_calls[0][:3] == ("UNSUBMITTED", False, 2)
async def test_identity_change_fails_without_overwriting_profile(app):
runner = await ready_runner(app)
async with runner.sessions() as db:
account = await db.get(Account, 1)
account.wq_user_id = "original-user"
await db.commit()
job_id = await job_for(runner, "connect")
await runner.execute(job_id)
assert (await result(runner, job_id)).status == "waiting_connection"
async with runner.sessions() as db:
assert (await db.get(Account, 1)).wq_user_id == "original-user"
async def test_scheduler_recovers_after_database_outage(app):
runner = await ready_runner(app)
job_id = await job_for(runner, "alpha_refresh", ["good"])
original = runner.run_next
attempts = 0
async def failing_once():
nonlocal attempts
attempts += 1
if attempts == 1:
async with runner.sessions() as db:
job = await db.get(Job, job_id)
job.status = "running"
await db.commit()
raise OperationalError("", {}, Exception("simulated database restart"))
await original()
runner.run_next = failing_once
await runner.start()
await wait_status(runner, job_id, {"completed", "failed"})
await runner.stop()
assert attempts >= 2 and (await result(runner, job_id)).status == "completed"
+186
View File
@@ -0,0 +1,186 @@
import asyncio
from datetime import datetime, timedelta, timezone
from email.utils import format_datetime
import httpx
import pytest
from app.alphas import pnl_points, sanitize
from app.worldquant import VerificationRequired, WqClient, WqError
async def test_cookie_auth_expiry_and_read_only_boundary(settings):
calls, auth_count = [], 0
def handler(request):
nonlocal auth_count
calls.append((request.method, request.url.path))
if request.url.path == "/authentication":
auth_count += 1
return httpx.Response(201, headers={"Set-Cookie": f"t=session{auth_count}; Path=/"}, json={})
assert "Authorization" not in request.headers
if request.headers.get("Cookie") == "t=session1":
return httpx.Response(401)
return httpx.Response(200, json={"id": "user1"})
client = WqClient(settings, transport=httpx.MockTransport(handler))
await client.authenticate("person@example.com", "secret")
assert await client.profile() == {"id": "user1"}
assert auth_count == 2
await asyncio.gather(*(client.authenticate("person@example.com", "secret") for _ in range(5)))
assert auth_count == 2
assert all(method == "GET" or path == "/authentication" for method, path in calls)
client.disconnect()
assert client.credentials is None and not list(client.client.cookies.items())
await client.close()
async def test_persona_verification_uses_same_cookie_and_safe_location(settings):
complete = False
def handler(request):
if request.url.path == "/authentication":
return httpx.Response(
401,
headers={
"WWW-Authenticate": "persona",
"Location": "/authentication/persona/challenge",
"Set-Cookie": "challenge=abc; Path=/",
},
)
assert request.headers.get("Cookie") == "challenge=abc"
return httpx.Response(201 if complete else 202, json={})
client = WqClient(settings, transport=httpx.MockTransport(handler))
with pytest.raises(VerificationRequired) as error:
await client.authenticate("test@example.com", "secret")
assert error.value.url.endswith("/authentication/persona/challenge")
with pytest.raises(VerificationRequired):
await client.verify()
complete = True
await client.verify()
assert client.authenticated and client.verification_url is None
await client.close()
unsafe = WqClient(
settings,
transport=httpx.MockTransport(
lambda r: httpx.Response(
401, headers={"WWW-Authenticate": "persona", "Location": "https://evil.example/secret"}
)
),
)
with pytest.raises(WqError) as error:
await unsafe.authenticate("test@example.com", "secret")
assert error.value.code == "invalid_verification"
await unsafe.close()
async def test_retry_after_network_budget_pending_and_permissions(settings):
delays, requests = [], 0
async def sleep(delay):
delays.append(delay)
def handler(request):
nonlocal requests
requests += 1
if requests == 1:
return httpx.Response(429, headers={"Retry-After": "120"})
if requests == 2:
raise httpx.ConnectError("contains private body", request=request)
return httpx.Response(200, json={"id": "ready"})
client = WqClient(settings, transport=httpx.MockTransport(handler), sleep=sleep)
client.credentials, client.authenticated = ("test@example.com", "secret"), True
assert await client.profile() == {"id": "ready"}
assert delays[0] == 120 and len(delays) == 2
date = format_datetime(datetime.now(timezone.utc) + timedelta(seconds=60), usegmt=True)
assert 58 < client.retry_delay(date, 0) <= 60
await client.close()
for status, code in [(403, "access_denied"), (404, "not_found"), (503, "retry_exhausted")]:
c = WqClient(
settings,
transport=httpx.MockTransport(lambda r: httpx.Response(status, text="private data")),
sleep=sleep,
)
c.credentials, c.authenticated = ("test@example.com", "secret"), True
with pytest.raises(WqError) as error:
await c.profile()
assert error.value.code == code and "private" not in str(error.value)
await c.close()
responses = iter(
[httpx.Response(202, headers={"Retry-After": "1"}), httpx.Response(200, json={"records": []})]
)
c = WqClient(settings, transport=httpx.MockTransport(lambda r: next(responses)), sleep=sleep)
c.credentials, c.authenticated = ("test@example.com", "secret"), True
assert await c.pnl("abc") == {"records": []}
await c.close()
@pytest.mark.parametrize(
"raw,expected",
[
(
{
"schema": {"properties": [{"name": "date"}, {"name": "pnl"}]},
"records": [["2025-01-01", 10], ["2025-01-02", None]],
},
[{"date": "2025-01-01", "value": 10.0}, {"date": "2025-01-02", "value": None}],
),
(
{"schema": {"properties": [{"name": "pnl"}, {"name": "date"}]}, "records": [[20, "2025-01-01"]]},
[{"date": "2025-01-01", "value": 20.0}],
),
(
{"schema": {"properties": {"date": {}, "pnl": {}}}, "records": [["2025-01-01", "NaN"]]},
[{"date": "2025-01-01", "value": None}],
),
(
{"records": [{"timestamp": 1735689600000, "value": "5"}]},
[{"date": "2025-01-01T00:00:00+00:00", "value": 5.0}],
),
],
)
def test_pnl_schemas_preserve_null(raw, expected):
assert pnl_points(raw) == expected
def test_unknown_pnl_schema_fails_instead_of_fabricating():
with pytest.raises(ValueError):
pnl_points({"records": [["2025-01-01", 100]]})
def test_sensitive_response_keys_are_removed_recursively():
assert sanitize(
{
"id": "a1",
"accessToken": "secret",
"nested": [{"refresh_token": "secret", "client-secret": "secret", "value": 1}],
"Set-Cookie": "secret",
}
) == {"id": "a1", "nested": [{"value": 1}]}
async def test_concurrent_expiry_authenticates_once(settings):
auth_count = 0
first_requests = 0
both_expired = asyncio.Event()
async def handler(request):
nonlocal auth_count, first_requests
if request.method == "POST":
auth_count += 1
return httpx.Response(201, json={})
if first_requests < 2:
first_requests += 1
if first_requests == 2:
both_expired.set()
await both_expired.wait()
return httpx.Response(401)
return httpx.Response(200, json={"id": "user"})
client = WqClient(settings, transport=httpx.MockTransport(handler))
await client.authenticate("test@example.com", "secret")
assert await asyncio.gather(client.profile(), client.profile()) == [{"id": "user"}, {"id": "user"}]
assert auth_count == 2 # Initial connection plus one shared reauthentication.
await client.close()