fix(511-B /calc 경로 막기): 계산 창구가 브라우저 파일 경로를 안 씀 — 단계 격자 = {model_id · smooth · design} → 서버가 그 프로젝트 storage 안 DTM 격자 경로로 갈음(dtm_grid_npz · read_surface_grid 와 한 벌 · storage 밖 = 거절) · 경로 칸(stages.grid.path · stations.grid) = 400 · 창구 묶음 안 파일 읽기 = gridAt 하나 · 시험 test_511_b_calc_path_guard — 요구 320-4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""PLAN 511-B (요구 320-4) — 계산 창구 `/calc` 가 브라우저 파일 경로를 안 씀.
|
||||
|
||||
못박는 것
|
||||
- 단계 격자 경로 칸(`stages.grid.path`) · 측점 격자 칸(`stations.grid`) = 400 · Node 안 부름.
|
||||
- 격자 = `{model_id, smooth, design}` → 서버가 그 프로젝트 storage 안 DTM 격자 경로로 갈음.
|
||||
- `dtm_grid_npz` — 다른 프로젝트 모델 · storage 밖 상대 경로(../) · 절대 경로 = LookupError.
|
||||
- 공유 DB 안 씀(가짜 풀 · 가짜 연결) · 실제 storage 안 씀(tmp_path).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_511_b_calc_path_guard.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B05_Profile import B05_Profile_Router_Terrain as terrain # noqa: E402
|
||||
from B06_Section import B06_Section_Router_Calc as calc_router # noqa: E402
|
||||
from common_util import common_util_auth as auth # noqa: E402
|
||||
from common_util import common_util_surface_sampler as sampler # noqa: E402
|
||||
|
||||
PROJECT = "11111111-2222-3333-4444-555555555555"
|
||||
OTHER = "99999999-2222-3333-4444-555555555555"
|
||||
|
||||
|
||||
class _Cursor:
|
||||
rows: dict = {}
|
||||
|
||||
async def execute(self, sql, args=None):
|
||||
self.args = args
|
||||
|
||||
async def fetchone(self):
|
||||
if self.args == (PROJECT,):
|
||||
return (1,) # 권한 — 회사 1
|
||||
return self.rows.get(self.args)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
|
||||
class _Conn(_Cursor):
|
||||
def cursor(self):
|
||||
return _Cursor()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
|
||||
class _Pool:
|
||||
def acquire(self):
|
||||
return _Conn()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def storage(tmp_path, monkeypatch):
|
||||
"""프로젝트 둘의 storage(tmp) — 모델 행 = (id, project) → (종류, 상대 경로)."""
|
||||
roots = {PROJECT: tmp_path / "p1", OTHER: tmp_path / "p2"}
|
||||
for root in roots.values():
|
||||
(root / "models").mkdir(parents=True)
|
||||
(roots[PROJECT] / "models" / "dtm_las.npz").write_bytes(b"x")
|
||||
(roots[OTHER] / "models" / "dtm_las.npz").write_bytes(b"x")
|
||||
|
||||
async def stored(_conn, project_id):
|
||||
return str(project_id)
|
||||
|
||||
monkeypatch.setattr(terrain, "get_project_storage_relative_path", stored)
|
||||
monkeypatch.setattr(terrain, "resolve_stored_project_path", lambda pid: str(roots[pid]))
|
||||
_Cursor.rows = {
|
||||
(7, PROJECT): ("dtm", "models/dtm_las.npz"),
|
||||
(8, OTHER): ("dtm", "models/dtm_las.npz"),
|
||||
(9, PROJECT): ("dtm", "../p2/models/dtm_las.npz"),
|
||||
(10, PROJECT): ("dtm", str(roots[OTHER] / "models" / "dtm_las.npz")),
|
||||
}
|
||||
return roots
|
||||
|
||||
|
||||
def _npz(model_id: int, project: str = PROJECT) -> Path:
|
||||
from uuid import UUID
|
||||
|
||||
return asyncio.run(terrain.dtm_grid_npz(_Conn(), UUID(project), model_id, False))
|
||||
|
||||
|
||||
def test_dtm_grid_npz_storage_안만(storage):
|
||||
assert _npz(7) == (storage[PROJECT] / "models" / "dtm_las.npz").resolve()
|
||||
for model_id in (8, 9, 10): # 다른 프로젝트 모델 · ../ · 절대 경로
|
||||
with pytest.raises(LookupError):
|
||||
_npz(model_id)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(monkeypatch, storage):
|
||||
monkeypatch.setattr(auth, "get_db_pool", lambda: _Pool())
|
||||
monkeypatch.setattr(calc_router, "get_db_pool", lambda: _Pool())
|
||||
monkeypatch.setattr(sampler, "dtm_grid_file", lambda npz: Path(npz).with_suffix(".bin"))
|
||||
seen: list = []
|
||||
|
||||
def fake_run(run, inputs):
|
||||
seen.append(inputs)
|
||||
return {"results": {}, "ms": 0}
|
||||
|
||||
monkeypatch.setattr(calc_router, "run_calc_served", fake_run)
|
||||
app = FastAPI()
|
||||
app.include_router(
|
||||
calc_router.router,
|
||||
dependencies=[Depends(auth.verify_session), Depends(auth.require_project_access)],
|
||||
)
|
||||
app.dependency_overrides[auth.verify_session] = lambda: {
|
||||
"role": "USER",
|
||||
"company_id": 1,
|
||||
"user_id": 7,
|
||||
}
|
||||
return TestClient(app), seen
|
||||
|
||||
|
||||
def _post(http, inputs, run=("stage.route",)):
|
||||
return http.post(f"/api/projects/{PROJECT}/calc", json={"run": list(run), "inputs": inputs})
|
||||
|
||||
|
||||
def test_경로_칸_거절(client, storage):
|
||||
http, seen = client
|
||||
other = str(storage[OTHER] / "models" / "dtm_las.bin")
|
||||
bad = [
|
||||
{"stages": {"grid": {"path": other, "design": "las"}}},
|
||||
{"stages": {"grid": {"path": "../../p2/models/dtm_las.bin"}}},
|
||||
{"stages": {"grid": {"model_id": 7, "path": other}}},
|
||||
{"stages": {"grid": "C:/Windows/win.ini"}},
|
||||
{"stages": {"grid": {"model_id": "7"}}},
|
||||
{"stages": {"grid": {"model_id": 7, "design": "../x"}}},
|
||||
{"stations": {"grid": other, "line": []}},
|
||||
]
|
||||
for inputs in bad:
|
||||
res = _post(http, inputs, ("stations.list",) if "stations" in inputs else ("stage.route",))
|
||||
assert res.status_code == 400, inputs
|
||||
for model_id in (8, 9, 10): # 다른 프로젝트 · ../ · 절대 경로 모델 = 거절
|
||||
assert _post(http, {"stages": {"grid": {"model_id": model_id}}}).status_code == 400
|
||||
assert seen == [] # Node 안 부름
|
||||
|
||||
|
||||
def test_격자_서버가_정함(client, storage):
|
||||
http, seen = client
|
||||
res = _post(http, {"stages": {"grid": {"model_id": 7, "smooth": False, "design": "sheet"}}})
|
||||
assert res.status_code == 200, res.text
|
||||
want = (storage[PROJECT] / "models" / "dtm_las.bin").resolve()
|
||||
assert seen[-1]["stages"]["grid"] == {"path": str(want), "design": "sheet"}
|
||||
assert _post(http, {"stages": {"grid": None}}).status_code == 200 # 격자 없음 = 그대로
|
||||
Reference in New Issue
Block a user