Files
Aislo/resources/tester/test_m02_layers_store.py
T
eomsangdonandClaude Opus 5.5 e65592a76d feat(M02): 항목 목록 줄마다 names(일위대가 · 자재조합 · 도번 · 면적번 · 재료번 화면 이름) (PLAN 21-9)
- GET /api/m02/items 줄에 names — 키 대신 보일 이름 · 없으면 null
- 개인 복사 응답 {key} = 새 SS 키 확인 시험
- 결정 기록: 일위대가 갈라 내기 SYSTEM_ADMIN 만 · 첫 진입 뒤 같은 이름 409 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUypcnp5d1gU2aeKh9F2H7
2026-09-29 12:32:37 +09:00

186 lines
9.6 KiB
Python

"""M02 층 집계표(PLAN 21-2) — 층별 읽기 · 쓰기 · 권한 · 첫 진입 뼈대 · 항목 목록 · 항목 복사 ·
시스템 회귀.
임시 storage(회사 · 개인 층) + 견본 시스템 집계표(`write_sample_table`)로만 봄 —
실제 양식은 안 읽음(시스템 층 두 자리 `store.FOLDER` · `layers.SYSTEM_ROOT` 모두 사본).
자재 코드는 실제 M01 마스터에 있는 MT 키를 씀(자재 조합 저장이 마스터 대조를 함).
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from common_util.common_util_auth import verify_session
from config import config_system
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router
from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layers_router
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Template_Layers as layers
from resources.tester.test_m02_structure import write_sample_table
TABLE = "table/구조물집계표"
def _material() -> dict[str, Any]:
for data in m01.loaded()[0].values():
for row in data.get("줄") or []:
if str(row.get("키", "")).startswith("MT"):
return {"코드": row["키"], "이름": row.get("이름"), "규격": None, "단위": None}
raise AssertionError("마스터에 자재 키가 없음")
@pytest.fixture()
def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
system = tmp_path / "system"
system.mkdir()
write_sample_table(system)
monkeypatch.setattr(store, "FOLDER", system)
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
store.renumber()
storage = tmp_path / "storage"
storage.mkdir()
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage))
session: dict[str, Any] = {"user_id": 42, "company_id": 7, "role": "USER", "is_master": False}
app = FastAPI()
app.include_router(layers_router.router)
app.include_router(items_router.router)
app.dependency_overrides[verify_session] = lambda: session
return {"client": TestClient(app), "session": session, "storage": storage, "system": system}
def _as(world: dict, role: str = "USER", user: int = 42, company: int | None = 7) -> TestClient:
world["session"].update(role=role, user_id=user, company_id=company)
return world["client"]
def _add_column(client: TestClient, layer: str, col: dict[str, Any]) -> None:
url = f"/api/m02/layers/{layer}/templates/{TABLE}"
got = client.get(url).json()
got["문서"]["열"].append(col)
put = client.put(url, json={"판": got["판"], "문서": got["문서"]})
assert put.status_code == 200, put.text
COL = {"id": "cw", "머리": ["돌기슭막이", "찰쌓기", "회사형", None], "단위": "m", "꼴": "수"}
def test_시스템_층은_그대로_항목_목록은_SS_줄(world):
client = _as(world)
rows = client.get("/api/m02/items").json()
ss = store.read_combos()["줄"]
assert [r["key"] for r in rows] == [r["키"] for r in ss] and rows[0]["key"] == "SS000001"
assert set(rows[0]) == {"key", "names", *items_router.items_store.ITEM_FIELDS}
assert set(rows[0]["names"]) == {"일위대가", "자재조합", "도번", "면적번", "재료번"}
assert client.get("/api/m02/layers/system/structure-combos").json() == store.read_combos()
# 시스템 층 쓰기는 SYSTEM_ADMIN 만
assert client.post("/api/m02/layers/system/structures/rv").status_code == 403
url = f"/api/m02/layers/system/templates/{TABLE}"
got = client.get(url).json()
assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 403
assert client.delete(f"{url}?판={got['판']}").status_code == 403
admin = _as(world, "SYSTEM_ADMIN")
assert admin.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200
assert admin.delete(f"{url}?판={got['판']}").status_code == 409 # 쓰는 양식
def test_회사_층_첫_진입_뼈대와_쓰기_권한(world):
client = _as(world)
assert client.get("/api/m02/items?layer=company").json() == []
root = world["storage"] / "7" / "templates"
table = json.loads((root / "table/구조물집계표.json").read_text("utf-8"))
assert (
table["열"] == []
and table["층"]
== json.loads((world["system"] / "table/구조물집계표.json").read_text("utf-8"))["층"]
)
assert json.loads((root / "구조물집계표조합.json").read_text("utf-8"))["다음"] == 1
assert json.loads((root / "자재조합.json").read_text("utf-8"))["다음"] == 1
url = f"/api/m02/layers/company/templates/{TABLE}"
got = client.get(url).json()
got["문서"]["열"].append(COL)
assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 403
_add_column(_as(world, "ADMIN"), "company", COL)
items = _as(world).get("/api/m02/items?layer=company").json() # 같은 회사 일반 사용자
assert [(r["key"], r["열"]) for r in items] == [("SS000001", "cw")]
assert items[0]["도번"].startswith("SD") and (root / "structure/cw.json").is_file()
assert client.get("/api/m02/items?layer=company&owner=9").status_code == 403 # 남의 회사
# 일위대가(UA) 갈라 내기는 M01 시스템 마스터에 쓰므로 회사 관리자도 못 함
body = {"본뜬키": "UA000001", "담은로직": [], "이름": "x"}
admin = _as(world, "ADMIN")
assert admin.post("/api/m02/layers/company/structures/cw/combo", json=body).status_code == 403
assert admin.delete(f"{url}?판={client.get(url).json()['판']}").status_code == 409 # 쓰는 양식
def test_개인_층은_본인만_회사_없으면_409(world):
client = _as(world)
_add_column(client, "personal", {**COL, "id": "p1"})
assert [r["열"] for r in client.get("/api/m02/items?layer=personal").json()] == ["p1"]
assert (world["storage"] / "7" / "42" / "templates" / "area" / "p1.json").is_file()
other = client.get("/api/m02/items?layer=personal&owner=7/43")
assert other.status_code == 403
lonely = _as(world, company=None)
groups = lonely.get("/api/m02/items?all=1").json()
assert [(g["layer"], g["items"]) for g in groups][1:] == [("company", []), ("personal", [])]
assert len(groups[0]["items"]) == len(store.read_combos()["줄"]) # 마스터는 보임
made = lonely.post(
"/api/m02/layers/personal/material-combos", json={"줄": {"이름": "a", "담은자재": []}}
)
assert made.status_code == 409 and "회사부터" in made.json()["detail"]
def test_회사_항목을_개인으로_복사(world):
admin = _as(world, "ADMIN")
_add_column(admin, "company", COL)
base = "/api/m02/layers/company"
drawing = admin.get(f"{base}/templates/structure/cw").json()
doc = {**drawing["문서"], "도면": {**drawing["문서"]["도면"], "entities": [{"type": "line"}]}}
admin.put(f"{base}/templates/structure/cw", json={"판": drawing["판"], "문서": doc})
판 = admin.get(f"{base}/material-combos").json()["판"]
row = {"이름": "회사 자재", "담은자재": [_material()]}
um = admin.post(f"{base}/material-combos", json={"판": 판, "줄": row}).json()["키"]
admin.post(f"{base}/structures/cw/use-combo", json={"종류": "자재조합", "키": um})
admin.post(f"{base}/structures/cw/use-combo", json={"종류": "일위대가", "키": "UA000004"})
row = admin.get("/api/m02/items?layer=company").json()[0]
ss = row["key"]
ua_name = items_router.items_store._combo_names()["UA000004"]
assert row["names"] == { # 키 대신 화면 이름
"일위대가": ua_name,
"자재조합": "회사 자재",
"도번": "돌기슭막이 찰쌓기 회사형",
"면적번": "돌기슭막이 찰쌓기 회사형",
"재료번": "돌기슭막이 찰쌓기 회사형",
}
user = _as(world)
body = {"from_layer": "company", "from_owner": "7", "key": ss}
got = user.post("/api/m02/layers/personal/items/copy", json=body)
assert got.status_code == 200, got.text
made = got.json()
assert made["열"] == "cw" and made["key"] == "SS000001" # 응답 = 새 SS 키(바로 고름)
mine_rows = user.get("/api/m02/items?layer=personal").json()
assert mine_rows[0]["key"] == made["key"] and mine_rows[0]["names"]["일위대가"] == ua_name
mine = "/api/m02/layers/personal"
copy = user.get(f"{mine}/templates/structure/cw").json()["문서"]
assert copy["도면"]["entities"] == [{"type": "line"}] and copy["열"] == "cw"
col = next(c for c in user.get(f"{mine}/templates/{TABLE}").json()["문서"]["열"])
assert col["본뜬"] == {"layer": "company", "owner": "7", "key": ss}
mat = user.get(f"{mine}/material-combos/{col['자재조합']}").json()
assert mat["이름"] == "회사 자재" and mat["담은자재"][0]["코드"] == _material()["코드"]
# 또 복사하면 새 열 id · 새 SS 키 · 자재 조합 이름 (2)
again = user.post("/api/m02/layers/personal/items/copy", json=body).json()
assert (again["열"], again["key"]) == ("cw_2", "SS000002")
names = [r["이름"] for r in user.get(f"{mine}/material-combos").json()["줄"]]
assert names == ["회사 자재", "회사 자재 (2)"]
# 마스터 항목도 개인으로 · 없는 키 404
master = {"from_layer": "system", "key": "SS000001"}
assert user.post("/api/m02/layers/personal/items/copy", json=master).status_code == 200
missing = {**body, "key": "SS999999"}
assert user.post("/api/m02/layers/personal/items/copy", json=missing).status_code == 404