"""S4:FastAPI 端点测试(server/app.py,httpx TestClient 全链路)。""" from __future__ import annotations from pathlib import Path import pytest from fastapi.testclient import TestClient from genesis.server.app import create_app from genesis.server.store import SessionStore _SAMPLE = Path(__file__).resolve().parents[1] / "sample" @pytest.fixture def client(tmp_path): app = create_app( store=SessionStore(db_path=str(tmp_path / "s.db")), data_root=str(tmp_path / "data"), engine="fake", ) return TestClient(app) def _upload_core(client, sid): files = [ ("requirements", "requirements_newdev.xlsx", _SAMPLE / "requirements_newdev.xlsx"), ("template", "template_design_ja.docx", _SAMPLE / "template_design_ja.docx"), ("write_instruction", "rules_design_ja.docx", _SAMPLE / "rules_design_ja.docx"), ("rules", "rules_entry_ja.docx", _SAMPLE / "rules_entry_ja.docx"), ] for ft, name, path in files: r = client.post(f"/api/sessions/{sid}/files", data={"file_type": ft}, files={"file": (name, path.read_bytes())}) assert r.status_code == 200, r.text def test_health(client): r = client.get("/api/health") assert r.status_code == 200 assert r.json()["status"] == "ok" def test_create_and_get_session(client): r = client.post("/api/sessions", json={"user_id": "u1"}) assert r.status_code == 200 sid = r.json()["session_id"] g = client.get(f"/api/sessions/{sid}") assert g.status_code == 200 assert g.json()["status"] == "uploading" def test_session_list(client): client.post("/api/sessions", json={"user_id": "u1"}) client.post("/api/sessions", json={"user_id": "u1"}) r = client.get("/api/sessions", params={"user_id": "u1"}) assert r.status_code == 200 assert len(r.json()) == 2 def test_full_flow_http(tmp_path): """HTTP 全链路:建会话 → 上传 → 解析 → 确认 → 生成 → QA → 预览/下载。""" app = create_app( store=SessionStore(db_path=str(tmp_path / "s.db")), data_root=str(tmp_path / "data"), engine="fake", ) client = TestClient(app) sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] _upload_core(client, sid) r = client.post(f"/api/sessions/{sid}/start-parse") assert r.status_code == 200 assert client.get(f"/api/sessions/{sid}").json()["status"] == "awaiting_parse_confirm" pr = client.get(f"/api/sessions/{sid}/parse-result") assert pr.status_code == 200 assert "tables" in pr.json() r = client.post(f"/api/sessions/{sid}/confirm-parse") assert r.status_code == 200 assert client.get(f"/api/sessions/{sid}").json()["status"] == "writing" # 无既有系统 → 影响跳过 r = client.post(f"/api/sessions/{sid}/generate", json={"output_language": "auto"}) assert r.status_code == 200 assert client.get(f"/api/sessions/{sid}").json()["result_path"] r = client.post(f"/api/sessions/{sid}/run-qa") assert r.status_code == 200 got = client.get(f"/api/sessions/{sid}").json() assert got["status"] == "done" assert "overall_score" in got["qa_summary"] prev = client.get(f"/api/sessions/{sid}/result/preview") assert prev.status_code == 200 assert "html" in prev.json() dl = client.get(f"/api/sessions/{sid}/result/download") assert dl.status_code == 200 assert dl.headers["content-type"].startswith("application/vnd.openxmlformats") def test_state_transition_invalid_409(client): sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] # uploading 直接 generate → 409 r = client.post(f"/api/sessions/{sid}/generate", json={}) assert r.status_code == 409 def test_upload_invalid_type_400(client): sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] r = client.post(f"/api/sessions/{sid}/files", data={"file_type": "bogus"}, files={"file": ("x.txt", b"x")}) assert r.status_code == 400 def test_root_serves_frontend(client): r = client.get("/") assert r.status_code == 200 assert "Genesis" in r.text def test_delete_session(client): sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] r = client.delete(f"/api/sessions/{sid}") assert r.status_code == 200 assert r.json()["deleted"] is True assert client.get(f"/api/sessions/{sid}").status_code == 404 def test_generate_requires_confirm_409(client): sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] _upload_core(client, sid) # 未确认解析就生成 → 409 r = client.post(f"/api/sessions/{sid}/generate", json={}) assert r.status_code == 409 def test_start_impact_without_existing_system_409(client): sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] _upload_core(client, sid) client.post(f"/api/sessions/{sid}/start-parse") client.post(f"/api/sessions/{sid}/confirm-parse") # 无既有系统 → writing,start-impact 409 r = client.post(f"/api/sessions/{sid}/start-impact") assert r.status_code == 409 def test_qa_requires_generate_409(client): sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] _upload_core(client, sid) client.post(f"/api/sessions/{sid}/start-parse") client.post(f"/api/sessions/{sid}/confirm-parse") r = client.post(f"/api/sessions/{sid}/run-qa") assert r.status_code == 409 def test_report_downloads_after_full_flow(tmp_path): """QA/影响报告下载端点(FileResponse 分支)。""" app = create_app( store=SessionStore(db_path=str(tmp_path / "s.db")), data_root=str(tmp_path / "data"), engine="fake", ) client = TestClient(app) sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] _upload_core(client, sid) client.post(f"/api/sessions/{sid}/start-parse") client.post(f"/api/sessions/{sid}/confirm-parse") client.post(f"/api/sessions/{sid}/generate", json={}) client.post(f"/api/sessions/{sid}/run-qa") qa = client.get(f"/api/sessions/{sid}/result/qa-report") assert qa.status_code == 200 assert "application/json" in qa.headers["content-type"] qa_body = client.get(f"/api/sessions/{sid}/qa-result") assert qa_body.status_code == 200 assert "overall_score" in qa_body.json() # 无影响报告(未跑影响)→ 404 assert client.get(f"/api/sessions/{sid}/result/impact-report").status_code == 404 def test_session_not_found_404(client): assert client.get("/api/sessions/nope").status_code == 404 def test_upload_to_missing_session_404(client): r = client.post("/api/sessions/nope/files", data={"file_type": "requirements"}, files={"file": ("a.xlsx", b"x")}) assert r.status_code == 404 def test_start_parse_no_files_409(client): sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] assert client.post(f"/api/sessions/{sid}/start-parse").status_code == 409 def test_start_parse_missing_session_404(client): assert client.post("/api/sessions/nope/start-parse").status_code == 404 def test_confirm_parse_wrong_state_409(client): sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] # uploading 态直接确认解析 → 409 assert client.post(f"/api/sessions/{sid}/confirm-parse").status_code == 409 def _upload_zip(client, sid): import io import zipfile buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("demo/OrderController.java", "package demo;\n@RestController public class OrderController {}\n") client.post(f"/api/sessions/{sid}/files", data={"file_type": "existing_system"}, files={"file": ("existing.zip", buf.getvalue())}) def test_impact_flow_http_zip(tmp_path): """HTTP 影响调查全流程 + 报告下载(app 164/168-170/174-178/226)。""" app = create_app( store=SessionStore(db_path=str(tmp_path / "s.db")), data_root=str(tmp_path / "data"), engine="fake", ) client = TestClient(app) sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] _upload_core(client, sid) _upload_zip(client, sid) client.post(f"/api/sessions/{sid}/start-parse") assert client.post(f"/api/sessions/{sid}/confirm-parse").json()["status"] == "impact_running" r = client.post(f"/api/sessions/{sid}/start-impact") assert r.status_code == 200 ir = client.get(f"/api/sessions/{sid}/impact-result") assert ir.status_code == 200 and "summary" in ir.json() assert client.post(f"/api/sessions/{sid}/confirm-impact").json()["status"] == "writing" dl = client.get(f"/api/sessions/{sid}/result/impact-report") assert dl.status_code == 200 and "application/json" in dl.headers["content-type"] def test_confirm_impact_wrong_state_409(client): sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] _upload_core(client, sid) client.post(f"/api/sessions/{sid}/start-parse") client.post(f"/api/sessions/{sid}/confirm-parse") # writing(无既有系统)直接确认影响 → 409 assert client.post(f"/api/sessions/{sid}/confirm-impact").status_code == 409 def test_download_result_not_found_404(client): sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] assert client.get(f"/api/sessions/{sid}/result/download").status_code == 404 assert client.get(f"/api/sessions/{sid}/result/qa-report").status_code == 404 def test_real_mode_generate_without_key_500(tmp_path, monkeypatch): """engine=None(真实模式)→ generate 按需 build;build 抛错 → 500。""" import genesis.server.app as app_mod def boom(): raise RuntimeError("LLMNotConfiguredError: 缺少 API Key") monkeypatch.setattr(app_mod, "build_inference_engine", boom) app = create_app( store=SessionStore(db_path=str(tmp_path / "s.db")), data_root=str(tmp_path / "data"), engine=None, ) client = TestClient(app, raise_server_exceptions=False) sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"] _upload_core(client, sid) client.post(f"/api/sessions/{sid}/start-parse") client.post(f"/api/sessions/{sid}/confirm-parse") r = client.post(f"/api/sessions/{sid}/generate", json={}) assert r.status_code == 500 # ---------- 项目配置(S6) ---------- def test_create_and_list_projects(client): body = { "name": "stock", "display_name": "股票系统", "template": str(_SAMPLE / "template_design_ja.docx"), "write_instruction": "", "rules": [], "existing_system_code_dir": "", "design_docs_dir": "", } r = client.post("/api/projects", json=body) assert r.status_code == 200 assert r.json()["name"] == "stock" lst = client.get("/api/projects").json() assert any(p["name"] == "stock" for p in lst) one = client.get("/api/projects/stock").json() assert one["template"] == str(_SAMPLE / "template_design_ja.docx") def test_project_invalid_template_400(client): r = client.post("/api/projects", json={"name": "p", "template": "/no/such.docx"}) assert r.status_code == 400 assert r.json()["detail"]["code"] == "PROJECT_CONFIG_INVALID" def test_delete_project(client): client.post("/api/projects", json={"name": "p", "template": str(_SAMPLE / "template_design_ja.docx")}) assert client.delete("/api/projects/p").json()["deleted"] is True assert client.get("/api/projects/p").status_code == 404 def test_session_accepts_name_and_project(client): r = client.post("/api/sessions", json={"user_id": "u1", "name": "我的会话", "project": "projA"}) assert r.status_code == 200 sid = r.json()["session_id"] assert r.json()["name"] == "我的会话" g = client.get(f"/api/sessions/{sid}").json() assert g["name"] == "我的会话" assert g["project"] == "projA" def test_session_list_includes_name_project(client): client.post("/api/sessions", json={"user_id": "u1", "name": "n1", "project": "pA"}) r = client.get("/api/sessions", params={"user_id": "u1"}).json() assert any(s["name"] == "n1" and s["project"] == "pA" for s in r) def test_generate_with_project_config_no_template_upload(client): """绑定项目(含模板/规则)后,仅上传要件定义即可解析生成。""" client.post("/api/projects", json={ "name": "projA", "template": str(_SAMPLE / "template_design_ja.docx"), "write_instruction": str(_SAMPLE / "rules_design_ja.docx"), "rules": [str(_SAMPLE / "rules_entry_ja.docx")], "existing_system_code_dir": "", "design_docs_dir": "", }) sid = client.post("/api/sessions", json={"user_id": "u1", "project": "projA"}).json()["session_id"] # 仅上传要件定义 r = client.post(f"/api/sessions/{sid}/files", data={"file_type": "requirements"}, files={"file": ("requirements_newdev.xlsx", (_SAMPLE / "requirements_newdev.xlsx").read_bytes())}) assert r.status_code == 200 p = client.post(f"/api/sessions/{sid}/start-parse") assert p.status_code == 200, p.text assert client.post(f"/api/sessions/{sid}/confirm-parse").status_code == 200 # 无既有系统 → 直接 writing,可生成 gen = client.post(f"/api/sessions/{sid}/generate", json={}) assert gen.status_code == 200, gen.text assert client.get(f"/api/sessions/{sid}/result/download").status_code == 200