Spaces:
Runtime error
Runtime error
| """ | |
| 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 | |
| # ============================================ | |
| 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"] | |
| 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"] | |
| 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] | |
| 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) | |
| # ============================================ | |
| 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) | |
| # ============================================ | |
| 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"] == "์ ์" | |
| 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 # ์์์ | |
| 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 | |
| # ============================================ | |
| 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"] | |
| 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"] | |
| 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" | |