"""프리셋이 저장 경로를 온전히 통과하는가 — 서버 검증기 → DB → 엣지 파서. WHY THIS TEST EXISTS 1.2.0 에서 프리셋 6종을 넣으면서 두 개가 **완전히 무동작**이었다: · 출입 금지(max_workers: 0) — ZoneConfigController 의 규칙 경계가 [1,50] 이라 저장이 400 · 고소작업(elevated: true) — 서버 경계에 키가 없어 400, 설령 통과해도 엣지 파서가 침묵 삭제 감지기·입력란·직렬화·표시를 다 고쳤는데도 그랬다. 고친 네 곳이 전부 **허용 목록보다 하류**였기 때문이다 — 값이 존재해도 되는지 결정하는 층은 손대지 않았다. 단위 테스트는 Zone 을 직접 만들어 검사했으므로 이 구멍을 볼 수 없었다. 그래서 카탈로그가 선언한 규칙을 **실제 저장 경로 그대로** 흘려보내는 이 테스트가 필요하다. 프리셋이 약속한 것을 시스템이 하지 않는 것은 이 제품에서 가장 나쁜 종류의 결함이다. """ import json import re import subprocess from pathlib import Path import pytest from edge.zones import _rules_of ROOT = Path(__file__).resolve().parents[1] CATALOG = ROOT / "server" / "src" / "Api" / "ZonePresetCatalog.php" def _presets() -> list[dict]: """PHP 카탈로그를 그대로 읽어 온다 — 파이썬에 값을 다시 적으면 그 사본이 또 어긋난다.""" out = subprocess.run( ["php", "-r", 'require "server/vendor/autoload.php";' 'echo json_encode(App\Api\ZonePresetCatalog::all());'], cwd=ROOT, capture_output=True, text=True, timeout=60, ) assert out.returncode == 0, out.stderr return json.loads(out.stdout) def _server_accepts(rules: dict) -> tuple[str | None, bool]: """ZoneConfigController::rulesJson 을 그대로 호출한다 (저장 경로의 1차 관문).""" php = ( 'require "server/vendor/autoload.php";' '$m = new ReflectionMethod("App\\Dashboard\\ZoneConfigController", "rulesJson");' '$m->setAccessible(true);' 'echo json_encode($m->invoke(null, json_decode($argv[1], true)));' ) out = subprocess.run(["php", "-r", php, "--", json.dumps(rules)], cwd=ROOT, capture_output=True, text=True, timeout=60) assert out.returncode == 0, out.stderr stored, ok = json.loads(out.stdout) return stored, ok php_missing = subprocess.run(["php", "-v"], capture_output=True).returncode != 0 pytestmark = pytest.mark.skipif(php_missing, reason="php not available") @pytest.mark.parametrize("preset", _presets() if not php_missing else [], ids=lambda p: p["id"]) def test_preset_survives_server_validation_and_edge_parsing(preset): rules = preset["rules"] stored, ok = _server_accepts(rules) assert ok, f"서버가 프리셋 '{preset['id']}' 의 규칙을 거부했습니다: {rules}" assert stored is not None, f"프리셋 '{preset['id']}' 이 규칙 없이 저장됩니다" parsed = _rules_of({"rules_json": stored}) assert parsed is not None, f"엣지가 프리셋 '{preset['id']}' 의 규칙을 통째로 버렸습니다" for key, want in rules.items(): assert key in parsed, ( f"프리셋 '{preset['id']}' 의 '{key}' 가 엣지까지 오지 못했습니다 — " f"프리셋이 약속한 것을 시스템이 하지 않습니다. 저장본: {stored}, 파싱: {parsed}") assert parsed[key] == want, f"{key}: {parsed[key]!r} != {want!r}" def test_catalog_and_ui_agree_on_the_capacity_floor(): # 입력란 하한이 1 이면 운영자가 0 을 넣을 방법이 없다 — 화면도 허용 목록의 일부다. view = (ROOT / "server" / "src" / "View" / "zones.php").read_text(encoding="utf-8") m = re.search(r'id="rule-max-workers"[^>]*min="(\d+)"', view) assert m, "최대 인원 입력란을 찾지 못했습니다" assert m.group(1) == "0", "출입 금지 구역을 입력할 수 없습니다 (min 이 0 이어야 합니다)"