Files
border-red-post-nalati/backend/tests/test_runtime.py
T

193 lines
7.1 KiB
Python

"""Run the default application lifecycle over HTTP and verify persistence."""
import os
import socket
import subprocess
import sys
import time
from contextlib import contextmanager
from pathlib import Path
import httpx
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models.auth import User
from app.services.auth import PASSWORD_HASHER
@contextmanager
def server(engine, *, authenticate=True):
with socket.socket() as socket_:
socket_.bind(("127.0.0.1", 0))
port = socket_.getsockname()[1]
environment = {
**os.environ,
"DATABASE_URL": engine.url.render_as_string(hide_password=False),
"APP_ENV": "test",
"AUTH_COOKIE_SECURE": "false",
}
if authenticate:
with Session(engine) as db:
if db.scalar(select(User.id).where(User.username == "http_admin")) is None:
db.add(
User(
username="http_admin",
password_hash=PASSWORD_HASHER.hash("test-password-123"),
role="admin",
)
)
db.commit()
backend = Path(__file__).resolve().parents[1]
process = subprocess.Popen(
[
sys.executable,
"-m",
"uvicorn",
"app.main:app",
"--host",
"127.0.0.1",
"--port",
str(port),
"--log-level",
"error",
],
cwd=backend,
env=environment,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
try:
with httpx.Client(base_url=f"http://127.0.0.1:{port}", timeout=2) as client:
client.headers["Origin"] = "http://localhost:5173"
deadline = time.monotonic() + 15
while time.monotonic() < deadline:
if process.poll() is not None:
raise AssertionError("默认后端进程启动失败")
try:
if client.get("/api/ready").status_code == 200:
break
except httpx.TransportError:
pass
time.sleep(0.05)
else:
raise AssertionError("默认后端未在时限内就绪")
if authenticate:
login = client.post(
"/api/auth/login",
json={"username": "http_admin", "password": "test-password-123"},
)
assert login.status_code == 200, login.text
client.headers["X-CSRF-Token"] = login.json()["csrf_token"]
yield client
finally:
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
def test_default_startup_and_persistence_across_restart(migrated_database):
with server(migrated_database) as client:
assert client.get("/docs").status_code == 200
response = client.post(
"/api/knowledge",
json={
"title": "持久化验收占位",
"content": "仅为自动化测试数据",
"source_url": "",
},
)
assert response.status_code == 201
item = response.json()
with server(migrated_database) as client:
assert client.get(f"/api/knowledge/{item['id']}").json()["content"] == "仅为自动化测试数据"
assert client.get("/api/knowledge").json()[0]["id"] == item["id"]
def test_retrieval_run_persists_across_restart(migrated_database):
with server(migrated_database) as client:
item = client.post(
"/api/knowledge",
json={
"title": "HTTP检索测试",
"content": "仅用于自动化测试的那拉提关键词。",
"source_url": "https://example.com/",
"collected_at": "2026-10-15T09:00:00+08:00",
},
).json()
assert (
client.post(
f"/api/knowledge/{item['id']}/review",
json={
"expected_version": item["version"],
"verification_status": "verified",
"reviewer": "test-reviewer",
"verification_notes": "仅验证检索流程",
},
).status_code
== 200
)
response = client.post("/api/retrieval", json={"query": "那拉提"})
assert response.status_code == 200
run = response.json()
assert run["results"][0]["knowledge_id"] == item["id"]
with server(migrated_database) as client:
assert client.get(f"/api/retrieval/runs/{run['run_id']}").json() == run
def test_session_and_logout_survive_http_restart(migrated_database):
from app.services.auth import COOKIE_NAME
with server(migrated_database, authenticate=False) as client:
payload = {"username": "http_user", "password": "test-password-123"}
assert client.post("/api/auth/register", json=payload).status_code == 201
response = client.post("/api/auth/login", json=payload)
cookie = client.cookies.get(COOKIE_NAME)
token = response.json()["csrf_token"]
with server(migrated_database, authenticate=False) as client:
client.cookies.set(COOKIE_NAME, cookie)
assert client.get("/api/auth/me").json()["username"] == "http_user"
assert client.post("/api/auth/logout", headers={"X-CSRF-Token": token}).status_code == 204
with server(migrated_database, authenticate=False) as client:
client.cookies.set(COOKIE_NAME, cookie)
assert client.get("/api/auth/me").status_code == 401
def test_published_index_and_chunks_survive_http_restart(migrated_database):
with server(migrated_database) as client:
item = client.post(
"/api/knowledge",
json={
"title": "切分持久化占位",
"content": "那拉提测试正文。" * 50,
"source_url": "https://example.com/",
"collected_at": "2026-10-16T00:00:00Z",
},
).json()
assert (
client.post(
f"/api/knowledge/{item['id']}/review",
json={
"expected_version": item["version"],
"verification_status": "verified",
"reviewer": "test-reviewer",
"verification_notes": "仅为切分持久化测试",
},
).status_code
== 200
)
built = client.post("/api/indexes", json={"chunk_size": 100, "overlap": 20}).json()
response = client.post(
f"/api/indexes/{built['id']}/publish", json={"expected_generation": 0}
)
assert response.status_code == 200
active = response.json()
chunks = client.get(f"/api/indexes/{built['id']}/chunks").json()
with server(migrated_database) as client:
assert client.get("/api/indexes/active").json() == active
assert client.get(f"/api/indexes/{built['id']}/chunks").json() == chunks
assert client.get("/api/indexes/publications").json()["total"] == 1