140 lines
5.4 KiB
Python
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
|