"""PLAN 511-A (요구 320-4) — 계산 자리 「서버」 창구 `POST /api/projects/{id}/calc` · 떠 있는 Node. 못박는 것 - 창구 = 프로젝트 권한(다른 회사 403 · 시스템 관리자 통과) · 본문 상한 413 · 모르는 id · 꼴 400. - 늘 떠 있는 Node 답 = 파일 길(`run_calc`) 답 · 죽여도 다음 부름에 다시 띄움. - 시험 길 id(`TRIAL_IDS`) = TS `design_calc_node_trial.ts` 표 · `available` 에 안 섞임. - 공유 DB 안 씀(가짜 풀) · 실제 마스터 안 씀(고정 자료 golden_472). ./venv/Scripts/python.exe -m pytest resources/tester/test_511_a_calc_server.py -q """ from __future__ import annotations import json import re import sys from pathlib import Path import pytest from fastapi import Depends, FastAPI from fastapi.testclient import TestClient ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B06_Section import B06_Section_Router_Calc as calc_router # noqa: E402 from common_util import common_util_auth as auth # noqa: E402 from common_util import common_util_calc_runner as runner # noqa: E402 FIXTURE = ROOT / "resources" / "tester" / "fixtures" / "golden_472" TRIAL_TS = ROOT / "A00_Common" / "design" / "logic" / "design_calc_node_trial.ts" PROJECT = "11111111-2222-3333-4444-555555555555" pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules" / "vite").is_dir(), reason="프론트엔드 의존성 미설치" ) def _fixture(name: str): return json.loads((FIXTURE / name).read_text(encoding="utf-8")) def _inputs() -> dict: ctx = _fixture("context.json") keys = ("earthwork_conversion", "haul_equipment_limits", "natural_spoil_min_ground_slope") return {"detail": _fixture("detail.json"), "context": {k: ctx[k] for k in keys}} class _Cursor: async def execute(self, sql, args=None): self.args = args async def fetchone(self): return (1,) if self.args == (PROJECT,) else None # 회사 1 프로젝트 하나 async def __aenter__(self): return self async def __aexit__(self, *exc): return False class _Conn(_Cursor): def cursor(self): return _Cursor() class _Pool: def acquire(self): return _Conn() @pytest.fixture def client(monkeypatch): monkeypatch.setattr(auth, "get_db_pool", lambda: _Pool()) app = FastAPI() app.include_router( calc_router.router, dependencies=[Depends(auth.verify_session), Depends(auth.require_project_access)], ) session = {"role": "USER", "company_id": 1, "user_id": 7} app.dependency_overrides[auth.verify_session] = lambda: dict(session) return TestClient(app), session def test_main_프로젝트_권한으로_붙음(): main = (ROOT / "main.py").read_text(encoding="utf-8") assert "app.include_router(b06_calc_router, dependencies=protected_with_company)" in main assert "note_api_call(session_id)" in main # 호출량 = 미들웨어(/api 전부) def test_권한_다른_회사_403_관리자_통과(client): http, session = client body = {"run": ["cross.fit"], "inputs": {"args": {"cross.fit": [{}]}}} ok = http.post(f"/api/projects/{PROJECT}/calc", json=body) assert ok.status_code == 200, ok.text assert ok.json()["results"] == {"cross.fit": {"left": None, "right": None}} session["company_id"] = 2 assert http.post(f"/api/projects/{PROJECT}/calc", json=body).status_code == 403 missing = "00000000-0000-0000-0000-000000000000" assert http.post(f"/api/projects/{missing}/calc", json=body).status_code == 404 session["role"] = "SYSTEM_ADMIN" assert http.post(f"/api/projects/{PROJECT}/calc", json=body).status_code == 200 def test_본문_상한_꼴_모르는_id(client, monkeypatch): http, _ = client url = f"/api/projects/{PROJECT}/calc" monkeypatch.setattr(calc_router, "MAX_BODY", 100) big = {"run": ["cross.fit"], "inputs": {"pad": "x" * 200}} assert http.post(url, json=big).status_code == 413 monkeypatch.setattr(calc_router, "MAX_BODY", 8 * 1024 * 1024) assert http.post(url, content=b"{nope").status_code == 400 assert http.post(url, json={"run": "cross.fit"}).status_code == 400 assert http.post(url, json={"run": ["cross.fit"], "inputs": []}).status_code == 400 bad = http.post(url, json={"run": ["ground.widen"], "inputs": {}}) assert bad.status_code == 400 and "ground.widen" in bad.json()["message"] def test_떠_있는_Node_답_파일_길과_같음_죽어도_다시(): ids = ["structure.layouts", "masshaul.cumulative", "haul.plan", "excavation.trench"] want = runner.run_calc(ids, _inputs()) got = runner.run_calc_served(ids, _inputs()) assert got["results"] == want and want["masshaul.cumulative"]["final_cumulative_m3"] assert got["ms"] >= 0 and got["wall_ms"] >= got["ms"] runner._SERVED.proc.kill() runner._SERVED.proc.wait() again = runner.run_calc_served(["structure.layouts"], _inputs()) assert again["results"]["structure.layouts"] == want["structure.layouts"] def test_시험_길_id_TS_표와_같음_available_밖(): src = TRIAL_TS.read_text(encoding="utf-8") table = src.split("const TRIAL: Record = {")[1].split("\n};")[0] assert set(re.findall(r'^ "([a-z_.]+)":', table, re.M)) == runner.TRIAL_IDS out = runner.run_bundle_json(runner.BUNDLE, runner.NPM_SCRIPT, {"run": [], "inputs": {}}) assert set(out["available"]) == runner.NODE_IDS assert not runner.NODE_IDS & runner.TRIAL_IDS