""" 단위 테스트: 예측 용량 계획 (Predictive Capacity Planning) - predictive_capacity 라우터 임포트 검증 - 신규 ORM 모델 3개 테이블명 검증 - Pydantic 스키마 필드 검증 - 예측 로직 헬퍼 함수 검증 - 경보 기준 상수 검증 - 예산 사이클 분기 권고 문구 검증 """ from __future__ import annotations # ── 임포트 가능 여부 ────────────────────────────────────────────────────────── def test_module_import(): """predictive_capacity 모듈이 오류 없이 임포트되는지 확인.""" from routers import predictive_capacity assert predictive_capacity.router is not None def test_router_prefix(): """라우터 prefix가 /api/capacity-ai인지 확인.""" from routers.predictive_capacity import router assert router.prefix == "/api/capacity-ai" # ── ORM 모델 검증 ───────────────────────────────────────────────────────────── def test_orm_models_import(): """신규 ORM 모델 3개가 임포트되는지 확인.""" from models import CapacityForecast, CapacityRecommendation, BudgetCycle assert CapacityForecast.__tablename__ == "tb_capacity_forecast" assert CapacityRecommendation.__tablename__ == "tb_capacity_recommendation" assert BudgetCycle.__tablename__ == "tb_budget_cycle" def test_capacity_forecast_columns(): """CapacityForecast 필수 컬럼 존재 확인.""" from models import CapacityForecast cols = {c.name for c in CapacityForecast.__table__.columns} required = {"id", "server_name", "metric", "forecast_days", "current_value", "predicted_value", "confidence", "trend", "created_at"} assert required.issubset(cols) def test_capacity_recommendation_columns(): """CapacityRecommendation 필수 컬럼 존재 확인.""" from models import CapacityRecommendation cols = {c.name for c in CapacityRecommendation.__table__.columns} required = {"id", "server_name", "rec_type", "urgency", "reason", "estimated_cost", "status", "approved_by", "created_at"} assert required.issubset(cols) def test_budget_cycle_columns(): """BudgetCycle 필수 컬럼 존재 확인.""" from models import BudgetCycle cols = {c.name for c in BudgetCycle.__table__.columns} required = {"id", "year", "quarter", "budget_infra", "budget_license", "budget_cloud", "spent", "forecast_spend", "status", "created_at"} assert required.issubset(cols) # ── Pydantic 스키마 검증 ────────────────────────────────────────────────────── def test_pydantic_schemas_import(): """Pydantic 스키마 4종 임포트 확인.""" from models import ( CapacityForecastOut, CapacityRecommendationOut, BudgetCycleOut, BudgetCycleCreate, ) assert CapacityForecastOut is not None assert CapacityRecommendationOut is not None assert BudgetCycleOut is not None assert BudgetCycleCreate is not None def test_budget_cycle_create_defaults(): """BudgetCycleCreate 기본값 검증.""" from models import BudgetCycleCreate body = BudgetCycleCreate(year=2026) assert body.quarter == 1 assert body.budget_infra == 0.0 assert body.status == "planning" def test_capacity_forecast_out_fields(): """CapacityForecastOut 필수 필드 검증.""" from models import CapacityForecastOut fields = set(CapacityForecastOut.model_fields.keys()) required = {"id", "server_name", "metric", "forecast_days", "current_value", "predicted_value", "confidence", "trend", "created_at"} assert required.issubset(fields) # ── 헬퍼 함수 검증 ──────────────────────────────────────────────────────────── def test_get_budget_recommendation_all_quarters(): """모든 분기(1~4)에 대해 권고 문구가 반환되는지 확인.""" from routers.predictive_capacity import get_budget_recommendation for q in (1, 2, 3, 4): msg = get_budget_recommendation(q) assert isinstance(msg, str) assert len(msg) > 0 def test_get_budget_recommendation_unknown_quarter(): """잘못된 분기 값에 대해 기본 메시지 반환 확인.""" from routers.predictive_capacity import get_budget_recommendation msg = get_budget_recommendation(99) assert msg == "예산 계획 수립 중" def test_trend_label_increasing(): """일별 증가율 > 0.5 → increasing 판정.""" from routers.predictive_capacity import _trend_label assert _trend_label(0.8) == "increasing" assert _trend_label(1.2) == "increasing" def test_trend_label_stable(): """일별 증가율 0.0~0.5 → stable 판정.""" from routers.predictive_capacity import _trend_label assert _trend_label(0.3) == "stable" assert _trend_label(0.0) == "stable" def test_trend_label_decreasing(): """일별 증가율 < -0.1 → decreasing 판정.""" from routers.predictive_capacity import _trend_label assert _trend_label(-0.5) == "decreasing" assert _trend_label(-1.0) == "decreasing" def test_urgency_from_predicted_immediate(): """30일 내 80% 초과 → immediate 긴급도.""" from routers.predictive_capacity import _urgency_from_predicted assert _urgency_from_predicted(30, 82.0) == "immediate" assert _urgency_from_predicted(25, 85.0) == "immediate" def test_urgency_from_predicted_30days(): """60일 내 90% 초과 → 30days 긴급도.""" from routers.predictive_capacity import _urgency_from_predicted # 30일 내 80% 미만이지만 60일 내 90% 초과 assert _urgency_from_predicted(60, 92.0) == "30days" def test_urgency_from_predicted_60days(): """90일 내 95% 초과 → 60days 긴급도.""" from routers.predictive_capacity import _urgency_from_predicted # 30/60일 기준 미달, 90일 기준 충족 assert _urgency_from_predicted(90, 96.0) == "60days" def test_urgency_from_predicted_none(): """임계치 미달 시 None 반환.""" from routers.predictive_capacity import _urgency_from_predicted # 모든 기준 미달 assert _urgency_from_predicted(90, 50.0) is None # ── 경보 기준 상수 검증 ─────────────────────────────────────────────────────── def test_alert_rules_structure(): """경보 규칙 상수(_ALERT_RULES)가 3개 항목이며 올바른 순서인지 확인.""" from routers.predictive_capacity import _ALERT_RULES assert len(_ALERT_RULES) == 3 days_list = [r[0] for r in _ALERT_RULES] thresholds = [r[1] for r in _ALERT_RULES] # 기간이 짧을수록 임계치가 낮아야 함 (더 빠른 경고) assert days_list == sorted(days_list) assert thresholds == sorted(thresholds) # ── 엔드포인트 경로 검증 ────────────────────────────────────────────────────── def test_all_routes_registered(): """9개 엔드포인트가 모두 등록되어 있는지 확인.""" from routers.predictive_capacity import router paths = {r.path for r in router.routes} expected = { "/api/capacity-ai/forecast", "/api/capacity-ai/forecast/{days}", "/api/capacity-ai/recommendations", "/api/capacity-ai/recommendations/{rec_id}/approve", "/api/capacity-ai/recommendations/{rec_id}/reject", "/api/capacity-ai/budget-cycle", "/api/capacity-ai/alerts", } assert expected.issubset(paths) def test_forecast_post_and_get_routes(): """forecast 경로에 GET과 POST가 모두 존재하는지 확인.""" from routers.predictive_capacity import router forecast_methods: set = set() for r in router.routes: if r.path == "/api/capacity-ai/forecast": forecast_methods.update(r.methods) assert "GET" in forecast_methods assert "POST" in forecast_methods def test_budget_cycle_post_and_get_routes(): """budget-cycle 경로에 GET과 POST가 모두 존재하는지 확인.""" from routers.predictive_capacity import router bc_methods: set = set() for r in router.routes: if r.path == "/api/capacity-ai/budget-cycle": bc_methods.update(r.methods) assert "GET" in bc_methods assert "POST" in bc_methods