o2o-negosium-original/lps/tests/test_api_guard.py
민헌 e7d2c87fbe feat(lps): API guard — LPS_API_KEY 설정 시에만 /v1 에 X-API-Key 검증
외부에서 API 를 함부로 호출(비용 발생 enqueue 등)하지 못하도록 정적 키
guard 를 추가한다. '키의 존재'가 토글 — 개발(local/dev)은 env 를 비워
개방 모드(기동 시 WARN), prod 만 키를 주입한다(협의 결정).

- router/v1/validator/auth.py: X-API-Key 의존성 — secrets.compare_digest
  상수시간 비교, 콤마 구분 복수 키(무중단 키 교체), 매 요청 env 조회
  (재기동 없이 테스트 가능). /v1 라우터 전체에 적용.
- /healthz·/readyz 는 라우터 밖이라 항상 개방(LB 프로브).
- negodata lps_sync_service: LPS_API_KEY env 있으면 헤더 자동 첨부(한 곳).
- compose(lps-api·negodata-backend) LPS_API_KEY 패스스루 + .env.example.
- prod 체크리스트(operations.md): 키 주입 + lps-api 포트 비공개 + 기동
  로그 'API guard ON' 확인. api.md 인증 섹션 추가.
- 라이브 스모크: 무헤더/오키 401 · 정키 2종 200 · healthz 200 확인.
- 테스트 6건 추가, 전체 141 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:45:31 +09:00

44 lines
1.5 KiB
Python

"""API guard 테스트 — LPS_API_KEY 설정 시에만 /v1 에 X-API-Key 검증(개발=미설정=개방 모드).
auth.configured_keys 가 매 요청 env 를 읽으므로 monkeypatch.setenv 만으로 on/off 를 전환한다
(앱 재기동 불필요).
"""
import pytest
@pytest.fixture
def guarded(monkeypatch):
monkeypatch.setenv("LPS_API_KEY", "k1,k2")
async def test_open_mode_without_key_env(client, monkeypatch):
monkeypatch.delenv("LPS_API_KEY", raising=False)
r = await client.get("/v1/lps/queue/stats") # 개방 모드 — 헤더 없이 통과
assert r.status_code == 200
async def test_guarded_rejects_missing_header(client, guarded):
r = await client.get("/v1/lps/queue/stats")
assert r.status_code == 401
async def test_guarded_rejects_wrong_key(client, guarded):
r = await client.get("/v1/lps/queue/stats", headers={"X-API-Key": "nope"})
assert r.status_code == 401
async def test_guarded_accepts_any_configured_key(client, guarded):
for key in ("k1", "k2"): # 복수 키 — 무중단 키 교체용
r = await client.get("/v1/lps/queue/stats", headers={"X-API-Key": key})
assert r.status_code == 200
async def test_guard_covers_post_search(client, guarded):
r = await client.post("/v1/lps/search", json={"data": []})
assert r.status_code == 401 # enqueue(비용 발생 경로)도 보호
async def test_healthz_stays_open(client, guarded):
assert (await client.get("/healthz")).status_code == 200 # LB 프로브는 guard 밖