216 lines
8.6 KiB
Python
216 lines
8.6 KiB
Python
"""
|
|
단위 테스트: 예측 용량 계획 (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
|