"""256-1 B08 거리이정표 장소 그림 영구 저장(사용자 162-6 · 7). 캡처는 가짜(make_place_image 자리)로: ① 장소 그림 페이지 = 480×320 · 고정 줌 · 경로 맞춤 안 함 ② [저장] 뒤 좌표 있는 상자(거래처 = 경로 id · 기준점 = b1)만 캡처 → place/.png · GET 답 place_images · place_times ③ 다시 [저장] = 바뀐 상자 · 그림 없는 상자만 · 실패 = 다음에 다시 · 지문에 좌표 원문 없음 ④ 지운 상자 그림 = [저장] 때 지움 · 화면 캡처 길(place-image) · 그림 파일 길(file · 경로 넘기 막음) """ import sys from pathlib import Path from uuid import uuid4 from fastapi import 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 B08_Quantity import B08_Quantity_Milestone_Capture as cap # noqa: E402 from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402 PNG = b"\x89PNG\r\n\x1a\nfake-place" DATA = { "base": {"enabled": True, "name": "거창IC", "lon": 127.91, "lat": 35.68}, "routes": [ {"id": "r1", "name": "거창레미콘", "lon": 127.9095, "lat": 35.6866}, {"id": "r2", "name": "좌표 없음"}, ], } class Fake: """가짜 캡처 — 부른 상자 기록 · ok=False 면 실패.""" def __init__(self): self.calls = [] self.ok = True def __call__(self, out_dir, place_id, mark): self.calls.append((place_id, mark["name"], mark["lon"], mark["lat"])) if not self.ok: return {"saved": False, "reason": "장소 그림 자동 캡처 실패 — 시험"} out_dir.mkdir(parents=True, exist_ok=True) (out_dir / f"{place_id}.png").write_bytes(PNG) return {"saved": True, "reason": None} def _save(root, data): return ms.save_data(root, ms.Milestone.model_validate(data)) def _fake(monkeypatch): fake = Fake() monkeypatch.setattr(ms, "make_place_image", fake) monkeypatch.setattr(ms, "route_image", lambda *a: {"saved": False}) # 경로 그림 = 256-2 시험 return fake def test_장소_그림_페이지_작은_지도(): html = cap.page_html( "k", [], [{"name": "거창레미콘", "lon": 127.9, "lat": 35.6}], cap.PLACE_VIEW ) assert "width:480px;height:320px" in html and "level: 4" in html assert "if (false)" in html # 경로 맞춤 안 함 — 표지 가운데 · 고정 줌 assert "if (true)" in cap.page_html("k", [], []) # 경로 그림은 그대로 def test_저장_뒤_좌표_있는_상자만_캡처(tmp_path, monkeypatch): fake = _fake(monkeypatch) _save(tmp_path, DATA) ms.refresh_images(tmp_path) assert sorted(c[0] for c in fake.calls) == ["b1", "r1"] got = ms.payload(tmp_path, ms.read_data(tmp_path)) assert got["place_images"] == {"b1": "place/b1.png", "r1": "place/r1.png"} assert set(got["place_times"]) == {"b1", "r1"} assert all(isinstance(t, int) and t > 0 for t in got["place_times"].values()) assert (tmp_path / "B08_Quantity/milestone/place/r1.png").read_bytes() == PNG prints = (tmp_path / "B08_Quantity/milestone/.prints.json").read_text(encoding="utf-8") assert "127.9095" not in prints and "35.6866" not in prints # 해시만 def test_바뀐_상자_그림_없는_상자만_다시(tmp_path, monkeypatch): fake = _fake(monkeypatch) _save(tmp_path, DATA) ms.refresh_images(tmp_path) fake.calls.clear() ms.refresh_images(tmp_path) assert fake.calls == [] # 안 바뀜 = 캡처 0 moved = {**DATA, "routes": [{**DATA["routes"][0], "lat": 35.7}, DATA["routes"][1]]} _save(tmp_path, moved) ms.refresh_images(tmp_path) assert [c[0] for c in fake.calls] == ["r1"] fake.calls.clear() (tmp_path / "B08_Quantity/milestone/place/b1.png").unlink() ms.refresh_images(tmp_path) assert [c[0] for c in fake.calls] == ["b1"] # 실패 = 지문 안 적음 → 다음에 다시 fake.calls.clear() fake.ok = False _save(tmp_path, {**moved, "base": {**DATA["base"], "lon": 127.95}}) ms.refresh_images(tmp_path) ms.refresh_images(tmp_path) assert [c[0] for c in fake.calls] == ["b1", "b1"] def test_지운_상자_그림은_저장_때_지움(tmp_path, monkeypatch): _fake(monkeypatch) _save(tmp_path, DATA) ms.refresh_images(tmp_path) got = _save(tmp_path, {"routes": [DATA["routes"][1]]}) # r1 · 기준점 좌표 지움 assert got["place_images"] == {} and got["place_times"] == {} assert not list((tmp_path / "B08_Quantity/milestone/place").iterdir()) def test_화면_캡처_길_그림_파일_길(tmp_path, monkeypatch): fake = _fake(monkeypatch) async def root(project_id): return tmp_path monkeypatch.setattr(ms, "_project_root", root) monkeypatch.delenv("PYTEST_CURRENT_TEST") # [저장] 뒤 캡처 켬(시험 중엔 꺼 둠) app = FastAPI() app.include_router(ms.router) client = TestClient(app) base = f"/api/projects/{uuid4()}/quantity/milestone" # [저장] = 답 뒤 캡처(BackgroundTasks — TestClient 는 답 전에 끝냄) assert client.put(base, json=DATA).status_code == 200 assert sorted(c[0] for c in fake.calls) == ["b1", "r1"] assert client.get(base).json()["place_images"]["r1"] == "place/r1.png" # 등록 · 끌기 뒤 화면이 부름 fake.calls.clear() mark = {"name": "새 거래처", "lon": 127.8, "lat": 35.5} res = client.post(base + "/place-image", params={"id": "r9"}, json=mark).json() assert res["name"] == "place/r9.png" and res["reason"] is None and res["time"] > 0 assert fake.calls == [("r9", "새 거래처", 127.8, 35.5)] fake.ok = False bad = client.post(base + "/place-image", params={"id": "r8"}, json=mark).json() assert bad == {"name": None, "time": None, "reason": "장소 그림 자동 캡처 실패 — 시험"} assert client.post(base + "/place-image", params={"id": "../x"}, json=mark).status_code == 400 # 그림 파일 길 — 하위 폴더 둘 · 경로 넘기 막음 assert client.get(base + "/file", params={"name": "place/r1.png"}).content == PNG assert client.get(base + "/file", params={"name": "place/zz.png"}).status_code == 404 for name in ("../milestone.json", "place/../x.png", "x/r1.png", "place/r1.json", "/etc/x.png"): assert client.get(base + "/file", params={"name": name}).status_code == 422, name def test_여러_기준점_상자_id(tmp_path, monkeypatch): fake = _fake(monkeypatch) bases = [ {"id": "b1", "name": "거창IC", "lon": 127.91, "lat": 35.68, "to": "b2"}, {"id": "b2", "name": "함양JC", "lon": 127.72, "lat": 35.52}, {"id": "b3", "name": "좌표 없음"}, ] _save(tmp_path, {"bases": bases, "routes": [{**DATA["routes"][0], "to": "b1"}]}) ms.refresh_images(tmp_path) assert sorted(c[0] for c in fake.calls) == ["b1", "b2", "r1"] got = _save(tmp_path, {"bases": bases[1:], "routes": []}) # b1 · r1 지움 assert got["place_images"] == {"b2": "place/b2.png"}