samchun-gemini / tests /test_admin_auth.py
JHyeok5's picture
Upload folder using huggingface_hub
266595d verified
"""
Admin API Authentication Tests
pytest tests for admin authentication middleware in admin_poi.py
Tests:
- ADMIN_API_KEY ๊ฒ€์ฆ ๋กœ์ง
- ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ์‹œ 401 ์—๋Ÿฌ
- ์ž˜๋ชป๋œ ํ‚ค ์ ‘๊ทผ ์‹œ 403 ์—๋Ÿฌ
- ํ‚ค ๋ฏธ์„ค์ • ์‹œ 503 ์—๋Ÿฌ
- ํƒ€์ด๋ฐ ๊ณต๊ฒฉ ๋ฐฉ์ง€ (์ƒ์ˆ˜ ์‹œ๊ฐ„ ๋น„๊ต)
"""
import pytest
import os
import secrets
from unittest.mock import patch
from httpx import AsyncClient
# ============================================
# Admin API Key Verification Tests
# ============================================
@pytest.mark.anyio
async def test_admin_preview_without_api_key(client: AsyncClient):
"""
์ธ์ฆ ์—†์ด Admin API ์ ‘๊ทผ ์‹œ 401 ์—๋Ÿฌ
X-Admin-API-Key ํ—ค๋” ์—†์ด ์š”์ฒญ
"""
response = await client.get("/admin/poi/preview/12") # 12 = ์• ์›”
# ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ—ค๋”๊ฐ€ ์—†์œผ๋ฉด 401 ๋˜๋Š” 503
assert response.status_code in [401, 503]
if response.status_code == 401:
data = response.json()
assert data["detail"]["code"] == "ADMIN_002"
assert "X-Admin-API-Key" in data["detail"]["message"]
@pytest.mark.anyio
async def test_admin_preview_with_invalid_api_key(client: AsyncClient):
"""
์ž˜๋ชป๋œ Admin API ํ‚ค๋กœ ์ ‘๊ทผ ์‹œ 403 ์—๋Ÿฌ
"""
# ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์–ด์•ผ 403 ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
if not os.getenv("ADMIN_API_KEY"):
pytest.skip("ADMIN_API_KEY not set - skipping invalid key test")
invalid_key = "invalid_api_key_12345"
response = await client.get(
"/admin/poi/preview/12",
headers={"X-Admin-API-Key": invalid_key}
)
assert response.status_code == 403
data = response.json()
assert data["detail"]["code"] == "ADMIN_003"
assert "์œ ํšจํ•˜์ง€ ์•Š์€" in data["detail"]["message"]
@pytest.mark.anyio
async def test_admin_sync_without_api_key(client: AsyncClient):
"""
์ธ์ฆ ์—†์ด Admin Sync API ์ ‘๊ทผ ์‹œ 401 ์—๋Ÿฌ
"""
response = await client.post("/admin/poi/sync/12?dry_run=true")
assert response.status_code in [401, 503]
@pytest.mark.anyio
async def test_admin_sync_with_invalid_api_key(client: AsyncClient):
"""
์ž˜๋ชป๋œ ํ‚ค๋กœ Admin Sync API ์ ‘๊ทผ ์‹œ 403 ์—๋Ÿฌ
"""
if not os.getenv("ADMIN_API_KEY"):
pytest.skip("ADMIN_API_KEY not set")
response = await client.post(
"/admin/poi/sync/12?dry_run=true",
headers={"X-Admin-API-Key": "wrong_key"}
)
assert response.status_code == 403
# ============================================
# Service Unavailable Tests (503)
# ============================================
@pytest.mark.anyio
async def test_admin_api_disabled_when_key_not_configured():
"""
ADMIN_API_KEY ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฏธ์„ค์ • ์‹œ 503 ์—๋Ÿฌ (์‹œ๋ฎฌ๋ ˆ์ด์…˜)
Note: ์‹ค์ œ ํ…Œ์ŠคํŠธ์—์„œ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์–ด๋ ค์šฐ๋ฏ€๋กœ
verify_admin_api_key ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ํ…Œ์ŠคํŠธ
"""
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# ์ง์ ‘ ํ•จ์ˆ˜ import ํ›„ ํ…Œ์ŠคํŠธ
from routers.admin_poi import verify_admin_api_key
from fastapi import HTTPException
# ADMIN_API_KEY๊ฐ€ None์ธ ์ƒํ™ฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
with patch('routers.admin_poi.ADMIN_API_KEY', None):
with pytest.raises(HTTPException) as exc_info:
await verify_admin_api_key(x_admin_api_key="any_key")
assert exc_info.value.status_code == 503
assert exc_info.value.detail["code"] == "ADMIN_001"
# ============================================
# Public Endpoints (No Auth Required)
# ============================================
@pytest.mark.anyio
async def test_get_regions_no_auth_required(client: AsyncClient):
"""
์ง€์—ญ ๋ชฉ๋ก ์กฐํšŒ API๋Š” ์ธ์ฆ ๋ถˆํ•„์š”
"""
response = await client.get("/admin/poi/regions")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
# ์• ์›”(12)์ด ๋ชฉ๋ก์— ์žˆ๋Š”์ง€ ํ™•์ธ
region_codes = [r["code"] for r in data]
assert "12" in region_codes
# ์ง€์—ญ ์ด๋ฆ„ ํ™•์ธ
aewol = next((r for r in data if r["code"] == "12"), None)
assert aewol is not None
assert aewol["name"] == "์• ์›”"
@pytest.mark.anyio
async def test_get_categories_no_auth_required(client: AsyncClient):
"""
์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก ์กฐํšŒ API๋Š” ์ธ์ฆ ๋ถˆํ•„์š”
"""
response = await client.get("/admin/poi/categories")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
# ํ•„์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ ํ™•์ธ
category_codes = [c["code"] for c in data]
assert "c1" in category_codes # ๊ด€๊ด‘์ง€
assert "c4" in category_codes # ์Œ์‹์ 
@pytest.mark.anyio
async def test_get_poi_stats_no_auth_required(client: AsyncClient):
"""
POI ํ†ต๊ณ„ ์กฐํšŒ API๋Š” ์ธ์ฆ ๋ถˆํ•„์š”
"""
response = await client.get("/admin/poi/stats")
assert response.status_code == 200
data = response.json()
assert "total_spots" in data
assert "visitjeju_spots" in data
assert "by_category" in data
# ============================================
# Region and Category Validation Tests
# ============================================
@pytest.mark.anyio
async def test_preview_invalid_region_code(client: AsyncClient):
"""
์œ ํšจํ•˜์ง€ ์•Š์€ ์ง€์—ญ ์ฝ”๋“œ๋กœ preview ์š”์ฒญ ์‹œ 400 ์—๋Ÿฌ
"""
# ์ธ์ฆ ์Šคํ‚ต์„ ์œ„ํ•ด ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ™•์ธ
admin_key = os.getenv("ADMIN_API_KEY")
if not admin_key:
pytest.skip("ADMIN_API_KEY not set")
response = await client.get(
"/admin/poi/preview/99", # ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ง€์—ญ ์ฝ”๋“œ
headers={"X-Admin-API-Key": admin_key}
)
assert response.status_code == 400
assert "Invalid region code" in response.json()["detail"]
@pytest.mark.anyio
async def test_preview_invalid_category(client: AsyncClient):
"""
์œ ํšจํ•˜์ง€ ์•Š์€ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ preview ์š”์ฒญ ์‹œ 400 ์—๋Ÿฌ
"""
admin_key = os.getenv("ADMIN_API_KEY")
if not admin_key:
pytest.skip("ADMIN_API_KEY not set")
response = await client.get(
"/admin/poi/preview/12?category=invalid",
headers={"X-Admin-API-Key": admin_key}
)
assert response.status_code == 400
assert "Invalid category" in response.json()["detail"]
@pytest.mark.anyio
async def test_sync_invalid_region_code(client: AsyncClient):
"""
์œ ํšจํ•˜์ง€ ์•Š์€ ์ง€์—ญ ์ฝ”๋“œ๋กœ sync ์š”์ฒญ ์‹œ 400 ์—๋Ÿฌ
"""
admin_key = os.getenv("ADMIN_API_KEY")
if not admin_key:
pytest.skip("ADMIN_API_KEY not set")
response = await client.post(
"/admin/poi/sync/invalid?dry_run=true",
headers={"X-Admin-API-Key": admin_key}
)
assert response.status_code == 400
# ============================================
# Timing Attack Prevention Tests
# ============================================
def test_constant_time_comparison():
"""
ํƒ€์ด๋ฐ ๊ณต๊ฒฉ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์ƒ์ˆ˜ ์‹œ๊ฐ„ ๋น„๊ต ํ…Œ์ŠคํŠธ
secrets.compare_digest๊ฐ€ ์‚ฌ์šฉ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
(์‹ค์ œ ํƒ€์ด๋ฐ ์ฐจ์ด ์ธก์ •์€ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋‹ค๋ฅด๋ฏ€๋กœ ํ•จ์ˆ˜ ์‚ฌ์šฉ ์—ฌ๋ถ€๋งŒ ํ™•์ธ)
"""
correct_key = "correct_admin_key_12345"
wrong_key = "wrong_admin_key_12345"
# secrets.compare_digest๋Š” ๊ธธ์ด๊ฐ€ ๊ฐ™์€ ๋ฌธ์ž์—ด์— ๋Œ€ํ•ด
# ์ƒ์ˆ˜ ์‹œ๊ฐ„ ๋น„๊ต๋ฅผ ์ˆ˜ํ–‰
result1 = secrets.compare_digest(correct_key, correct_key)
result2 = secrets.compare_digest(correct_key, wrong_key)
assert result1 is True
assert result2 is False
# ์ฒซ ๊ธ€์ž๋งŒ ๋‹ค๋ฅธ ๊ฒฝ์šฐ
almost_correct = "aorrect_admin_key_12345"
result3 = secrets.compare_digest(correct_key, almost_correct)
assert result3 is False
# ============================================
# Helper Function Tests
# ============================================
def test_parse_tags():
"""ํƒœ๊ทธ ๋ฌธ์ž์—ด ํŒŒ์‹ฑ ํ…Œ์ŠคํŠธ"""
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from routers.admin_poi import parse_tags
# ์ •์ƒ ์ผ€์ด์Šค
tags = parse_tags("์ž์—ฐ, ํ•ด๋ณ€, ํž๋ง")
assert tags == ["์ž์—ฐ", "ํ•ด๋ณ€", "ํž๋ง"]
# ๋นˆ ๋ฌธ์ž์—ด
assert parse_tags("") == []
# None
assert parse_tags(None) == []
# ๊ณต๋ฐฑ ํฌํ•จ
tags = parse_tags(" ์ž์—ฐ , ํ•ด๋ณ€ ")
assert tags == ["์ž์—ฐ", "ํ•ด๋ณ€"]
def test_infer_themes():
"""ํ…Œ๋งˆ ์ถ”๋ก  ํ…Œ์ŠคํŠธ"""
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from routers.admin_poi import infer_themes
# ์—ญ์‚ฌ ํ…Œ๋งˆ
themes = infer_themes(["์—ญ์‚ฌ", "์œ ์ ์ง€", "๋ฌธํ™”"])
assert "history" in themes
# ์ž์—ฐ ํ…Œ๋งˆ
themes = infer_themes(["๋ฐ”๋‹ค", "ํ•ด๋ณ€", "์ž์—ฐ๊ฒฝ๊ด€"])
assert "nature" in themes
# ์Œ์‹ ํ…Œ๋งˆ
themes = infer_themes(["๋ง›์ง‘", "์นดํŽ˜", "๋จน๊ฑฐ๋ฆฌ"])
assert "food" in themes
# ์‚ฌ์ง„ ํ…Œ๋งˆ
themes = infer_themes(["ํฌํ† ์ŠคํŒŸ", "์ผ๋ชฐ", "๋ทฐ๋ง›์ง‘"])
assert "photo" in themes
# ํž๋ง ํ…Œ๋งˆ
themes = infer_themes(["ํž๋ง", "ํœด์‹", "๋ช…์ƒ"])
assert "healing" in themes
# ํƒœ๊ทธ ์—†์„ ๋•Œ ๊ธฐ๋ณธ๊ฐ’ nature
themes = infer_themes([])
assert themes == ["nature"]
def test_infer_activity_level():
"""ํ™œ๋™ ๋ ˆ๋ฒจ ์ถ”๋ก  ํ…Œ์ŠคํŠธ"""
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from routers.admin_poi import infer_activity_level
# active
assert infer_activity_level(["ํŠธ๋ ˆํ‚น", "๋“ฑ์‚ฐ"]) == "active"
assert infer_activity_level(["์˜ฌ๋ ˆ๊ธธ", "ํ•˜์ดํ‚น"]) == "active"
# moderate
assert infer_activity_level(["์‚ฐ์ฑ…", "๊ฑท๊ธฐ"]) == "moderate"
# light (๊ธฐ๋ณธ๊ฐ’)
assert infer_activity_level([]) == "light"
assert infer_activity_level(["์นดํŽ˜", "์Œ์‹"]) == "light"
def test_infer_mood():
"""๋ถ„์œ„๊ธฐ ์ถ”๋ก  ํ…Œ์ŠคํŠธ"""
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from routers.admin_poi import infer_mood
# quiet
moods = infer_mood(["์กฐ์šฉํ•œ", "ํ•œ์ ํ•œ"])
assert "quiet" in moods
# vibrant
moods = infer_mood(["ํ™œ๊ธฐ์ฐฌ", "์ถ•์ œ"])
assert "vibrant" in moods
# romantic
moods = infer_mood(["๋กœ๋งจํ‹ฑ", "๋ฐ์ดํŠธ"])
assert "romantic" in moods
# family
moods = infer_mood(["๊ฐ€์กฑ", "์–ด๋ฆฐ์ด"])
assert "family" in moods
# ๊ธฐ๋ณธ๊ฐ’
moods = infer_mood([])
assert moods == ["quiet"]
def test_infer_time_of_day():
"""์‹œ๊ฐ„๋Œ€ ์ถ”๋ก  ํ…Œ์ŠคํŠธ"""
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from routers.admin_poi import infer_time_of_day
# morning
times = infer_time_of_day(["์ผ์ถœ", "์•„์นจ"])
assert "morning" in times
# evening
times = infer_time_of_day(["์ผ๋ชฐ", "์•ผ๊ฒฝ"])
assert "evening" in times
# ๊ธฐ๋ณธ๊ฐ’ afternoon
times = infer_time_of_day([])
assert times == ["afternoon"]
def test_normalize_phone():
"""์ „ํ™”๋ฒˆํ˜ธ ์ •๊ทœํ™” ํ…Œ์ŠคํŠธ"""
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from routers.admin_poi import normalize_phone
# ์ •์ƒ ๋ฒˆํ˜ธ
assert normalize_phone("064-123-4567") == "064-123-4567"
# ๋นˆ ๊ฐ’
assert normalize_phone(None) is None
assert normalize_phone("") is None
assert normalize_phone(" ") is None
assert normalize_phone("--") is None
def test_infer_restaurant_category():
"""์Œ์‹์  ์นดํ…Œ๊ณ ๋ฆฌ ์„ธ๋ถ„ํ™” ํ…Œ์ŠคํŠธ"""
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from routers.admin_poi import infer_restaurant_category
# ์นดํŽ˜ ๊ตฌ๋ถ„
assert infer_restaurant_category(["์นดํŽ˜", "์ปคํ”ผ"]) == "cafe"
assert infer_restaurant_category(["๋””์ €ํŠธ", "๋ฒ ์ด์ปค๋ฆฌ"]) == "cafe"
# ์ผ๋ฐ˜ ์Œ์‹์ 
assert infer_restaurant_category(["ํ•œ์‹", "๋ง›์ง‘"]) == "restaurant"
assert infer_restaurant_category([]) == "restaurant"
def test_infer_category_from_tags():
"""๊ด€๊ด‘์ง€ ์นดํ…Œ๊ณ ๋ฆฌ ์„ธ๋ถ„ํ™” ํ…Œ์ŠคํŠธ"""
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from routers.admin_poi import infer_category_from_tags
# ํ•ด๋ณ€
assert infer_category_from_tags(["ํ•ด๋ณ€", "ํ•ด์ˆ˜์š•์žฅ"], "coastline") == "beach"
# ์˜ค๋ฆ„
assert infer_category_from_tags(["์˜ค๋ฆ„", "์‚ฐ"], "coastline") == "oreum"
# ์ˆฒ
assert infer_category_from_tags(["์ˆฒ", "๊ณถ์ž์™ˆ"], "coastline") == "forest"
# 4.3 ์œ ์ 
assert infer_category_from_tags(["4.3", "์—ญ์‚ฌ"], "coastline") == "jeju43"
# ๊ธฐ๋ณธ๊ฐ’ ์œ ์ง€
assert infer_category_from_tags(["์ผ๋ฐ˜ํƒœ๊ทธ"], "coastline") == "coastline"