145 lines
5.2 KiB
Python
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
|