Files
Aislo/resources/tester/test_m02_combo_layers.py
T
eomsangdonandClaude Opus 5.5 c7cbef86a0 feat(826-2): M01 기계 = 한 대 한 파일 · M01 저장 길 줄이기
- 기계.json 한 파일 → 폴더 기계/(EQ 키마다 파일 + _머리.json · 차례 = 키 차례) · 옮긴 빌더 build_기계폴더_826_2 (655 대 머리 · 줄 · 차례 대조 뒤 옛 파일 지움)
- 읽기 도우미 master_machine_files — cm.load · Store_Cache · Store.read 가 옛 「기계.json」 한 표 꼴로 지어 줌(API · 화면 · 색인 그대로) · 쓰기 = 바뀐 대 파일만
- 기계.json 직접 열던 곳 도우미로: M02_San_Basis_Index · M02_Logic_Standard 지문 · build_용도 · master_keys.adopt · _틀 · _갱신절차
- 저장 몸 M01_MasterData_Store_Save — 전 = 기억 · 후 = 고친 파일만 얕은 사본 · 틀은 고친 줄만 · 조합 · 일위대가 문서는 로직 줄이 빠질 때만 · 재료 찾기 기억 이어받음 · 저장 뒤 쓴 파일만 기억에 갈아 끼움
- 마스터 사본 시험 22 개 = 기계 폴더도 복사 · 821_2 · 815_2 글 대조 = 대 파일 하나 대조(옛 한 파일 글 대조 낡음)
- 새 시험 test_826_2 — 폴더 = 한 표 · 한 대 한 파일 · 새 · 지움 · 409 · 옛 길과 같은 오류 스무 견본

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 19:45:00 +09:00

204 lines
10 KiB
Python

"""일위대가 조합 층(PLAN 27-2a · 27-2c) — 세 층 한 길 `/api/m02/layers/{layer}/combos`.
못박는 것
- 키 = 회사 `UC{회사}-######` · 개인 `UP{사용자}-######` · 지운 번호 다시 안 줌 · 마스터 UA.
- 권한 = 층 규칙(마스터 쓰기 시스템 관리자 · 회사 쓰기 회사 관리자 · 개인 쓰기 본인 · 읽기 규칙).
- 검사 = 마스터와 같음(빈 조합 · 이름 겹침 · 없는 본뜬키) · 판 어긋남 409.
- 나무 고침 · 층 사이 붙여넣기 길은 없음(PLAN 39-2 · 일위대가는 구조물 열마다).
- 만들기 · 고치기 · 지우기 · 알약 · 고르기(use-combo) · 갈라 내기 길은 은퇴(39-1d) — 만들기 ·
지우기는 층 문(`_root`) + 저장소 함수로 봄.
- 세 층 목록 · 키 하나 읽기 · 조합 이름 · 알약(저장소) · 스냅샷 원본이 회사 · 개인 조합도 읽음.
- 조합 목록 · 키 하나 · 통째 지우기 서버 길은 없앰(732-1) — 읽기는 저장소 함수로 봄.
정본(마스터 · 마스터 템플릿)은 읽기만 · 쓰기는 임시 storage 와 마스터 사본.
"""
from __future__ import annotations
import json
import shutil
from pathlib import Path
from typing import Any
import pytest
from fastapi import FastAPI, HTTPException
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_MasterTemplete_Combo_Store as combos
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:
"""만들기 길은 은퇴(39-1d) — 층 문(`_root` · 권한 · 회사 없음 409) + 저장소 `create`."""
session = client.app.dependency_overrides[verify_session]()
try:
folder = items_router._root(session, layer, params.get("owner"), write=True)
with store.at(folder):
got = combos.create(combo, None)
except HTTPException as error:
code, detail = error.status_code, error.detail
except store.StoreError as error:
code, detail = error.status, error.detail
else:
code, detail = 200, None
assert code == want, detail
return got if code == 200 else {"detail": detail}
# ── 키 · 자리 ─────────────────────────────────────────────────────────
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()
with store.at(layers.personal_dir(7, 42)): # 지우기 길은 은퇴(39-1d) — 저장소 함수로
combos.delete(mine["키"], mine["판"])
assert _new(me, "personal", _combo("다음 것"))["키"] == "UP42-000002"
with store.at(layers.personal_dir(7, 42)):
got = combos.one("UP42-000002")
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")
_as(world, "ADMIN")
_new(user, "company", _combo("회사"))
world["session"]["company_id"] = None
_new(user, "personal", _combo("x"), 409)
def test_검사_빈_조합_이름_겹침_본뜬키_판(world):
me = _as(world)
got = _new(me, "personal", _combo("빈 것", ()), 422)
assert any("빈 조합" in x for x in got["detail"]["errors"])
_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 # 층을 넘는 계보
def test_조합_쓰기_알약_길은_은퇴(world):
"""39-1d — 만들기 · 고치기 · 지우기 · 알약 길 없음(조합 서버 길 자체가 없음 · 732-1)."""
me = _as(world, "SYSTEM_ADMIN")
key = _new(me, "personal", _combo("원본"))["키"]
url = f"{URL.format('personal')}/{key}"
assert me.post(URL.format("personal"), json={"combo": _combo("새")}).status_code in (404, 405)
assert me.put(url, json={"combo": _combo("고침")}).status_code in (404, 405)
assert me.delete(url, params={"판": ""}).status_code in (404, 405)
assert me.get(url).status_code in (404, 405)
assert me.get(f"/api/m02/combos/key/{key}/inputs").status_code == 404
# ── 마스터 사본 ───────────────────────────────────────────────────────
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)
shutil.copytree(m01.FOLDER / "기계", folder / "기계") # 기계 = 폴더(826-2)
own = folder / combos.FILE # 정본 머리의 빈 폴더 목록(사용자 화면 편집)은 안 물려받음
data = json.loads(own.read_text(encoding="utf-8"))
data.pop("폴더", None)
own.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
monkeypatch.setattr(m01, "FOLDER", folder)
return folder
# ── 세 층 고르기 · 갈라 내기 · 읽는 곳 ─────────────────────────────────
def test_세_층_조합_찾기_갈라_낸_것은_그_층에(world):
boss = _as(world, "ADMIN")
uc = _new(boss, "company", _combo("회사 조합", (A, B)))["키"]
me = _as(world)
up = _new(me, "personal", _combo("내 조합"))["키"]
assert me.get("/api/m02/combos/all").status_code == 404 # 세 층 고르기 길 은퇴(39-1d)
assert combos.find(uc)["이름"] == "회사 조합" and combos.find(up)["이름"] == "내 조합"
assert combos.find("UP42-999999") is None and combos.find("엉터리") is None
assert combos.where(up) == ("personal", "7/42") and combos.where(uc) == ("company", "7")
assert me.get(f"/api/m02/combos/key/{up}").status_code == 404 # 키 하나 읽기 길 없앰(732-1)
# 고르기(use-combo) · 갈라 내기 길은 은퇴(39-1d) — 열마다 labor 문서
base = "/api/m02/layers/personal/structures/c1"
assert me.post(f"{base}/use-combo", json={"종류": "일위대가", "키": up}).status_code == 404
assert me.post(f"{base}/combo", json={"본뜬키": uc, "담은로직": []}).status_code == 404
def test_면적_알약이_개인_조합을_읽음(world):
me = _as(world)
up = _new(me, "personal", _combo("찰쌓기", (A, B)))["키"]
with store.at(layers.personal_dir(7, 42)): # 알약 길은 은퇴(39-1d) — 저장소 함수로
got = combos.pills(up)
assert [p["키"] for p in got["물량"]] == ["돌쌓기 찰쌓기|㎡"]
# 항목 알약 길(`/items/{key}/pills`)은 선택 입력과 같이 없앰(39-7b)
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
# ── 나무 길 없음(PLAN 39-2) ────────────────────────────────────────────
def test_나무_고침_붙여넣기_길은_없음(world):
"""PLAN 39-2 — 일위대가 조합 나무 길(`/tree` · `/tree-many` · `/paste`)을 지움."""
admin = _as(world, "SYSTEM_ADMIN")
for tail in ("/tree", "/tree-many", "/paste"):
res = admin.post(URL.format("personal") + tail, json={"판": ""})
assert res.status_code in (404, 405), tail