fix(388-2 보안): [확정] 면적 · 관 길이 = 서버 셈만 · 재계산 실패 경고
- 화면 [저장] · [확정]이 면적 · 관 길이 9칸을 안 보냄 - 서버가 받아도 _AREA_KEYS 걷음(옛 화면 · 위조 값 정본에 안 남음) - 서버 재계산 실패(상세 못 받음 · Node 실패)를 삼키지 않음 → 응답 recomputed:false - 화면 경고 한 줄(저장은 됨 · 옛 서버 값 남음 · 다시 [저장]) - 새 test_384_s_area_keys · 옛 시험 셋 새 동작에 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
"""388-2 [저장] · [확정] 면적 · 관 길이 = 서버 셈만(요구 242 · 분석 1장 S3).
|
||||
|
||||
- 브라우저가 면적 · 관 길이 9칸(`_AREA_KEYS`)을 보내도 정본에 안 적음 · 다른 칸은 그대로
|
||||
- 서버 재계산 실패를 삼키지 않음 — 상세 못 받음 · Node 실패 = 던짐 → 응답 `recomputed: false`
|
||||
- 화면은 면적 · 관 길이를 안 보냄 · `recomputed: false` 면 경고 한 줄
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
import B06_Section.B06_Section_Router_Confirm as confirm
|
||||
import B06_Section.B06_Section_Server_Calc_Prebuild as prebuild
|
||||
from B06_Section.B06_Section_Schema import SectionConfirmRequest, SectionConfirmResponse
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_면적_보내도_정본에_안_적음(monkeypatch):
|
||||
written: list = []
|
||||
|
||||
async def merge(connection, *, route_id, entries, replace, project_id=None):
|
||||
written.append((replace, entries))
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(confirm, "merge_cross_section_designs", merge)
|
||||
forged = {key: 999.0 for key in prebuild._AREA_KEYS}
|
||||
request = SectionConfirmRequest(
|
||||
cross_patches=[{"chainage_m": 20.0, "ground_type": "rock", **forged}]
|
||||
)
|
||||
asyncio.run(confirm._apply_section_edits(None, 3, request, []))
|
||||
patches = written[-1][1]
|
||||
assert patches == [(20.0, {"ground_type": "rock"})]
|
||||
|
||||
|
||||
def test_면적만_보낸_측점은_patch_없음(monkeypatch):
|
||||
written: list = []
|
||||
|
||||
async def merge(connection, *, route_id, entries, replace, project_id=None):
|
||||
written.append(entries)
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(confirm, "merge_cross_section_designs", merge)
|
||||
request = SectionConfirmRequest(cross_patches=[{"chainage_m": 40.0, "cut_area_m2": 12.5}])
|
||||
asyncio.run(confirm._apply_section_edits(None, 3, request, []))
|
||||
assert written[-1] == []
|
||||
|
||||
|
||||
def test_재계산_실패는_False(monkeypatch):
|
||||
async def boom(project_id, route_id):
|
||||
raise RuntimeError("Node 실패")
|
||||
|
||||
async def ok(project_id, route_id):
|
||||
return 1
|
||||
|
||||
monkeypatch.setattr(prebuild, "recompute_server_side", boom)
|
||||
assert asyncio.run(confirm._recompute_stored_designs(uuid4(), 3)) is False
|
||||
monkeypatch.setattr(prebuild, "recompute_server_side", ok)
|
||||
assert asyncio.run(confirm._recompute_stored_designs(uuid4(), 3)) is True
|
||||
|
||||
|
||||
def test_상세_못_받으면_던짐(monkeypatch):
|
||||
import B06_Section.B06_Section_Router as b06_router
|
||||
|
||||
async def no_detail(project_id, route_id):
|
||||
return SimpleNamespace() # model_dump 없음 = JSONResponse 실패
|
||||
|
||||
async def run_with(fn, *args):
|
||||
return None
|
||||
|
||||
async def nothing(project_id):
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr(b06_router, "get_section_detail", no_detail)
|
||||
monkeypatch.setattr(prebuild, "run_with_connection", run_with)
|
||||
monkeypatch.setattr(prebuild, "haul_inputs_for", nothing)
|
||||
with pytest.raises(RuntimeError):
|
||||
asyncio.run(prebuild.recompute_server_side(uuid4(), 3))
|
||||
|
||||
|
||||
def test_Node_실패면_던짐(monkeypatch, tmp_path):
|
||||
import B06_Section.B06_Section_Router as b06_router
|
||||
import B06_Section.B06_Section_Router_Design as b06_design
|
||||
|
||||
detail = {"cross_sections": [{"chainage_m": 0.0, "design": {}}], "longitudinal": {}}
|
||||
|
||||
async def got(project_id, route_id):
|
||||
return SimpleNamespace(model_dump=lambda mode: detail)
|
||||
|
||||
async def run_with(fn, *args):
|
||||
return "p" if fn is prebuild.get_project_storage_relative_path else {}
|
||||
|
||||
async def nothing(project_id):
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr(b06_router, "get_section_detail", got)
|
||||
monkeypatch.setattr(prebuild, "run_with_connection", run_with)
|
||||
monkeypatch.setattr(prebuild, "haul_inputs_for", nothing)
|
||||
monkeypatch.setattr(prebuild, "resolve_stored_project_path", lambda path: str(tmp_path))
|
||||
monkeypatch.setattr(b06_design, "enforce_design_lines", lambda *a: None)
|
||||
monkeypatch.setattr(prebuild, "_enforce_stored_designs", lambda *a: None)
|
||||
monkeypatch.setattr(prebuild, "quantity_settings", lambda root: {})
|
||||
monkeypatch.setattr(prebuild, "mixed_conversion_factors", lambda settings: {})
|
||||
monkeypatch.setattr(prebuild, "haul_equipment_limits", lambda settings: [])
|
||||
monkeypatch.setattr(prebuild, "_node_output", lambda node_input: None)
|
||||
with pytest.raises(RuntimeError):
|
||||
asyncio.run(prebuild.recompute_server_side(uuid4(), 3))
|
||||
|
||||
|
||||
def test_응답에_recomputed():
|
||||
assert SectionConfirmResponse(project_id="p", route_id=1).recomputed is True
|
||||
assert SectionConfirmResponse(project_id="p", route_id=1, recomputed=False).recomputed is False
|
||||
|
||||
|
||||
def test_화면은_면적_안_보내고_경고():
|
||||
persist = (ROOT / "B06_Section/B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
|
||||
assert "STRUCTURE_ROW_KEYS" not in persist
|
||||
api = (ROOT / "B06_Section/B06_Section_Api_Fetch.ts").read_text(encoding="utf-8")
|
||||
assert api.count("return warnUnrecomputed(") == 2
|
||||
assert "result.recomputed === false" in api
|
||||
@@ -63,8 +63,9 @@ def _wire(monkeypatch: pytest.MonkeyPatch, latest: int) -> list[str]:
|
||||
async def _edits(*_: Any, **__: Any) -> None:
|
||||
called.append("edits")
|
||||
|
||||
async def _recompute(*_: Any, **__: Any) -> None:
|
||||
async def _recompute(*_: Any, **__: Any) -> bool:
|
||||
called.append("recompute")
|
||||
return True # 388-2 — 재계산 됐나(응답 recomputed)
|
||||
|
||||
monkeypatch.setattr(confirm, "get_db_pool", lambda: _Pool())
|
||||
monkeypatch.setattr(
|
||||
|
||||
@@ -99,7 +99,7 @@ def api(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(sections, "confirm_route_status", rec.fake("confirm_route"))
|
||||
monkeypatch.setattr(sections, "complete_stage", rec.fake("complete_stage"))
|
||||
monkeypatch.setattr(sections, "start_stage", rec.fake("start_stage"))
|
||||
monkeypatch.setattr(sections, "_recompute_stored_designs", rec.fake("recompute"))
|
||||
monkeypatch.setattr(sections, "_recompute_stored_designs", rec.fake("recompute", True))
|
||||
# 272 — 측점을 다시 맞춘 뒤 정본에 없는 측점의 횡단 행 지우기
|
||||
monkeypatch.setattr(route_confirm, "sync_cross_section_rows", rec.fake("sync_rows", 0))
|
||||
monkeypatch.setattr(
|
||||
|
||||
@@ -45,7 +45,8 @@ def test_같은_길로_정본에_실린다():
|
||||
block = source[source.index("STRUCTURE_ROW_KEYS = [") :][:300]
|
||||
assert '"pipe_length_m"' in block, "관 길이가 정본에 얹는 키 목록에 없다"
|
||||
assert '"cut_area_m2"' in block, "면적과 같은 목록이어야 한다(길이 하나만 따로 나르지 않는다)"
|
||||
assert "STRUCTURE_ROW_KEYS" in _PERSIST.read_text(encoding="utf-8")
|
||||
# 388-2 — 화면 [저장]은 면적 · 관 길이를 안 보냄(서버 셈만)
|
||||
assert "STRUCTURE_ROW_KEYS" not in _PERSIST.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_면적을_못_내는_측점에서도_길이는_낸다():
|
||||
@@ -65,7 +66,6 @@ def test_이름이_한_벌이다():
|
||||
assert "pipe_length_m" in path.read_text(encoding="utf-8"), path.name
|
||||
persist = _PERSIST.read_text(encoding="utf-8")
|
||||
assert "pipe_length_m" not in persist, "이름을 또 적으면 갈릴 자리가 하나 더 생긴다"
|
||||
assert "STRUCTURE_ROW_KEYS" in persist
|
||||
|
||||
|
||||
def test_서버_화이트리스트가_TS_목록과_짝이다():
|
||||
|
||||
@@ -78,7 +78,7 @@ def _run_save(monkeypatch: pytest.MonkeyPatch, missing: list[float]) -> list[flo
|
||||
|
||||
monkeypatch.setattr(confirm, "_compute_default_designs", _spy)
|
||||
monkeypatch.setattr(confirm, "_apply_section_edits", _async(None))
|
||||
monkeypatch.setattr(confirm, "_recompute_stored_designs", _async(None))
|
||||
monkeypatch.setattr(confirm, "_recompute_stored_designs", _async(True)) # 388-2 recomputed
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
Reference in New Issue
Block a user