- 회사 · 개인 조합 편집이 M01 관리자 전용 길 없이 로직을 고르고 미리 보기 - 시험 test_m02_combo_layers 한 벌 더함(9) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
291 lines
15 KiB
Python
291 lines
15 KiB
Python
"""일위대가 조합 층(PLAN 27-2a · 27-2c) — 세 층 한 길 `/api/m02/layers/{layer}/combos`.
|
|
|
|
못박는 것
|
|
- 키 = 회사 `UC{회사}-######` · 개인 `UP{사용자}-######` · 지운 번호 다시 안 줌 · 마스터 UA.
|
|
- 권한 = 층 규칙(마스터 쓰기 시스템 관리자 · 회사 쓰기 회사 관리자 · 개인 쓰기 본인 · 읽기 규칙).
|
|
- 검사 = 마스터와 같음(빈 조합 · 이름 겹침 · 없는 본뜬키) · 판 어긋남 409.
|
|
- 나무 = 폴더 [구분, 상세구분] 깊이 2 · 더하기 · 이름 · 옮기기 · 지우기(쓰는 조합 409).
|
|
- 마스터 나무 고침 = 줄은 M01 저장 길 · 빈 폴더는 머리 `폴더` · 틀 검사 그대로 통과.
|
|
- 고르기(use-combo) · 갈라 내기 = 세 층 조합 · 남의 층 것 403 · 갈라 낸 새 조합은 그 층에.
|
|
- 조합 이름 · 알약 · 스냅샷 원본이 회사 · 개인 조합도 읽음.
|
|
정본(마스터 · 마스터 템플릿)은 읽기만 · 쓰기는 임시 storage 와 마스터 사본.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
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 M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
|
from M02_MasterTemplete import M02_Item_Source as source
|
|
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
|
|
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
|
|
from M02_MasterTemplete import M02_MasterTemplete_Router_Combos as combos_router
|
|
from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_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
|
|
|
|
A, B = "GF000222", "GF000221" # 찰쌓기 인력 · 장비
|
|
URL = "/api/m02/layers/{}/combos"
|
|
|
|
|
|
@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)
|
|
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(items_router.router)
|
|
app.include_router(combos_router.router)
|
|
app.dependency_overrides[verify_session] = lambda: session
|
|
return {"client": TestClient(app), "session": session, "storage": storage}
|
|
|
|
|
|
def _as(world: dict, role: str = "USER", user: int = 42) -> TestClient:
|
|
world["session"].update(role=role, user_id=user)
|
|
return world["client"]
|
|
|
|
|
|
def _combo(name: str, codes=(A,), **more) -> dict:
|
|
return {"이름": name, "담은로직": [{"로직": c, "메모": None} for c in codes], **more}
|
|
|
|
|
|
def _new(client: TestClient, layer: str, combo: dict, want: int = 200, **params) -> dict:
|
|
res = client.post(URL.format(layer), json={"combo": combo}, params=params)
|
|
assert res.status_code == want, res.text
|
|
return res.json()
|
|
|
|
|
|
def _list(client: TestClient, layer: str, **params) -> dict:
|
|
res = client.get(URL.format(layer), params=params)
|
|
assert res.status_code == 200, res.text
|
|
return res.json()
|
|
|
|
|
|
def _tree(client: TestClient, layer: str, want: int = 200, **body) -> dict:
|
|
body.setdefault("판", _list(client, layer)["판"])
|
|
res = client.post(URL.format(layer) + "/tree", json=body)
|
|
assert res.status_code == want, res.text
|
|
return res.json()
|
|
|
|
|
|
# ── 키 · 자리 ─────────────────────────────────────────────────────────
|
|
def test_회사_개인_키_장부_지운_번호는_다시_안_줌(world):
|
|
boss = _as(world, "ADMIN")
|
|
one = _new(boss, "company", _combo("회사 하나"))
|
|
assert one["키"] == "UC7-000001" and one["combo"]["소유"] == "현장"
|
|
assert tuple(one["combo"]) == combo_store.mcb.COLS # 열 한 벌 · 마스터와 같음
|
|
file = world["storage"] / "7" / "templates" / combos.FILE
|
|
assert json.loads(file.read_text(encoding="utf-8"))["다음"] == 2
|
|
me = _as(world)
|
|
mine = _new(me, "personal", _combo("내 것", (A, B)))
|
|
assert mine["키"] == "UP42-000001"
|
|
assert (world["storage"] / "7" / "42" / "templates" / combos.FILE).is_file()
|
|
gone = me.delete(f"{URL.format('personal')}/{mine['키']}", params={"판": mine["판"]})
|
|
assert gone.status_code == 200
|
|
assert _new(me, "personal", _combo("다음 것"))["키"] == "UP42-000002"
|
|
got = me.get(f"{URL.format('personal')}/UP42-000002").json()
|
|
assert [x["로직"] for x in got["줄"]] == [A] and got["blocked"] is False
|
|
|
|
|
|
def test_권한_층_규칙(world):
|
|
user = _as(world)
|
|
_new(user, "system", _combo("마스터"), 403)
|
|
_new(user, "company", _combo("회사"), 403)
|
|
_new(user, "personal", _combo("남의 것"), 403, owner="7/43")
|
|
assert user.get(URL.format("system")).status_code == 200 # 마스터 읽기 = 누구나
|
|
assert user.get(URL.format("company")).status_code == 200 # 같은 회사 읽기
|
|
assert user.get(URL.format("personal"), params={"owner": "8/9"}).status_code == 403
|
|
_as(world, "ADMIN")
|
|
_new(user, "company", _combo("회사"))
|
|
world["session"]["company_id"] = None
|
|
assert user.post(URL.format("personal"), json={"combo": _combo("x")}).status_code == 409
|
|
|
|
|
|
def test_검사_빈_조합_이름_겹침_본뜬키_판(world):
|
|
me = _as(world)
|
|
got = _new(me, "personal", _combo("빈 것", ()), 422)
|
|
assert any("빈 조합" in x for x in got["detail"]["errors"])
|
|
first = _new(me, "personal", _combo("원본"))
|
|
assert "이미 있음" in _new(me, "personal", _combo("원본"), 422)["detail"]["errors"][0]
|
|
_new(me, "personal", _combo("엉터리 계보", 본뜬키="UP42-999999"), 400)
|
|
master_key = m01.read(combo_store.FILE)[0]["줄"][0]["키"]
|
|
branched = _new(me, "personal", _combo("마스터 본뜸", 본뜬키=master_key))
|
|
assert branched["combo"]["본뜬키"] == master_key # 층을 넘는 계보
|
|
url = f"{URL.format('personal')}/{first['키']}"
|
|
assert me.put(url, json={"combo": _combo("원본 고침"), "판": "낡은판"}).status_code == 409
|
|
now = _list(me, "personal")["판"]
|
|
res = me.put(url, json={"combo": _combo("원본 고침", (B,)), "판": now})
|
|
assert res.status_code == 200 and res.json()["combo"]["담은로직"][0]["로직"] == B
|
|
|
|
|
|
# ── 나무 ─────────────────────────────────────────────────────────────
|
|
def test_나무_폴더_깊이_2_이름_옮기기_지우기(world):
|
|
me = _as(world)
|
|
key = _new(me, "personal", _combo("돌쌓기 한 벌"))["키"]
|
|
_tree(me, "personal", op="add-folder", 경로=[], 이름="돌쌓기")
|
|
_tree(me, "personal", op="add-folder", 경로=["돌쌓기"], 이름="찰쌓기")
|
|
_tree(me, "personal", 422, op="add-folder", 경로=["돌쌓기", "찰쌓기"], 이름="셋째")
|
|
_tree(me, "personal", 409, op="add-folder", 경로=[], 이름="돌쌓기")
|
|
_tree(me, "personal", 404, op="move", 키=key, 새경로=["없음"])
|
|
got = _tree(me, "personal", op="move", 키=key, 새경로=["돌쌓기", "찰쌓기"])
|
|
row = next(r for r in got["줄"] if r["키"] == key)
|
|
assert (row["구분"], row["상세구분"]) == ("돌쌓기", "찰쌓기")
|
|
got = _tree(me, "personal", op="rename", 경로=["돌쌓기"], 이름="석축")
|
|
assert got["폴더"] == [["석축"], ["석축", "찰쌓기"]]
|
|
assert next(r for r in got["줄"] if r["키"] == key)["구분"] == "석축"
|
|
_tree(me, "personal", 422, op="move", 경로=["석축"], 새경로=["석축", "찰쌓기"])
|
|
got = _tree(me, "personal", op="move", 경로=["석축", "찰쌓기"], 새경로=[])
|
|
assert got["폴더"] == [["석축"], ["찰쌓기"]] # 빈 「석축」 은 남음
|
|
row = next(r for r in got["줄"] if r["키"] == key)
|
|
assert (row["구분"], row["상세구분"]) == ("찰쌓기", None)
|
|
_tree(me, "personal", 409, op="add-folder", 경로=[], 이름="석축", 판="낡은판")
|
|
got = _tree(me, "personal", op="delete", 경로=["찰쌓기"])
|
|
assert got["지운조합"] == [key] and got["줄"] == [] and got["폴더"] == [["석축"]]
|
|
|
|
|
|
def test_쓰는_조합이_든_폴더는_못_지움_이름을_알림(world):
|
|
me = _as(world)
|
|
key = _new(me, "personal", _combo("쓰는 것", 구분="석축"))["키"]
|
|
tree = me.post(
|
|
"/api/m02/layers/personal/tree",
|
|
json={"op": "add-item", "경로": ["옹벽"], "이름": "찰쌓기", "단위": "m", "판": _판(me)},
|
|
).json()
|
|
column = tree["알림"]["열"]
|
|
use = me.post(
|
|
f"/api/m02/layers/personal/structures/{column}/use-combo",
|
|
json={"종류": "일위대가", "키": key},
|
|
)
|
|
assert use.status_code == 200, use.text
|
|
got = _tree(me, "personal", 409, op="delete", 경로=["석축"])
|
|
assert got["detail"]["users"] == ["옹벽 찰쌓기"]
|
|
res = me.delete(f"{URL.format('personal')}/{key}", params={"판": _list(me, "personal")["판"]})
|
|
assert res.status_code == 409
|
|
assert [r["키"] for r in _list(me, "personal")["줄"]] == [key] # 그대로 남음
|
|
|
|
|
|
def _판(client: TestClient) -> str:
|
|
return client.get("/api/m02/layers/personal/tree").json()["판"]
|
|
|
|
|
|
@pytest.fixture()
|
|
def master_copy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
folder = tmp_path / "master"
|
|
folder.mkdir()
|
|
for path in m01.FOLDER.glob("*.json"):
|
|
if not path.name.startswith("_") or path.name == m01.mk.BOOK.name:
|
|
shutil.copy(path, folder / path.name)
|
|
monkeypatch.setattr(m01, "FOLDER", folder)
|
|
return folder
|
|
|
|
|
|
def test_마스터_나무는_M01_저장_길_빈_폴더는_머리_틀_검사_통과(world, master_copy):
|
|
admin = _as(world, "SYSTEM_ADMIN")
|
|
before = _list(admin, "system")
|
|
assert ["실무 일위대가"] in before["폴더"]
|
|
_tree(admin, "system", op="add-folder", 경로=[], 이름="빈 폴더")
|
|
got = _tree(admin, "system", op="rename", 경로=["실무 일위대가"], 이름="실무")
|
|
assert ["실무"] in got["폴더"] and ["빈 폴더"] in got["폴더"]
|
|
assert ["실무 일위대가"] not in got["폴더"]
|
|
doc = json.loads((master_copy / combos.FILE).read_text(encoding="utf-8"))
|
|
assert ["빈 폴더"] in doc["폴더"]
|
|
assert sum(r["구분"] == "실무" for r in doc["줄"]) == len(
|
|
[r for r in before["줄"] if r["구분"] == "실무 일위대가"]
|
|
)
|
|
assert m01.cm.check_form(combos.FILE, doc) == [] # 마스터 틀 그대로
|
|
_tree(_as(world), "system", 403, op="add-folder", 경로=[], 이름="막힘")
|
|
|
|
|
|
# ── 세 층 고르기 · 갈라 내기 · 읽는 곳 ─────────────────────────────────
|
|
def test_세_층_목록_고르기_남의_층_403_갈라_낸_것은_그_층에(world):
|
|
boss = _as(world, "ADMIN")
|
|
uc = _new(boss, "company", _combo("회사 조합", (A, B)))["키"]
|
|
me = _as(world)
|
|
up = _new(me, "personal", _combo("내 조합"))["키"]
|
|
all_ = me.get("/api/m02/combos/all").json()
|
|
assert [g["layer"] for g in all_] == ["system", "company", "personal"]
|
|
assert [r["키"] for r in all_[1]["줄"]] == [uc] and [r["키"] for r in all_[2]["줄"]] == [up]
|
|
assert combos.find(uc)["이름"] == "회사 조합" and combos.find(up)["이름"] == "내 조합"
|
|
assert combos.find("UP42-999999") is None and combos.find("엉터리") is None
|
|
|
|
column = me.post(
|
|
"/api/m02/layers/personal/tree",
|
|
json={"op": "add-item", "경로": [], "이름": "돌붙임", "단위": "m", "판": _판(me)},
|
|
).json()["알림"]["열"]
|
|
base = f"/api/m02/layers/personal/structures/{column}"
|
|
for key in (uc, up):
|
|
res = me.post(f"{base}/use-combo", json={"종류": "일위대가", "키": key})
|
|
assert res.status_code == 200 and res.json()["키"] == key
|
|
lines = [{"로직": A}, {"로직": B, "메모": "장비"}] # 이 층에 같은 내용 없음 = 새로
|
|
made = me.post(f"{base}/combo", json={"본뜬키": uc, "담은로직": lines, "이름": "회사 것 고침"})
|
|
assert made.status_code == 200, made.text
|
|
new = made.json()["키"]
|
|
assert new == "UP42-000002" and combos.find(new)["본뜬키"] == uc
|
|
folder = layers.personal_dir(7, 42)
|
|
item = next(i for i in items_store.items(folder) if i["열"] == column)
|
|
assert item["일위대가"] == new and item["names"]["일위대가"] == "회사 것 고침"
|
|
assert source.labor_rows(new)["줄"]["키"] == new # 프로젝트 스냅샷 원본도 층 조합
|
|
|
|
_as(world, "ADMIN")
|
|
company_col = boss.post(
|
|
"/api/m02/layers/company/tree",
|
|
json={
|
|
"op": "add-item",
|
|
"경로": [],
|
|
"이름": "회사 항목",
|
|
"단위": "m",
|
|
"판": boss.get("/api/m02/layers/company/tree").json()["판"],
|
|
},
|
|
).json()["알림"]["열"]
|
|
res = boss.post(
|
|
f"/api/m02/layers/company/structures/{company_col}/use-combo",
|
|
json={"종류": "일위대가", "키": up},
|
|
)
|
|
assert res.status_code == 403 # 회사 항목은 개인 조합을 못 씀
|
|
|
|
|
|
def test_면적_알약이_개인_조합을_읽음(world):
|
|
me = _as(world)
|
|
up = _new(me, "personal", _combo("찰쌓기", (A, B)))["키"]
|
|
got = me.get(f"{URL.format('personal')}/{up}/inputs").json()
|
|
assert [p["키"] for p in got["물량"]] == ["돌쌓기 찰쌓기|㎡"]
|
|
tree = me.post(
|
|
"/api/m02/layers/personal/tree",
|
|
json={"op": "add-item", "경로": [], "이름": "찰쌓기 옹벽", "단위": "m", "판": _판(me)},
|
|
).json()
|
|
column = tree["알림"]["열"]
|
|
me.post(
|
|
f"/api/m02/layers/personal/structures/{column}/use-combo",
|
|
json={"종류": "일위대가", "키": up},
|
|
)
|
|
key = next(i["key"] for i in tree["items"] if i["열"] == column)
|
|
pills = me.get(f"/api/m02/layers/personal/items/{key}/pills", params={"place": "면적"})
|
|
assert pills.status_code == 200, pills.text
|
|
assert [p["키"] for p in pills.json()["물량"]] == ["돌쌓기 찰쌓기|㎡"]
|
|
|
|
|
|
def test_담을_로직_읽기는_로그인이면_됨(world):
|
|
user = _as(world)
|
|
logics = user.get("/api/m02/logic-lib/logics").json()["logics"]
|
|
assert A in {x["키"] for x in logics}
|
|
assert user.get("/api/m02/logic-lib/logic", params={"key": A}).json()["logic"]["키"] == A
|
|
assert user.get("/api/m02/logic-lib/logic", params={"key": "GF999999"}).status_code == 404
|
|
assert user.get("/api/m02/logic-lib/subs").json()["subs"]
|
|
assert user.get("/api/m02/logic-lib/auto", params={"key": A}).status_code == 200
|