Files
Aislo/resources/tester/test_511_a_calc_server.py
T

140 lines
5.4 KiB
Python

"""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<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