feat(511-A 계산 자리 서버 창구): POST /api/projects/{id}/calc — 프로젝트 권한 · 본문 상한 8 MB · 늘 떠 있는 Node 하나(design_calc_node --serve · 죽음 · 묶음 바뀜 = 다시 띄움 · 1.4 MB 33 ms · 새 Node 110~170 ms) · 시험 길 id 따로(TRIAL_IDS) · 브라우저 길 design_calc_server(잠잠 400 ms · 늦은 답 버림 · 대조 · 기록 __aisloCalcServer) — 요구 320-4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -237,7 +237,7 @@ def test_연결_자리_cross_격자_그대로():
|
||||
assert body.index('if (await calcOnServer("corridor_mesh")) return null;') < body.index(
|
||||
"getWorker()"
|
||||
)
|
||||
assert "void openCalcSites();" in _text("A00_Common/design/design_page.ts")
|
||||
assert "void openCalcSites(projectId);" in _text("A00_Common/design/design_page.ts")
|
||||
# 저장 길은 판을 안 읽음
|
||||
for rel in ("A00_Common/design/design_save.ts", "A00_Common/design/design_save_api.ts"):
|
||||
assert "calc_sites" not in _text(rel) and "calcOnServer" not in _text(rel)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""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": ["stations.list"], "inputs": {}})
|
||||
assert bad.status_code == 400 and "stations.list" 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<string, Call> = {")[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
|
||||
Reference in New Issue
Block a user