- 판 1 옮김 37종 · 옵션 235 · 검사 통과 · 읽기 누구나 · 쓰기 403 · 판 +1 · 이력 판_N · 낡은 판 409 - 검사 422 = 키 겹침 · 꼴 · 규칙 대상 · 배치 · 종류 덧칸 · 표시 · 그룹 형상 · 모르는 칸(안 씀) - 프로젝트 저장 정의 판 · 없는 키 값 보존 · ui_checks 48-1_구조물정의_읽기 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
159 lines
7.0 KiB
Python
159 lines
7.0 KiB
Python
"""M02 구조물 정의 한 벌(`/api/m02/structure-definition` · PLAN 48-1).
|
|
|
|
못박는 것
|
|
- 판 1 = 옛 레지스트리 그대로 옮김(37종 · 옵션 235 · 키 그대로) · 검사 통과.
|
|
- 읽기 = 로그인 누구나 · 쓰기 = 시스템 관리자만(403).
|
|
- 저장마다 판 +1 · 앞 판은 `구조물정의_이력/판_N.json` · 낡은 판 409 · 틀린 정의 422(안 씀).
|
|
- 검사 = 키 겹침 · 꼴 · 규칙 대상 있음 · 그룹 형상.
|
|
- 저장한 정의를 `/structure-types` 가 바로 읽음 · 프로젝트 저장에 정의 판 · 없는 키 값 보존.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from B05_Profile import B05_Profile_Structures_Schema as schema
|
|
from B05_Profile.B05_Profile_Structures_Repository import load_structures, save_structures
|
|
from B05_Profile.B05_Profile_Structures_Router import router as b05_router
|
|
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance, load_structure_types
|
|
from common_util.common_util_auth import verify_session
|
|
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
|
from M02_MasterTemplete.StructOptions.M02_SO_Router import read_router
|
|
|
|
URL = "/api/m02/structure-definition"
|
|
TYPES = "/api/projects/structure-types"
|
|
ADMIN = {"role": "SYSTEM_ADMIN", "user_id": 1}
|
|
USER = {"role": "USER", "user_id": 2, "company_id": 1}
|
|
|
|
|
|
@pytest.fixture
|
|
def copy(tmp_path: Path, monkeypatch) -> Path:
|
|
"""정의 사본 — 마스터 정본은 안 씀."""
|
|
path = tmp_path / "구조물정의.json"
|
|
path.write_text(Path(schema.DEFINITION_PATH).read_text("utf-8"), "utf-8")
|
|
monkeypatch.setattr(schema, "DEFINITION_PATH", str(path))
|
|
return path
|
|
|
|
|
|
def _client(session: dict) -> TestClient:
|
|
app = FastAPI()
|
|
app.include_router(read_router)
|
|
app.include_router(b05_router)
|
|
app.dependency_overrides[verify_session] = lambda: session
|
|
return TestClient(app)
|
|
|
|
|
|
def test_판1_은_옛_레지스트리_그대로_검사_통과() -> None:
|
|
doc = definition.read()
|
|
assert doc["판"] == 1 and len(doc["types"]) == 37
|
|
assert sum(len(t["options"]) for t in doc["types"]) == 235
|
|
assert len(load_structure_types()) == 37
|
|
assert sum(len(t.options) for t in load_structure_types()) == 235
|
|
assert definition.check(doc) == []
|
|
assert [g["이름"] for g in doc["그룹"]["pipe"]] == ["관", "관 보호공(유입)", "관 보호공(유출)"]
|
|
assert {t["group"] for t in doc["types"]} <= set(doc["구조물군"])
|
|
|
|
|
|
def test_읽기는_누구나_쓰기는_시스템_관리자만(copy) -> None:
|
|
user = _client(USER)
|
|
got = user.get(URL)
|
|
assert got.status_code == 200 and got.json()["판"] == 1
|
|
assert user.put(URL, json={"판": 1, "정의": got.json()}).status_code == 403
|
|
assert json.loads(copy.read_text("utf-8"))["판"] == 1 # 안 씀
|
|
|
|
|
|
def test_저장마다_판_더하기_1_앞_판은_이력_structure_types_가_바로_읽음(copy) -> None:
|
|
admin = _client(ADMIN)
|
|
doc = admin.get(URL).json()
|
|
tid = doc["types"][0]["type_id"]
|
|
doc["types"][0]["name"] = "이름 바꿈"
|
|
res = admin.put(URL, json={"판": 1, "정의": doc})
|
|
assert res.status_code == 200, res.text
|
|
assert res.json()["판"] == 2
|
|
old = json.loads((copy.parent / "구조물정의_이력" / "판_1.json").read_text("utf-8"))
|
|
assert old["판"] == 1 and old["types"][0]["name"] != "이름 바꿈"
|
|
seen = {t["type_id"]: t for t in admin.get(TYPES).json()["types"]}
|
|
assert seen[tid]["name"] == "이름 바꿈" and len(seen) == 37
|
|
assert admin.put(URL, json={"판": 1, "정의": doc}).status_code == 409 # 낡은 판
|
|
assert admin.put(URL, json={"판": 2, "정의": res.json()}).json()["판"] == 3
|
|
assert (copy.parent / "구조물정의_이력" / "판_2.json").is_file()
|
|
|
|
|
|
def _rule(doc: dict) -> tuple[str, dict, dict]:
|
|
"""고르기 옵션 둘 이상 가진 종류 — (종류 키, 상위 옵션, 하위 옵션)."""
|
|
for t in doc["types"]:
|
|
picks = [o for o in t["options"] if o["input"] == "select"]
|
|
if len(picks) >= 2:
|
|
return t["type_id"], picks[0], picks[1]
|
|
raise AssertionError("고르기 둘인 종류 없음")
|
|
|
|
|
|
def test_검사_키_겹침_꼴_규칙_대상_형상(copy) -> None:
|
|
admin = _client(ADMIN)
|
|
base = admin.get(URL).json()
|
|
tid, up, down = _rule(base)
|
|
good = {
|
|
"종류": tid,
|
|
"만약": {"옵션": up["key"], "값들": [up["choices"][0]]},
|
|
"그러면": {"옵션": down["key"], "선택지": down["choices"][:1], "보이기": True},
|
|
}
|
|
assert definition.check({**base, "규칙": [good]}) == []
|
|
|
|
def bad(**change) -> dict:
|
|
return json.loads(json.dumps({**base, **change}))
|
|
|
|
dup = bad()
|
|
dup["types"].append(dup["types"][0]) # 종류 키 겹침
|
|
opt_dup = bad()
|
|
opt_dup["types"][0]["options"].append(opt_dup["types"][0]["options"][0]) # 옵션 키 겹침
|
|
shape = bad()
|
|
shape["types"][0]["options"][0]["input"] = "날짜" # 없는 꼴
|
|
cases = [
|
|
dup,
|
|
opt_dup,
|
|
shape,
|
|
bad(규칙=[{**good, "그러면": {**good["그러면"], "옵션": "없는키"}}]), # 대상 없음
|
|
bad(규칙=[{**good, "종류": "없는종류"}]),
|
|
bad(규칙=[{**good, "만약": {"옵션": up["key"], "값들": ["없는값"]}}]),
|
|
bad(규칙=[{**good, "그러면": {"옵션": down["key"], "선택지": ["없는값"]}}]),
|
|
bad(규칙=[{**good, "그러면": {"옵션": down["key"]}}]), # 효과 없음
|
|
bad(배치={tid: {up["key"]: {"줄": 0, "칸": 1}}}),
|
|
bad(배치={tid: {"없는키": {"줄": 1}}}),
|
|
bad(종류={tid: {"동작": ["없는동작"]}}),
|
|
bad(종류={"없는종류": {"기준측점": False}}),
|
|
bad(표시={tid: {"없는키": False}}),
|
|
bad(
|
|
그룹={"pipe": [{"id": "g1", "이름": "가", "옵션": [], "형상": {"폭": {"받기": True}}}]}
|
|
),
|
|
bad(모르는칸=1),
|
|
]
|
|
for i, doc in enumerate(cases):
|
|
res = admin.put(URL, json={"판": 1, "정의": doc})
|
|
assert res.status_code == 422, (i, res.text)
|
|
assert json.loads(copy.read_text("utf-8"))["판"] == 1 # 거절되면 안 씀
|
|
assert not (copy.parent / "구조물정의_이력").exists()
|
|
shaped = bad(종류={tid: {"기준측점": False}})
|
|
shaped["그룹"]["pipe"][0]["형상"] = {"길이": {"받기": True, "뜻": "관 길이"}}
|
|
assert admin.put(URL, json={"판": 1, "정의": shaped}).status_code == 200
|
|
|
|
|
|
def test_프로젝트_저장에_정의_판_없는_키_값은_남김(copy, tmp_path) -> None:
|
|
root = str(tmp_path / "project")
|
|
item = StructureInstance.model_validate(
|
|
{
|
|
"type_id": "mirror",
|
|
"placement": "point",
|
|
"chainage_m": 10.0,
|
|
"options": {"지난정의키": "옛값"},
|
|
}
|
|
)
|
|
assert save_structures(root, [item], base_revision=0) == 1
|
|
raw = json.loads(Path(root, "B05_Profile", "route", "structures.json").read_text("utf-8"))
|
|
assert raw["definition_version"] == 1
|
|
assert load_structures(root)[1][0].options["지난정의키"] == "옛값"
|