o2o-plagiarism-ai/tests/test_auth_middleware.py

145 lines
5.2 KiB
Python

"""API 키 인증 미들웨어 (#1).
앱 전체를 띄우지 않고 순수 함수로 검증한다. 엔진 기동(코퍼스 인덱싱)을 타면
테스트가 느려지고 인증 로직과 무관한 이유로 깨진다.
"""
from __future__ import annotations
import pytest
from app.core.config import Settings
from app.main import (
AuthConfigurationError,
is_authorized,
public_paths,
validate_auth_settings,
)
def _settings(**overrides) -> Settings:
base = {"api_key": "", "require_api_key": False,
"public_health": True, "public_docs": True}
base.update(overrides)
return Settings(**base)
# ---------------------------------------------------------------------------
# fail-closed 기동 검증
# ---------------------------------------------------------------------------
def test_require_api_key_without_key_fails_startup():
with pytest.raises(AuthConfigurationError) as exc:
validate_auth_settings(_settings(require_api_key=True, api_key=""))
assert "REQUIRE_API_KEY" in str(exc.value)
def test_require_api_key_with_whitespace_only_key_fails():
with pytest.raises(AuthConfigurationError):
validate_auth_settings(_settings(require_api_key=True, api_key=" "))
def test_require_api_key_with_key_starts_fine():
validate_auth_settings(_settings(require_api_key=True, api_key="secret"))
def test_default_settings_start_without_key():
"""기본값(개발)에서는 기동을 막지 않는다 — 기존 동작 보존."""
validate_auth_settings(_settings())
# ---------------------------------------------------------------------------
# 기존 기본 동작 보존
# ---------------------------------------------------------------------------
def test_no_key_configured_allows_everything():
s = _settings(api_key="")
assert is_authorized(s, "/v1/plagiarism/detect", "") is True
assert is_authorized(s, "/v1/corpus", "") is True
def test_key_configured_rejects_missing_and_wrong_key():
s = _settings(api_key="secret")
assert is_authorized(s, "/v1/plagiarism/detect", "") is False
assert is_authorized(s, "/v1/plagiarism/detect", "wrong") is False
assert is_authorized(s, "/v1/plagiarism/detect", "secret") is True
def test_corpus_write_paths_are_protected():
s = _settings(api_key="secret")
for path in ("/v1/corpus", "/v1/corpus/file", "/v1/corpus/doc-1", "/v1/plagiarism/batch"):
assert is_authorized(s, path, "") is False, path
# ---------------------------------------------------------------------------
# 공개 경로 설정
# ---------------------------------------------------------------------------
def test_health_public_by_default():
s = _settings(api_key="secret")
assert "/v1/health" in public_paths(s)
assert is_authorized(s, "/v1/health", "") is True
def test_health_can_be_protected():
s = _settings(api_key="secret", public_health=False)
assert "/v1/health" not in public_paths(s)
assert is_authorized(s, "/v1/health", "") is False
assert is_authorized(s, "/v1/health", "secret") is True
def test_docs_public_by_default_and_can_be_closed():
s = _settings(api_key="secret")
assert "/openapi.json" in public_paths(s)
closed = _settings(api_key="secret", public_docs=False)
assert "/openapi.json" not in public_paths(closed)
for path in ("/docs", "/openapi.json", "/redoc"):
assert is_authorized(closed, path, "") is False
assert is_authorized(closed, path, "secret") is True
def test_root_console_stays_public():
s = _settings(api_key="secret")
assert is_authorized(s, "/", "") is True
# ---------------------------------------------------------------------------
# 회귀: 비ASCII 키가 TypeError 로 500 을 내지 않아야 한다
# ---------------------------------------------------------------------------
def test_non_ascii_key_does_not_raise():
s = _settings(api_key="비밀키-한글🔑")
assert is_authorized(s, "/v1/plagiarism/detect", "비밀키-한글🔑") is True
assert is_authorized(s, "/v1/plagiarism/detect", "틀린키") is False
assert is_authorized(s, "/v1/plagiarism/detect", "") is False
def test_non_ascii_configured_key_returns_401_not_500():
"""서버에 한글 키를 설정해도 500 이 아니라 401 이어야 한다.
HTTP 헤더는 비ASCII 를 전송할 수 없으므로 이런 키는 사실상 인증 불가지만,
최소한 서버가 TypeError 로 터지면 안 된다. (운영 키는 ASCII 로 발급할 것)
"""
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from app.main import is_authorized as guard
settings = _settings(api_key="한글키")
app = FastAPI()
@app.middleware("http")
async def auth(request: Request, call_next):
if not guard(settings, request.url.path, request.headers.get("x-api-key", "")):
return JSONResponse(status_code=401, content={"detail": "Invalid or missing API key"})
return await call_next(request)
@app.get("/v1/thing")
async def thing():
return {"ok": True}
with TestClient(app) as client:
assert client.get("/v1/thing").status_code == 401
assert client.get("/v1/thing", headers={"x-api-key": "ascii-guess"}).status_code == 401