"""166-4 자재단가대비표 견적 열 · 적용 출처 고르기 · 견적서 여러 파일(임시 storage) · 거울.""" import sys from pathlib import Path from uuid import uuid4 import pytest ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B09_Estimation import B09_Estimation_Store as store # noqa: E402 from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402 from resources.tester.test_144_b09_base_calc import _run_ts # noqa: E402 MATERIALS = [ {"key": "MT1", "name": "자갈", "prices": {"물가정보": 23000, "물가자료": 22000}, "qty": 2}, {"key": "MT2", "name": "폭약", "prices": {"물가자료": 12000}}, {"key": "b08:번호판", "name": "국가지점번호판", "prices": {}}, {"key": "MT4", "name": "모래", "prices": {"물가자료": 30000}}, ] INPUTS = {"labor": [], "machines": [], "materials": MATERIALS} USER = { "materials": { "MT1": {"quote": 21000, "quote_page": "견적 A"}, # 견적이 최저 → 자동 "MT2": {"quote": 12712, "pick": "견적"}, # 견적이 높아도 고름 "b08:번호판": {"quote": 520000}, # 견적만 있는 줄 = 견적 자동 "MT4": {"quote": 29000, "applied": 31000, "pick": "물가자료"}, # 고름 › 손 수 } } def test_견적_최저_고름_손수(): s = compute_base(INPUTS, USER) rows = {r["key"]: r for r in s["compare"]} got = { k: (r["lowest"], r["lowest_from"], r["applied"], r["applied_from"]) for k, r in rows.items() } assert got == { "MT1": (21000, "견적", 21000, "견적"), "MT2": (12000, "물가자료", 12712, "견적"), "b08:번호판": (520000, "견적", 520000, "견적"), "MT4": (29000, "견적", 30000, "물가자료"), } assert rows["MT1"]["pages"]["견적"] == "견적 A" and rows["MT2"]["pick"] == "견적" assert {r["key"]: r["price"] for r in s["material"]}["MT2"] == 12712 # 목록표 단가 따라감 assert s["material"][0]["amount"] == 42000 # 손 수만 있으면 「사용자」 · 고름 없으면 최저(견적 포함) s2 = compute_base(INPUTS, {"materials": {"MT4": {"quote": 29000, "applied": 31000}}}) assert s2["compare"][3]["applied_from"] == "사용자" def test_거울_TS(): assert _run_ts(INPUTS, USER) == compute_base(INPUTS, USER) def test_저장_다듬기_파일_목록(tmp_path): raw = {"materials": {"MT2": {"quote": 12712, "quote_page": " 견적 B ", "pick": "없는 출처", "files": ["x"]}, "MT1": {"pick": "견적"}}} # fmt: skip clean = store.write_user(tmp_path, raw) assert clean["materials"] == { "MT2": {"quote": 12712, "quote_page": "견적 B"}, "MT1": {"pick": "견적"}, } # files 는 저장 안 함(폴더가 목록) folder = store.quote_dir(tmp_path, "b08:이름|㎥/hr") folder.mkdir(parents=True) (folder / "a.pdf").write_bytes(b"1") (folder / ".a.pdf.123.upload").write_bytes(b"temp") assert folder.parent == store.folder(tmp_path) / "quotes" user = store.read_user(tmp_path) assert user["materials"]["b08:이름|㎥/hr"] == {"files": ["a.pdf"]} assert store.safe_name("../../x\\y:z.pdf") == "y_z.pdf" and store.safe_name("..") == "" with pytest.raises(ValueError): store.quote_dir(tmp_path, "..") def test_올리기_목록_내려받기_지우기(tmp_path, monkeypatch): from fastapi import FastAPI from fastapi.testclient import TestClient from B09_Estimation import B09_Estimation_Router_Quotes as quotes async def root(_project_id): return tmp_path monkeypatch.setattr(quotes, "_project_root", root) app = FastAPI() app.include_router(quotes.router, prefix="/api/projects") client = TestClient(app) base = f"/api/projects/{uuid4()}/estimation/quotes" key = {"key": "b08:pipe_600"} files = [("files", ("견적1.pdf", b"one", "application/pdf")), ("files", ("sub/견적2.xlsx", b"two!", "application/octet-stream"))] # fmt: skip listed = client.post(base, params=key, files=files).json() assert [(f["name"], f["size"]) for f in listed] == [("견적1.pdf", 3), ("견적2.xlsx", 4)] assert (store.quote_dir(tmp_path, "b08:pipe_600") / "견적1.pdf").read_bytes() == b"one" assert len(client.get(base, params=key).json()) == 2 got = client.get(base + "/file", params={**key, "name": "견적2.xlsx"}) assert got.status_code == 200 and got.content == b"two!" assert client.get(base + "/file", params={**key, "name": "../x"}).status_code == 404 assert client.get(base, params={"key": ".."}).status_code == 400 left = client.delete(base, params={**key, "name": "견적1.pdf"}).json() assert [f["name"] for f in left] == ["견적2.xlsx"] assert store.read_user(tmp_path)["materials"] == {"b08:pipe_600": {"files": ["견적2.xlsx"]}}