158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
"""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 # 격자 없음 = 그대로
|