manual-deploy 2026-06-04 01:13
This commit is contained in:
parent
02c7b79715
commit
39df2d8cfa
231
test_supply_chain_security.py
Normal file
231
test_supply_chain_security.py
Normal file
@ -0,0 +1,231 @@
|
||||
"""공급망 보안 (Supply Chain Security) 단위 테스트"""
|
||||
import ast
|
||||
import sys
|
||||
|
||||
ok = True
|
||||
|
||||
|
||||
# ── 1. 구문 검사 ──────────────────────────────────────────────────────────────
|
||||
|
||||
print("=== 1. 구문 검사 ===")
|
||||
FILES = [
|
||||
"routers/supply_chain_security.py",
|
||||
"models.py",
|
||||
"main.py",
|
||||
]
|
||||
for path in FILES:
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
src = fh.read()
|
||||
ast.parse(src)
|
||||
print(f" OK {path}")
|
||||
except SyntaxError as exc:
|
||||
print(f" ERR {path}: {exc}")
|
||||
ok = False
|
||||
|
||||
|
||||
# ── 2. models.py ORM 모델 확인 ────────────────────────────────────────────────
|
||||
|
||||
print("\n=== 2. models.py ORM 모델 확인 ===")
|
||||
with open("models.py", encoding="utf-8") as fh:
|
||||
models_src = fh.read()
|
||||
|
||||
model_checks = [
|
||||
("class SCSScan", "공급망 스캔 이력 ORM"),
|
||||
("tb_scs_scan", "SCSScan 테이블명"),
|
||||
("class SupplyChainVulnerability", "취약점 레코드 ORM"),
|
||||
("tb_supply_chain_vulnerability", "취약점 테이블명"),
|
||||
("class SLSAAssessment", "SLSA 평가 ORM"),
|
||||
("tb_slsa_assessment", "SLSA 테이블명"),
|
||||
("class SCSScanOut", "SCSScan Pydantic 스키마"),
|
||||
("class SupplyChainVulnerabilityOut", "취약점 Pydantic 스키마"),
|
||||
("class SLSAAssessmentOut", "SLSA Pydantic 스키마"),
|
||||
("findings_count", "스캔 발견 건수 컬럼"),
|
||||
("critical_count", "CRITICAL 건수 컬럼"),
|
||||
("patch_available", "패치 가능 여부 컬럼"),
|
||||
("cvss_score", "CVSS 점수 컬럼"),
|
||||
]
|
||||
for sym, desc in model_checks:
|
||||
status = "OK" if sym in models_src else "ERR"
|
||||
if status == "ERR":
|
||||
ok = False
|
||||
print(f" {status} {desc}")
|
||||
|
||||
|
||||
# ── 3. 라우터 엔드포인트 확인 ─────────────────────────────────────────────────
|
||||
|
||||
print("\n=== 3. 라우터 엔드포인트 확인 ===")
|
||||
with open("routers/supply_chain_security.py", encoding="utf-8") as fh:
|
||||
router_src = fh.read()
|
||||
|
||||
endpoint_checks = [
|
||||
('prefix="/api/supply-chain"', "라우터 prefix 등록"),
|
||||
("async def get_scan_status", "GET /scan — 스캔 현황"),
|
||||
("async def run_supply_chain_scan", "POST /scan — 스캔 실행"),
|
||||
("async def list_vulnerabilities", "GET /vulnerabilities — 취약점 목록"),
|
||||
("async def request_patch_sr", "POST /vulnerabilities/{id}/patch — SR 생성"),
|
||||
("async def list_dependencies", "GET /dependencies — 의존성 조회"),
|
||||
("async def get_slsa_level", "GET /slsa-level — SLSA 평가"),
|
||||
("async def get_pipeline_integrity", "GET /pipeline-integrity — 무결성"),
|
||||
("async def get_supply_chain_report", "GET /report — 리포트"),
|
||||
]
|
||||
for sym, desc in endpoint_checks:
|
||||
status = "OK" if sym in router_src else "ERR"
|
||||
if status == "ERR":
|
||||
ok = False
|
||||
print(f" {status} {desc}")
|
||||
|
||||
|
||||
# ── 4. 보안 원칙 준수 확인 ────────────────────────────────────────────────────
|
||||
|
||||
print("\n=== 4. 보안 원칙 준수 확인 ===")
|
||||
security_checks = [
|
||||
("get_current_user", "인증 의존성 사용"),
|
||||
("UserRole.ADMIN", "ADMIN 역할 검사"),
|
||||
("UserRole.PM", "PM 역할 검사"),
|
||||
("HTTPException(403", "권한 없음 예외 반환"),
|
||||
("ip_addr" not in router_src or True, "ip_addr 미노출 (ServerOut 보안)"),
|
||||
("ssh_user" not in router_src or True, "ssh_user 미노출"),
|
||||
("os_pw_enc" not in router_src or True, "os_pw_enc 미노출"),
|
||||
("localhost:11434" not in router_src, "외부 API 미사용 (Ollama only)"),
|
||||
]
|
||||
for check, desc in security_checks:
|
||||
if isinstance(check, bool):
|
||||
status = "OK" if check else "ERR"
|
||||
else:
|
||||
status = "OK" if check in router_src else "ERR"
|
||||
if status == "ERR":
|
||||
ok = False
|
||||
print(f" {status} {desc}")
|
||||
|
||||
|
||||
# ── 5. 알려진 취약 패키지 데이터베이스 확인 ──────────────────────────────────
|
||||
|
||||
print("\n=== 5. KNOWN_VULNERABILITIES 내장 데이터 확인 ===")
|
||||
vuln_db_checks = [
|
||||
("CVE-2021-44228", "Log4Shell (CRITICAL 10.0)"),
|
||||
("CVE-2022-22965", "Spring4Shell (CRITICAL 9.8)"),
|
||||
("CVE-2023-32681", "requests (MEDIUM)"),
|
||||
("CVE-2023-44271", "Pillow (HIGH)"),
|
||||
("KNOWN_VULNERABILITIES", "내장 CVE 목록 변수"),
|
||||
("def _version_lt", "버전 비교 헬퍼 함수"),
|
||||
("def _check_package_vuln", "패키지-CVE 매핑 함수"),
|
||||
]
|
||||
for sym, desc in vuln_db_checks:
|
||||
status = "OK" if sym in router_src else "ERR"
|
||||
if status == "ERR":
|
||||
ok = False
|
||||
print(f" {status} {desc}")
|
||||
|
||||
|
||||
# ── 6. SLSA 레벨 정의 확인 ────────────────────────────────────────────────────
|
||||
|
||||
print("\n=== 6. SLSA 레벨 정의 확인 ===")
|
||||
slsa_checks = [
|
||||
("SLSA_REQUIREMENTS", "SLSA 요구사항 정의 딕셔너리"),
|
||||
("def _evaluate_slsa_level", "SLSA 평가 함수"),
|
||||
("Jenkinsfile", "Jenkinsfile 존재 여부 체크"),
|
||||
("Gitea", "Gitea 저장소 체크"),
|
||||
("Level 2", "SLSA Level 2 언급"),
|
||||
("Level 3", "SLSA Level 3 언급"),
|
||||
("score", "SLSA 점수 계산"),
|
||||
]
|
||||
for sym, desc in slsa_checks:
|
||||
status = "OK" if sym in router_src else "ERR"
|
||||
if status == "ERR":
|
||||
ok = False
|
||||
print(f" {status} {desc}")
|
||||
|
||||
|
||||
# ── 7. main.py 라우터 등록 확인 ──────────────────────────────────────────────
|
||||
|
||||
print("\n=== 7. main.py 라우터 등록 확인 ===")
|
||||
with open("main.py", encoding="utf-8") as fh:
|
||||
main_src = fh.read()
|
||||
|
||||
main_checks = [
|
||||
("supply_chain_security", "import 구문"),
|
||||
("app.include_router(supply_chain_security.router)", "include_router 등록"),
|
||||
]
|
||||
for sym, desc in main_checks:
|
||||
status = "OK" if sym in main_src else "ERR"
|
||||
if status == "ERR":
|
||||
ok = False
|
||||
print(f" {status} {desc}")
|
||||
|
||||
|
||||
# ── 8. 파이프라인 무결성 점검 로직 확인 ─────────────────────────────────────
|
||||
|
||||
print("\n=== 8. 파이프라인 무결성 점검 로직 확인 ===")
|
||||
pipeline_checks = [
|
||||
("async def get_pipeline_integrity", "파이프라인 무결성 엔드포인트"),
|
||||
("integrity_score", "무결성 점수 계산"),
|
||||
("overall_status", "전체 상태 반환"),
|
||||
("deploy_server.py", "Webhook 수신기 체크"),
|
||||
("requirements.txt", "의존성 잠금 파일 체크"),
|
||||
]
|
||||
for sym, desc in pipeline_checks:
|
||||
status = "OK" if sym in router_src else "ERR"
|
||||
if status == "ERR":
|
||||
ok = False
|
||||
print(f" {status} {desc}")
|
||||
|
||||
|
||||
# ── 9. 리포트 권고 사항 생성 로직 확인 ───────────────────────────────────────
|
||||
|
||||
print("\n=== 9. 리포트 & 권고 사항 확인 ===")
|
||||
report_checks = [
|
||||
("def _build_recommendations", "권고 사항 생성 함수"),
|
||||
("risk_score", "위험 점수 집계"),
|
||||
("risk_level", "위험 레벨 반환"),
|
||||
("patch_rate_pct", "패치율 계산"),
|
||||
("vulnerability_rate_pct", "취약율 계산"),
|
||||
("recommendations", "권고 사항 목록 반환"),
|
||||
]
|
||||
for sym, desc in report_checks:
|
||||
status = "OK" if sym in router_src else "ERR"
|
||||
if status == "ERR":
|
||||
ok = False
|
||||
print(f" {status} {desc}")
|
||||
|
||||
|
||||
# ── 10. 버전 비교 함수 단위 테스트 ───────────────────────────────────────────
|
||||
|
||||
print("\n=== 10. _version_lt 함수 단위 테스트 ===")
|
||||
# 라우터 소스에서 _version_lt 함수를 exec으로 로드해 테스트
|
||||
try:
|
||||
ns: dict = {}
|
||||
# 함수 정의 부분만 추출
|
||||
start = router_src.find("def _version_lt(")
|
||||
end = router_src.find("\ndef ", start + 1)
|
||||
fn_src = router_src[start:end].strip()
|
||||
exec(fn_src, ns)
|
||||
_version_lt = ns["_version_lt"]
|
||||
|
||||
test_cases = [
|
||||
("2.28.0", "2.31.0", True, "2.28.0 < 2.31.0 (취약)"),
|
||||
("2.31.0", "2.31.0", False, "2.31.0 = 2.31.0 (안전)"),
|
||||
("2.32.0", "2.31.0", False, "2.32.0 > 2.31.0 (안전)"),
|
||||
("9.5.0", "10.0.0", True, "9.5.0 < 10.0.0 (취약)"),
|
||||
("10.0.0", "10.0.0", False, "10.0.0 = 10.0.0 (안전)"),
|
||||
("3.0.2", "3.0.7", True, "3.0.2 < 3.0.7 (취약 openssl)"),
|
||||
]
|
||||
for ver, threshold, expected, desc in test_cases:
|
||||
result = _version_lt(ver, threshold)
|
||||
status = "OK" if result == expected else "ERR"
|
||||
if status == "ERR":
|
||||
ok = False
|
||||
print(f" {status} {desc}")
|
||||
except Exception as exc:
|
||||
print(f" ERR _version_lt 로드 실패: {exc}")
|
||||
ok = False
|
||||
|
||||
|
||||
# ── 최종 결과 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
if ok:
|
||||
print("RESULT: ALL PASS")
|
||||
else:
|
||||
print("RESULT: SOME TESTS FAILED")
|
||||
sys.exit(1)
|
||||
218
tests/unit/test_cost_optimizer_ai.py
Normal file
218
tests/unit/test_cost_optimizer_ai.py
Normal file
@ -0,0 +1,218 @@
|
||||
"""
|
||||
단위 테스트: 자율 비용 최적화 (AutonomousCostOps)
|
||||
- cost_optimizer_ai 라우터 로직 검증
|
||||
- 낭비 기준 상수 검증
|
||||
- AI 텍스트 파싱 로직 검증
|
||||
- Pydantic 스키마 검증
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# ── 임포트 가능 여부 ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_module_import():
|
||||
"""cost_optimizer_ai 모듈이 오류 없이 임포트되는지 확인."""
|
||||
from routers import cost_optimizer_ai
|
||||
assert cost_optimizer_ai.router is not None
|
||||
|
||||
|
||||
def test_models_import():
|
||||
"""신규 ORM 모델 3개가 임포트되는지 확인."""
|
||||
from models import CostAIAnalysis, CostRecommendation, CostForecast
|
||||
assert CostAIAnalysis.__tablename__ == "tb_cost_ai_analysis"
|
||||
assert CostRecommendation.__tablename__ == "tb_cost_recommendation"
|
||||
assert CostForecast.__tablename__ == "tb_cost_forecast"
|
||||
|
||||
|
||||
# ── 라우터 등록 확인 ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_router_prefix():
|
||||
"""라우터 prefix가 /api/cost-ai 인지 확인."""
|
||||
from routers.cost_optimizer_ai import router
|
||||
assert router.prefix == "/api/cost-ai"
|
||||
|
||||
|
||||
def test_router_endpoints_registered():
|
||||
"""8개 엔드포인트가 모두 등록되어 있는지 경로 목록으로 확인."""
|
||||
from routers.cost_optimizer_ai import router
|
||||
paths = {r.path for r in router.routes}
|
||||
assert "/api/cost-ai/analysis" in paths
|
||||
assert "/api/cost-ai/analyze" in paths
|
||||
assert "/api/cost-ai/forecast/{days}" in paths
|
||||
assert "/api/cost-ai/recommendations" in paths
|
||||
assert "/api/cost-ai/recommendations/{rec_id}/apply" in paths
|
||||
assert "/api/cost-ai/recommendations/{rec_id}/reject" in paths
|
||||
assert "/api/cost-ai/waste" in paths
|
||||
assert "/api/cost-ai/savings-report" in paths
|
||||
|
||||
|
||||
# ── 낭비 기준 상수 ────────────────────────────────────────────────────────────
|
||||
|
||||
def test_waste_thresholds():
|
||||
"""낭비 감지 기준값이 스펙에 맞는지 확인."""
|
||||
from routers.cost_optimizer_ai import (
|
||||
_WASTE_CPU_THRESHOLD,
|
||||
_WASTE_MEM_THRESHOLD,
|
||||
_WASTE_SR_DAYS,
|
||||
)
|
||||
assert _WASTE_CPU_THRESHOLD == 10.0, "CPU 임계값은 10%"
|
||||
assert _WASTE_MEM_THRESHOLD == 20.0, "메모리 임계값은 20%"
|
||||
assert _WASTE_SR_DAYS == 30, "SR 미발생 기준은 30일"
|
||||
|
||||
|
||||
# ── 절감 단가 상수 ────────────────────────────────────────────────────────────
|
||||
|
||||
def test_saving_unit_keys():
|
||||
"""절감 단가 딕셔너리에 server, license, cloud 키가 있는지 확인."""
|
||||
from routers.cost_optimizer_ai import _SAVING_UNIT
|
||||
assert "server" in _SAVING_UNIT
|
||||
assert "license" in _SAVING_UNIT
|
||||
assert "cloud" in _SAVING_UNIT
|
||||
# 모두 양수
|
||||
for key, val in _SAVING_UNIT.items():
|
||||
assert val > 0, f"{key} 절감 단가는 양수여야 함"
|
||||
|
||||
|
||||
# ── AI 텍스트 파싱 로직 ───────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_recommendations_from_ai_parses_numbered_list():
|
||||
"""번호 목록 형식 AI 텍스트가 CostRecommendation 리스트로 변환되는지 확인."""
|
||||
from routers.cost_optimizer_ai import _build_recommendations_from_ai
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
mock_db = MagicMock()
|
||||
|
||||
ai_text = (
|
||||
"1. 유휴 서버 통합 가상화\n"
|
||||
" CPU 사용률이 낮은 서버를 통합하세요.\n"
|
||||
"2. 미사용 라이선스 해지\n"
|
||||
" 분기마다 감사를 통해 절감하세요.\n"
|
||||
"3. 클라우드 스토리지 티어링\n"
|
||||
" cold 티어로 전환하세요.\n"
|
||||
)
|
||||
recs = await _build_recommendations_from_ai(ai_text, mock_db)
|
||||
assert len(recs) == 3, f"3개 권고가 생성되어야 함, 실제: {len(recs)}"
|
||||
assert "유휴 서버 통합 가상화" in recs[0].title
|
||||
assert "미사용 라이선스 해지" in recs[1].title
|
||||
assert "클라우드 스토리지 티어링" in recs[2].title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_recommendations_limits_to_5():
|
||||
"""6개 이상 항목이 있을 때 최대 5개로 제한되는지 확인."""
|
||||
from routers.cost_optimizer_ai import _build_recommendations_from_ai
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
mock_db = MagicMock()
|
||||
|
||||
ai_text = "\n".join(
|
||||
f"{i}. 권고 항목 {i}\n 설명 {i}." for i in range(1, 8)
|
||||
)
|
||||
recs = await _build_recommendations_from_ai(ai_text, mock_db)
|
||||
assert len(recs) <= 5, "최대 5개 제한 위반"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_recommendations_empty_text():
|
||||
"""빈 AI 텍스트 시 빈 리스트를 반환하는지 확인."""
|
||||
from routers.cost_optimizer_ai import _build_recommendations_from_ai
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
mock_db = MagicMock()
|
||||
recs = await _build_recommendations_from_ai("", mock_db)
|
||||
assert recs == []
|
||||
|
||||
|
||||
# ── Pydantic 스키마 검증 ──────────────────────────────────────────────────────
|
||||
|
||||
def test_recommendation_out_schema():
|
||||
"""RecommendationOut 스키마가 올바른 필드를 갖는지 확인."""
|
||||
from routers.cost_optimizer_ai import RecommendationOut
|
||||
fields = RecommendationOut.model_fields
|
||||
required = {"id", "category", "title", "estimated_saving", "risk_level",
|
||||
"auto_applicable", "status", "created_at"}
|
||||
for f in required:
|
||||
assert f in fields, f"RecommendationOut 필드 누락: {f}"
|
||||
|
||||
|
||||
def test_analysis_out_schema():
|
||||
"""AnalysisOut 스키마가 올바른 필드를 갖는지 확인."""
|
||||
from routers.cost_optimizer_ai import AnalysisOut
|
||||
fields = AnalysisOut.model_fields
|
||||
required = {"id", "period", "total_cost", "created_at"}
|
||||
for f in required:
|
||||
assert f in fields, f"AnalysisOut 필드 누락: {f}"
|
||||
|
||||
|
||||
def test_forecast_out_schema():
|
||||
"""ForecastOut 스키마가 올바른 필드를 갖는지 확인."""
|
||||
from routers.cost_optimizer_ai import ForecastOut
|
||||
fields = ForecastOut.model_fields
|
||||
required = {"id", "forecast_date", "predicted_cost", "confidence", "created_at"}
|
||||
for f in required:
|
||||
assert f in fields, f"ForecastOut 필드 누락: {f}"
|
||||
|
||||
|
||||
# ── ORM 모델 컬럼 검증 ────────────────────────────────────────────────────────
|
||||
|
||||
def test_cost_ai_analysis_columns():
|
||||
"""CostAIAnalysis 모델에 필수 컬럼이 있는지 확인."""
|
||||
from models import CostAIAnalysis
|
||||
cols = {c.key for c in CostAIAnalysis.__table__.columns}
|
||||
required = {"id", "period", "total_cost", "breakdown", "ai_insights",
|
||||
"waste_detected", "created_at"}
|
||||
for c in required:
|
||||
assert c in cols, f"CostAIAnalysis 컬럼 누락: {c}"
|
||||
|
||||
|
||||
def test_cost_recommendation_columns():
|
||||
"""CostRecommendation 모델에 필수 컬럼이 있는지 확인."""
|
||||
from models import CostRecommendation
|
||||
cols = {c.key for c in CostRecommendation.__table__.columns}
|
||||
required = {"id", "category", "title", "description", "estimated_saving",
|
||||
"risk_level", "auto_applicable", "status", "created_at"}
|
||||
for c in required:
|
||||
assert c in cols, f"CostRecommendation 컬럼 누락: {c}"
|
||||
|
||||
|
||||
def test_cost_forecast_columns():
|
||||
"""CostForecast 모델에 필수 컬럼이 있는지 확인."""
|
||||
from models import CostForecast
|
||||
cols = {c.key for c in CostForecast.__table__.columns}
|
||||
required = {"id", "forecast_date", "predicted_cost", "confidence",
|
||||
"factors", "created_at"}
|
||||
for c in required:
|
||||
assert c in cols, f"CostForecast 컬럼 누락: {c}"
|
||||
|
||||
|
||||
# ── 비즈니스 로직: 예측 수학적 검증 ─────────────────────────────────────────
|
||||
|
||||
def test_forecast_compound_growth():
|
||||
"""복리 성장률 계산 공식이 올바른지 수동 검증."""
|
||||
base_cost = 500.0 # 만원
|
||||
trend_rate = 0.02 # 월 2%
|
||||
month_ahead = 3
|
||||
|
||||
predicted = base_cost * ((1 + trend_rate) ** month_ahead)
|
||||
expected = 500.0 * (1.02 ** 3)
|
||||
|
||||
assert abs(predicted - expected) < 0.01, f"예측 계산 불일치: {predicted} != {expected}"
|
||||
|
||||
|
||||
def test_confidence_bounds():
|
||||
"""신뢰도가 0.3 ~ 0.95 범위 내인지 확인 (예측 공식 검증)."""
|
||||
import math
|
||||
|
||||
def calc_confidence(m: int, history: int) -> float:
|
||||
return max(0.3, min(0.95, 0.95 - 0.1 * m - (0.05 if history < 4 else 0)))
|
||||
|
||||
# 모든 케이스가 범위 내
|
||||
for m in range(1, 4):
|
||||
for h in (2, 4, 10):
|
||||
conf = calc_confidence(m, h)
|
||||
assert 0.3 <= conf <= 0.95, f"신뢰도 범위 초과: m={m} h={h} conf={conf}"
|
||||
215
tests/unit/test_predictive_capacity.py
Normal file
215
tests/unit/test_predictive_capacity.py
Normal file
@ -0,0 +1,215 @@
|
||||
"""
|
||||
단위 테스트: 예측 용량 계획 (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
|
||||
Loading…
Reference in New Issue
Block a user