"""eid.tools.dispatch 단위 테스트 — 고정 enum · 동적해석 0 · egress 잠금 (stdlib only). 실행: python3 tests/eid/test_dispatch.py (또는 pytest) """ from __future__ import annotations import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "app")) from eid.tools.dispatch import ( # noqa: E402 ALLOWED_ACTIONS, _FORBIDDEN_EGRESS_VERBS, EidAction, _HANDLERS, dispatch, register_handler, ) def _reset_handlers(): _HANDLERS.clear() def test_unknown_action_rejected(): _reset_handlers() r = dispatch("frobnicate") assert r.ok is False assert "unknown" in r.reason.lower() or "화이트리스트" in r.reason def test_no_egress_verb_in_enum(): # 이중 보증: 화이트리스트 ∩ egress verb = 0 assert ALLOWED_ACTIONS.isdisjoint(_FORBIDDEN_EGRESS_VERBS) def test_egress_verb_dispatch_rejected(): _reset_handlers() for verb in ("send_smtp_email", "create_caldav_todo", "call_fallback", "httpx"): r = dispatch(verb) assert r.ok is False, f"egress verb {verb} 가 통과됨" def test_external_approval_immediate_reject_no_enqueue(): _reset_handlers() r = dispatch("request_external_approval", {"to": "x@y.com", "body": "..."}) assert r.ok is False assert "거부" in r.reason or "권한 0" in r.reason # Phase1 즉시거부 def test_external_approval_handler_cannot_register(): raised = False try: register_handler(EidAction.REQUEST_EXTERNAL_APPROVAL, lambda a: None) except ValueError: raised = True assert raised, "request_external_approval 핸들러 등록이 허용됨(즉시거부 위반)" def test_registered_handler_runs(): _reset_handlers() register_handler(EidAction.READ_DOCUMENTS, lambda a: {"rows": 3, "echo": a}) r = dispatch("read_documents", {"q": "vessel"}) assert r.ok is True assert r.data == {"rows": 3, "echo": {"q": "vessel"}} def test_unregistered_known_action_rejected(): _reset_handlers() # 화이트리스트엔 있으나 핸들러 미등록(W3 이전) → reject (동적 해석으로 새지 않음) r = dispatch("read_events") assert r.ok is False assert "미등록" in r.reason or "handler" in r.reason.lower() def test_handler_error_becomes_reject(): _reset_handlers() def _boom(_a): raise RuntimeError("db down") register_handler(EidAction.READ_STUDY, _boom) r = dispatch("read_study") assert r.ok is False assert "error" in r.reason.lower() def _run(): fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")] fails = 0 for fn in fns: try: fn() print(f" PASS {fn.__name__}") except Exception as exc: # noqa: BLE001 fails += 1 print(f" FAIL {fn.__name__}: {type(exc).__name__}: {exc}") print(f"\n{len(fns) - fails}/{len(fns)} passed") return 1 if fails else 0 if __name__ == "__main__": raise SystemExit(_run())