"""D 분석 API(PLAN 530-2 · 530-3) — GET 이은 누가 · PUT [저장] = 서버 저장본으로 다시 셈 · 모르는 갈래 거절 · 끈 지점(points) = 그 지점으로 보기 · 안 이어지면 거절 · [저장] 때만 지점 정본(산림청만). ./venv/Scripts/python.exe -m pytest resources/tester/test_530_2_plan_router.py -q """ import json import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from common_util.common_util_auth import require_company from common_util.common_util_json import atomic_write_json from D03_PlanAnalysis import D03_PlanAnalysis_Analyze as analyze from D03_PlanAnalysis import D03_PlanAnalysis_Router as router_mod PID = "12953f5a-5ba1-42ad-a427-8ac857a58bc2" P0, P1, P2 = (0.0, 0.0), (300.0, 400.0), (900.0, 400.0) def _route(points: list, length: float) -> dict: return {"planned": points, "nodes": [], "curves": [], "metrics": {"length_m": length}} class _Cursor: def __init__(self, log): self.log = log async def __aenter__(self): return self async def __aexit__(self, *exc): return False async def execute(self, sql, args=None): self.log.append((sql, args)) class _Connection(_Cursor): async def begin(self): pass async def commit(self): self.log.append(("COMMIT", None)) async def rollback(self): pass def cursor(self, *args): return _Cursor(self.log) class _Pool: def __init__(self, log): self.log = log def acquire(self): return _Connection(self.log) @pytest.fixture def client(tmp_path, monkeypatch): segments = [ {"year": 2026, "start": P0, "end": P1, "epsg": 5186, "route_json": None}, {"year": 2027, "start": P1, "end": P2, "epsg": 5186, "route_json": '{"mode": "gentle"}'}, ] for seg, routes in ( (segments[0], {"gradeline": _route([[0, 0], [100, 200], [300, 400]], 530.0)}), ( segments[1], { "gradeline": _route([[300, 400], [900, 400]], 600.0), "gentle": _route([[300, 400], [600, 500], [900, 400]], 640.0), "earthwork": {"error": "길 없음"}, }, ), ): atomic_write_json( analyze.derived_dir(tmp_path) / f"route_{seg['year']}.json", { "year": seg["year"], "start": list(seg["start"]), "end": list(seg["end"]), "fingerprint": "f", "routes": routes, }, ) project = { "road_type": "work", "design_speed_kph": None, "route_start_m": 1000.0, "route_end_m": 2500.0, } async def inputs(project_id, points=None): return tmp_path, project, router_mod._override(segments, points) if points else segments log: list = [] monkeypatch.setattr(router_mod, "_plan_inputs", inputs) monkeypatch.setattr(router_mod, "get_db_pool", lambda: _Pool(log)) app = FastAPI() app.include_router(router_mod.router) session = {"role": "USER", "email": "a@forest.go.kr"} app.dependency_overrides[require_company] = lambda: session stored: list = [] async def store_points(project_id, segs): stored.append([(s["year"], s["start"], s["end"]) for s in segs]) monkeypatch.setattr(router_mod, "_store_points", store_points) test = TestClient(app) test.log = log test.session = session test.stored = stored return test def test_get_chains_saved_choice(client) -> None: body = client.get(f"/api/projects/{PID}/plan/analysis").json() chain = body["chain"] assert [r["mode"] for r in chain] == ["gradeline", "gentle"] # 저장값 · 없으면 첫 성공 assert chain[0]["start_chainage_m"] == 1000.0 assert chain[1]["start_chainage_m"] == chain[0]["end_chainage_m"] == 1530.0 assert chain[1]["end_chainage_m"] == 2170.0 assert body["line"] == [[0, 0], [100, 200], [300, 400], [600, 500], [900, 400]] assert body["end_chainage_target_m"] == 2500.0 # 등록 목표 — 화면이 차이를 보임 def test_put_saves_server_routes(client) -> None: got = client.put(f"/api/projects/{PID}/plan/analysis", json={"choices": {"2027": "gradeline"}}) assert got.status_code == 200 assert got.json()["chain"][1]["end_chainage_m"] == 2130.0 updates = [args for sql, args in client.log if sql.startswith("UPDATE plan_route_years")] saved = {args[3]: json.loads(args[0]) for args in updates} assert saved[2027]["mode"] == "gradeline" and saved[2027]["planned"] == [[300, 400], [900, 400]] assert saved[2026]["start_chainage_m"] == 1000.0 and updates[0][1] == "f" assert client.log[-1][0] == "COMMIT" def test_put_rejects_unknown_or_empty_mode(client) -> None: url = f"/api/projects/{PID}/plan/analysis" assert client.put(url, json={"choices": {"2027": "follow"}}).status_code == 400 assert client.put(url, json={"choices": {"2027": "earthwork"}}).status_code == 409 assert not [sql for sql, _ in client.log if sql.startswith("UPDATE")] def _points(p1) -> list: return [ {"year": 2026, "start": list(P0), "end": list(p1)}, {"year": 2027, "start": list(p1), "end": list(P2)}, ] def test_points_override_sees_matching_store_only(client) -> None: url = f"/api/projects/{PID}/plan/analysis" same = client.get(url, params={"points": json.dumps(_points(P1))}).json() assert same["chain"] is not None and same["line"][-1] == [900, 400] moved = client.get(url, params={"points": json.dumps(_points((310.0, 400.0)))}).json() assert moved["chain"] is None and moved["years"][0]["analysis"] is None # 끈 지점 = 저장본 없음 def test_points_must_chain_and_cover_years(client) -> None: url = f"/api/projects/{PID}/plan/analysis" broken = _points(P1) broken[1]["start"] = [301.0, 400.0] assert client.get(url, params={"points": json.dumps(broken)}).status_code == 400 assert client.get(url, params={"points": json.dumps(_points(P1)[:1])}).status_code == 400 def test_put_with_points_stores_points_forest_only(client) -> None: url = f"/api/projects/{PID}/plan/analysis" body = {"choices": {"2027": "gradeline"}, "points": _points(P1)} assert client.put(url, json=body).status_code == 200 assert client.stored == [[(2026, P0, P1), (2027, P1, P2)]] assert client.put(url, json={"choices": {}}).status_code == 200 and len(client.stored) == 1 client.session["email"] = "a@gmail.com" assert client.put(url, json=body).status_code == 403 moved = {"choices": {}, "points": _points((310.0, 400.0))} client.session["email"] = "a@forest.go.kr" assert client.put(url, json=moved).status_code == 409 and len(client.stored) == 1