"""Phase 3.5 fix2: /ask 의 X-Source / X-Eval-Case-Id trust boundary. `_resolve_eval_identity()` 단위 테스트. - token 없음/틀림 + X-Source=eval → source='document_server', eval_case_id=None - token 일치 + X-Source=eval + X-Eval-Case-Id=case_xxx → ('eval', 'case_xxx') - token 틀림 + X-Eval-Case-Id 만 (X-Source 미지정) → eval_case_id=None - 일반 호출 (X-Source=ui_search, no eval headers) → ('ui_search', None) - env 미설정 (eval_runner_token='') 시 모든 eval claim 거부 """ from __future__ import annotations import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app")) import pytest @pytest.fixture def resolve_with_token(monkeypatch): """settings.eval_runner_token 을 monkey-patch 해서 _resolve_eval_identity 테스트.""" def _make(token: str): from core import config as cfg_mod from api import search as search_mod # 두 모듈 모두에서 settings 객체 참조하므로 직접 attr 변경 monkeypatch.setattr(search_mod.settings, "eval_runner_token", token) return search_mod._resolve_eval_identity return _make def test_no_token_no_eval_headers_default(resolve_with_token): """일반 호출 — eval 헤더 없음, source 기본값.""" resolve = resolve_with_token("secret123") assert resolve(None, None, None) == ("document_server", None) def test_normal_source_with_token(resolve_with_token): """ui_search 호출 — eval 클레임 아님이라 token 무관.""" resolve = resolve_with_token("secret123") assert resolve("ui_search", None, None) == ("ui_search", None) def test_eval_claim_no_token_rejected(resolve_with_token): """X-Source=eval 인데 token 없음 → 거부, source='document_server'.""" resolve = resolve_with_token("secret123") assert resolve("eval", "case_001", None) == ("document_server", None) def test_eval_claim_wrong_token_rejected(resolve_with_token): """token 틀림 → 거부.""" resolve = resolve_with_token("secret123") assert resolve("eval", "case_001", "wrong_token") == ("document_server", None) def test_eval_claim_correct_token_accepted(resolve_with_token): """token 일치 → 'eval' source + case_id 적재.""" resolve = resolve_with_token("secret123") assert resolve("eval", "case_001", "secret123") == ("eval", "case_001") def test_eval_case_id_only_no_source_no_token(resolve_with_token): """X-Eval-Case-Id 만 있고 token 없음 → 거부, case_id=None.""" resolve = resolve_with_token("secret123") assert resolve(None, "case_001", None) == ("document_server", None) def test_eval_case_id_only_wrong_token(resolve_with_token): """X-Eval-Case-Id 만 + token 틀림 → 거부.""" resolve = resolve_with_token("secret123") assert resolve(None, "case_001", "wrong") == ("document_server", None) def test_env_unset_rejects_even_correct_format(resolve_with_token): """settings.eval_runner_token='' 인 환경 → 모든 eval 클레임 거부.""" resolve = resolve_with_token("") # token 헤더가 와도 server side 가 비어있으면 거부 (constant-time False) assert resolve("eval", "case_001", "") == ("document_server", None) assert resolve("eval", "case_001", "anything") == ("document_server", None) def test_non_eval_source_forces_case_id_none(resolve_with_token): """X-Source=ui_detail + X-Eval-Case-Id (실수로 같이 보냄) → case_id=None. eval claim 아님 (source != 'eval' 이고 case_id 가 fallback 으로 eval claim 트리거) 이지만 source claim 이 명시적으로 non-eval 이라 token 검증 후 case_id None. """ resolve = resolve_with_token("secret123") # case_id 가 있으면 eval claim 으로 처리됨 → token 없으면 거부 → ('ui_detail' 클레임, # 하지만 거부 분기에서 claimed_source != 'eval' 이라 그대로 'ui_detail' 반환, case_id=None) assert resolve("ui_detail", "case_001", None) == ("ui_detail", None)