193 lines
7.1 KiB
Python
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
|