Files
Aislo/resources/tester/test_m01_sl_router.py
T
eomsangdonandClaude Opus 5.5 687484bdc3 feat(m01-sl): 시트 로직 서버 길 — 목록 · 열기 · 저장(서버 다시 셈) · 지우기 · 바깥 값
- M01_SL_Router.py — /api/m01/sl/list · get · save · delete · outside · 자리 SheetLogic/_data(git 밖) · 키 SL-0001 꼴
- 저장 때 바깥 값(품셈 표 · 노임 · 자재 · 장비 · 기존 로직 결과)을 마스터에서 다시 넣고 common_util_spreadsheet(Node)로 다시 셈 · 브라우저 계산값 버림
- 단가 · 장비 · 기존 로직은 /calc(Store.calc)와 같은 셈 · 표는 머리 줄 + 줄(범위는 「이름·아래」 「이름·위」)
- main.py 등록 한 문장(시스템 관리자 · 폴더째 지우면 이 줄도 지움)
- M01_SL_Api.ts 몸 — slList · slGet · slSave · slDelete · slOutside · slPlain(EasyCalc plainError)
- 시험 test_m01_sl_router.py — 저장 · 열기 · 목록 · 지우기 · 서버 다시 셈 · 바깥 값 · 403 · 마스터 사본 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
2026-10-01 13:03:32 +09:00

219 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""시트 로직 시험 길(`M01_MasterData/SheetLogic/M01_SL_Router.py`) 시험 — PLAN 35-5.
· 임시 `_data` 로 저장 → 열기 → 목록 → 지우기 · 번호 SL-0001 꼴
· 저장 때 바깥 값을 다시 넣고 서버가 같은 엔진(Node)으로 다시 셈 — 브라우저 계산값은 버림
· 바깥 값 — 견본 표(범위 조건은 두 칸) · 노임 · 기존 로직 결과 · 틀린 종류
· 권한 없음 403 · 마스터 사본 파일은 안 바뀜
견본 표 · 로직 · 통합문서는 시험 안에 둠(임시 폴더 사본에 견본 표를 더함).
"""
from __future__ import annotations
import json
import shutil
from decimal import Decimal
from pathlib import Path
import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from common_util.common_util_auth import require_system_admin, verify_session
from M01_MasterData import M01_MasterData_Router as router_module
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData.SheetLogic import M01_SL_Router as sl
REAL = store.FOLDER
OWN = "로직_자체.json"
SAMPLE = "QF999997"
TABLE = {
"키": SAMPLE,
"원문번호": "",
"구분": "",
"상세구분": "",
"이름": "견본 높이 표",
"기준": "1㎡",
"출처": "자체",
"조건": {"돌": "고르기", "높이": "범위"},
"범위규칙": "초과·이하",
"값칸": {"석공": "인"},
"줄": [
{"돌": "깬돌", "높이": [None, 3], "석공": 0.5},
{"돌": "깬돌", "높이": [3, None], "석공": 0.7},
],
"주": [],
}
NO_NODE = shutil.which("node") is None
def _labor() -> tuple[str, Decimal]:
rows = json.loads((store.FOLDER / "인력.json").read_text(encoding="utf-8"))["줄"]
row = next(
r
for r in rows
if r.get("상태") == "공표" and not r.get("준용") and isinstance(r.get("값"), (int, float))
)
return row["키"], Decimal(str(row["값"]))
@pytest.fixture
def folder(tmp_path: Path, monkeypatch) -> Path:
master = tmp_path / "master"
master.mkdir()
for path in REAL.glob("*.json"):
if path.name == OWN:
continue
if not path.name.startswith("_") or path.name == store.mk.BOOK.name:
shutil.copy(path, master / path.name)
host = sorted(master.glob("소요량_*.json"))[0]
data = json.loads(host.read_text(encoding="utf-8"))
data["표"].append(TABLE)
host.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
monkeypatch.setattr(store, "FOLDER", master)
monkeypatch.setattr(sl, "DATA", tmp_path / "_data")
return master
@pytest.fixture
def client(folder: Path) -> TestClient:
app = FastAPI()
app.include_router(router_module.router)
app.include_router(sl.router)
return TestClient(app)
def _post(client: TestClient, url: str, body: dict, want: int = 200) -> dict:
res = client.post(url, json=body)
assert res.status_code == want, res.text
return res.json()
def _logic(labor: str, key: str | None = None) -> dict:
"""노임 바깥 값(A1) × 2 = B1 · 표 바깥 값은 둘째 시트 A1 부터 · 브라우저 계산값은 엉터리."""
return {
"key": key,
"이름": "견본 시트 로직",
"결과단위": "원/㎡",
"book": {
"종류": "통합문서",
"판": 1,
"서식": [{}],
"시트": [
{"id": "s1", "이름": "계산", "칸": {"A1": {"값": 1}, "B1": {"식": "A1*2"}}},
{"id": "s2", "이름": "표", "칸": {}},
],
"계산값": {"s1": {"B1": {"수": "999"}}},
},
"outside": [
{"item": {"id": "a", "kind": "노임", "key": labor}, "sheet": "s1", "cell": "A1"},
{"item": {"id": "b", "kind": "표", "table": SAMPLE}, "sheet": "s2", "cell": "A1"},
],
"상자": [{"id": "x", "종류": "계산"}], # 다른 창 칸 — 그대로 저장 · 돌려줌
}
@pytest.mark.skipif(NO_NODE, reason="node 가 없음")
def test_저장_열기_목록_지우기_서버가_다시_셈(client: TestClient, folder: Path) -> None:
labor, price = _labor()
before = {p.name: p.read_bytes() for p in folder.glob("*.json")}
saved = _post(client, "/api/m01/sl/save", {"logic": _logic(labor)})
assert saved["key"] == "SL-0001"
book = saved["logic"]["book"]
assert book["시트"][0]["칸"]["A1"]["값"] == price # 바깥 값을 마스터에서 다시 넣음
assert Decimal(book["계산값"]["s1"]["B1"]["수"]) == 2 * price # 서버 다시 셈(999 는 버림)
table = book["시트"][1]["칸"]
assert [table[c]["값"] for c in ("A1", "B1", "C1", "D1")] == [
"돌",
"높이·아래",
"높이·위",
"석공",
]
assert (
"B2" not in table and table["C2"]["값"] == 3 and table["D3"]["값"] == 0.7
) # 빈 끝은 빈 칸
assert saved["logic"]["상자"] == [{"id": "x", "종류": "계산"}]
assert [v["id"] for v in saved["values"]] == ["a", "b"]
second = _post(client, "/api/m01/sl/save", {"logic": _logic(labor)})
assert second["key"] == "SL-0002"
renamed = {**saved["logic"], "이름": "고친 이름"}
assert _post(client, "/api/m01/sl/save", {"logic": renamed})["key"] == "SL-0001"
listed = client.get("/api/m01/sl/list").json()["logics"]
assert [(x["key"], x["이름"]) for x in listed] == [
("SL-0001", "고친 이름"),
("SL-0002", "견본 시트 로직"),
]
got = client.get("/api/m01/sl/get", params={"key": "SL-0001"}).json()["logic"]
assert got["book"]["계산값"] == book["계산값"]
_post(client, "/api/m01/sl/delete", {"key": "SL-0002"})
assert client.get("/api/m01/sl/get", params={"key": "SL-0002"}).status_code == 404
_post(client, "/api/m01/sl/delete", {"key": "SL-0002"}, want=404)
_post(client, "/api/m01/sl/save", {"logic": _logic(labor, "SL-0009")}, want=404)
_post(client, "/api/m01/sl/save", {"logic": {**_logic(labor), "이름": " "}}, want=400)
assert client.get("/api/m01/sl/get", params={"key": "../x"}).status_code == 400
assert {p.name: p.read_bytes() for p in folder.glob("*.json")} == before # 마스터 사본 그대로
assert sorted(p.name for p in sl.DATA.iterdir()) == ["SL-0001.json"]
def test_바깥_값_표_노임_로직_틀린_종류(client: TestClient) -> None:
labor, price = _labor()
made = _post(
client,
"/api/m01/logic/new",
{
"logic": {
"원문번호": "",
"이름": "견본 노무",
"결과단위": "원/㎡",
"출처": "자체",
"입력": [{"이름": "품"}],
"중간": [],
"호표": [
{
"종류": "인력",
"요소": labor,
"이름": "석공",
"단위": "인",
"수량": "품",
"비목": "노무비",
}
],
"덧줄": [],
"끝수": None,
}
},
)
items = [
{"id": "t", "kind": "표", "table": SAMPLE},
{"id": "l", "kind": "노임", "key": labor},
{"id": "g", "kind": "로직", "key": made["key"], "inputs": {"품": 0.5}},
{"id": "g2", "kind": "로직", "key": made["key"], "inputs": {"품": 0.5}, "field": "재료비"},
{"id": "w", "kind": "자재", "key": labor},
{"id": "x", "kind": "표", "table": "QF999996"},
{"id": "z", "kind": "모름"},
]
got = {v["id"]: v for v in _post(client, "/api/m01/sl/outside", {"items": items})["values"]}
assert got["t"]["rows"][0] == ["돌", "높이·아래", "높이·위", "석공"]
assert got["t"]["rows"][1:] == [["깬돌", None, 3, 0.5], ["깬돌", 3, None, 0.7]]
assert Decimal(str(got["l"]["value"])) == price and got["l"]["unit"]
assert Decimal(str(got["g"]["value"])) == Decimal("0.5") * price
assert got["g2"]["value"] == 0
assert [got[k]["ok"] for k in ("w", "x", "z")] == [False, False, False]
def test_시스템_관리자가_아니면_403(folder: Path) -> None:
app = FastAPI()
app.include_router(
sl.router, dependencies=[Depends(verify_session), Depends(require_system_admin)]
)
app.dependency_overrides[verify_session] = lambda: {"role": "USER", "company_id": 1}
client = TestClient(app)
assert client.get("/api/m01/sl/list").status_code == 403
assert client.post("/api/m01/sl/outside", json={"items": []}).status_code == 403
app.dependency_overrides[verify_session] = lambda: {"role": "SYSTEM_ADMIN", "company_id": None}
assert client.get("/api/m01/sl/list").json() == {"logics": []}