阶段3(上):Python/asyncio/FastAPI 专题的课程、企业场景、面试题和可运行示例
This commit is contained in:
1 parent
b8010a74f3
commit
7f52513ffc
24 files changed
+2817
-33
No files matched your search
@@ -0,0 +1,90 @@
|
||||
# requires: fastapi, httpx
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app import Message, app, get_llm
|
||||
|
||||
AUTH = {"X-API-Key": "demo-key"}
|
||||
BODY = {"messages": [{"role": "user", "content": "你好"}]}
|
||||
|
||||
|
||||
class ScriptedLLM:
|
||||
"""测试替身:按脚本返回 token,可以模拟中途失败。"""
|
||||
|
||||
def __init__(self, tokens: list[str], fail_at: int | None = None) -> None:
|
||||
self.tokens, self.fail_at = tokens, fail_at
|
||||
self.calls: list[list[Message]] = []
|
||||
|
||||
async def stream(self, messages: list[Message], temperature: float) -> AsyncIterator[str]:
|
||||
self.calls.append(messages)
|
||||
for i, t in enumerate(self.tokens):
|
||||
if i == self.fail_at:
|
||||
raise RuntimeError("上游模型 500")
|
||||
yield t
|
||||
|
||||
|
||||
def client_with(llm) -> TestClient:
|
||||
app.dependency_overrides[get_llm] = lambda: llm
|
||||
return TestClient(app, raise_server_exceptions=False)
|
||||
|
||||
|
||||
def parse_sse(text: str) -> list[tuple[str, str]]:
|
||||
events = []
|
||||
for block in text.strip().split("\n\n"):
|
||||
name, data = "message", ""
|
||||
for line in block.splitlines():
|
||||
if line.startswith("event: "):
|
||||
name = line[7:]
|
||||
elif line.startswith("data: "):
|
||||
data = line[6:]
|
||||
events.append((name, data))
|
||||
return events
|
||||
|
||||
|
||||
def teardown_function():
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_auth_required():
|
||||
r = client_with(ScriptedLLM(["x"])).post("/v1/chat", json=BODY)
|
||||
assert r.status_code == 401 and r.json()["code"] == "HTTP_401" and r.json()["request_id"]
|
||||
|
||||
|
||||
def test_validation_error_format():
|
||||
r = client_with(ScriptedLLM(["x"])).post("/v1/chat", headers=AUTH,
|
||||
json={"messages": [{"role": "robot", "content": ""}], "temperature": 5})
|
||||
assert r.status_code == 422
|
||||
fields = {d["field"] for d in r.json()["details"]}
|
||||
assert {"messages.0.role", "messages.0.content", "temperature"} <= fields
|
||||
|
||||
|
||||
def test_non_stream():
|
||||
llm = ScriptedLLM(["你", "好"])
|
||||
r = client_with(llm).post("/v1/chat", headers={**AUTH, "X-Request-ID": "rid-1"}, json={**BODY, "stream": False})
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"request_id": "rid-1", "content": "你好"}
|
||||
assert r.headers["X-Request-ID"] == "rid-1"
|
||||
assert llm.calls[0][0].content == "你好"
|
||||
|
||||
|
||||
def test_stream_sse():
|
||||
r = client_with(ScriptedLLM(["A", "B", "C"])).post("/v1/chat", headers=AUTH, json=BODY)
|
||||
assert r.status_code == 200 and r.headers["content-type"].startswith("text/event-stream")
|
||||
events = parse_sse(r.text)
|
||||
assert [json.loads(d)["delta"] for n, d in events[:-1]] == ["A", "B", "C"]
|
||||
assert events[-1] == ("message", "[DONE]")
|
||||
|
||||
|
||||
def test_stream_error_event():
|
||||
r = client_with(ScriptedLLM(["A", "B"], fail_at=1)).post("/v1/chat", headers=AUTH, json=BODY)
|
||||
events = parse_sse(r.text)
|
||||
assert events[0][0] == "message" and events[-1][0] == "error"
|
||||
assert json.loads(events[-1][1])["message"] == "生成失败"
|
||||
|
||||
|
||||
def test_internal_error_hides_details():
|
||||
r = client_with(ScriptedLLM([])).get("/boom")
|
||||
assert r.status_code == 500
|
||||
assert r.json()["message"] == "服务器内部错误" and "postgres" not in r.text
|
||||
Reference in new issue
Block a user