"""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 # 격자 없음 = 그대로