"""E-5 FinOps 비용 분석 테스트""" import sys, ast, os, re, json os.environ.setdefault("GUARDIA_SECRET_KEY", "test-e5-secret-key-32bytes-padded!") os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_e5.db") ok = True print("=== 1. 구문 검사 ===") files = ["routers/finops.py", "main.py"] for f in files: try: with open(f, encoding="utf-8") as fh: src = fh.read() ast.parse(src) print(f" OK {f}") except SyntaxError as e: print(f" ERR {f}: {e}") ok = False print("\n=== 2. routers/finops.py 엔드포인트 확인 ===") with open("routers/finops.py", encoding="utf-8") as f: finops_src = f.read() checks = [ ('@router.post("/costs"', "POST /costs 비용 등록"), ('@router.get("/costs"', "GET /costs 비용 목록"), ('@router.get("/summary"', "GET /summary 월별 요약"), ('@router.get("/trend"', "GET /trend 트렌드"), ('@router.get("/allocation"', "GET /allocation 배분"), ('@router.get("/anomalies"', "GET /anomalies 이상 탐지"), ('@router.get("/recommendations"', "GET /recommendations 권고"), ('@router.post("/budget"', "POST /budget 예산 등록"), ('@router.get("/budget"', "GET /budget 예산 대비"), ('@router.get("/optimize"', "GET /optimize AI 최적화"), ("COST_CATEGORIES", "COST_CATEGORIES 카테고리 정의"), ("SERVICES", "SERVICES 서비스 목록"), ("_costs", "_costs 인메모리 저장소"), ("_budgets", "_budgets 예산 저장소"), ("_gen_cost_id", "_gen_cost_id ID 생성"), ("_filter_costs", "_filter_costs 필터 함수"), ("_sum_costs", "_sum_costs 합계 함수"), ("mom_change_pct", "mom_change_pct 전월 비교"), ("usage_pct", "usage_pct 예산 사용률"), ("potential_saving_pct", "potential_saving_pct 절감 가능률"), ("localhost:11434", "Ollama 내부 LLM (외부 API 금지)"), ("UserRole.ADMIN", "ADMIN 권한 제한"), ("UserRole.PM", "PM 권한 제한"), ("trend_dir", "trend_dir UP/DOWN/STABLE"), ("COST-", "COST- ID 접두사"), ("variance", "variance 예산 편차"), ] for sym, desc in checks: status = "OK" if sym in finops_src else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 3. main.py E-5 라우터 등록 확인 ===") with open("main.py", encoding="utf-8") as f: main_src = f.read() main_checks = [ ("finops," in main_src or "finops\n" in main_src, "finops 임포트"), ("finops.router" in main_src, "finops.router 등록"), ("E-5" in main_src, "E-5 주석"), ] for check, desc in main_checks: status = "OK" if check else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 4. 비용 카테고리 및 서비스 구성 검증 ===") try: # 로컬 정의 COST_CATEGORIES = { "SERVER": "서버 운영비", "NETWORK": "네트워크/통신비", "STORAGE": "스토리지 비용", "LICENSE": "소프트웨어 라이선스", "MAINTENANCE": "유지보수비", "PERSONNEL": "인건비", "OTHER": "기타", } SERVICES = ["MES", "ERP", "GROUPWARE", "SECURITY", "INFRA", "BACKUP", "OTHER"] assert len(COST_CATEGORIES) >= 6, f"카테고리 6개 이상 필요: {len(COST_CATEGORIES)}" assert len(SERVICES) >= 5, f"서비스 5개 이상 필요: {len(SERVICES)}" assert "SERVER" in COST_CATEGORIES, "SERVER 카테고리 없음" assert "LICENSE" in COST_CATEGORIES, "LICENSE 카테고리 없음" assert "INFRA" in SERVICES, "INFRA 서비스 없음" print(f" OK 카테고리 {len(COST_CATEGORIES)}개: {list(COST_CATEGORIES)}") print(f" OK 서비스 {len(SERVICES)}개: {SERVICES}") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 5. 비용 집계 로직 검증 ===") try: # _sum_costs 로직 재현 def sum_costs(items): return round(sum(c["amount"] for c in items), 2) def filter_costs(costs, year, month=None, service=None): result = [] for c in costs.values(): if c["year"] != year: continue if month is not None and c["month"] != month: continue if service is not None and c["service"] != service: continue result.append(c) return result costs_db = { "C1": {"year": 2026, "month": 5, "category": "SERVER", "service": "INFRA", "amount": 1500000}, "C2": {"year": 2026, "month": 5, "category": "LICENSE", "service": "ERP", "amount": 800000}, "C3": {"year": 2026, "month": 5, "category": "NETWORK", "service": "INFRA", "amount": 300000}, "C4": {"year": 2026, "month": 4, "category": "SERVER", "service": "INFRA", "amount": 1400000}, } may_items = filter_costs(costs_db, 2026, 5) assert len(may_items) == 3, f"5월 항목 3개 기대: {len(may_items)}" assert sum_costs(may_items) == 2600000, f"5월 합계 오류: {sum_costs(may_items)}" print(f" OK 5월 필터: {len(may_items)}건, 합계: {sum_costs(may_items):,}원") infra_items = filter_costs(costs_db, 2026, 5, "INFRA") assert len(infra_items) == 2, f"INFRA 5월 2건 기대: {len(infra_items)}" assert sum_costs(infra_items) == 1800000, f"INFRA 합계 오류" print(f" OK INFRA 5월 필터: {len(infra_items)}건, {sum_costs(infra_items):,}원") # 전월 비교 apr_total = sum_costs(filter_costs(costs_db, 2026, 4)) may_total = sum_costs(filter_costs(costs_db, 2026, 5)) mom_change = round((may_total - apr_total) / apr_total * 100, 1) assert mom_change > 0, f"전월 대비 증가 기대: {mom_change}%" print(f" OK 전월 비교: {apr_total:,}원 -> {may_total:,}원 ({mom_change:+.1f}%)") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 6. 예산 대비 실적 검증 ===") try: def calc_budget_status(actual, budget): if budget <= 0: return "NO_BUDGET", 0.0 usage_pct = round(actual / budget * 100, 1) if usage_pct > 110: return "OVER", usage_pct elif usage_pct > 90: return "WARNING", usage_pct else: return "OK", usage_pct tests = [ (900000, 1000000, "OK", 90.0), (950000, 1000000, "WARNING", 95.0), (1150000,1000000, "OVER", 115.0), (0, 0, "NO_BUDGET", 0.0), ] for actual, budget, exp_status, exp_pct in tests: status, pct = calc_budget_status(actual, budget) assert status == exp_status, f"상태 오류: actual={actual}, budget={budget}, got={status}" assert pct == exp_pct, f"사용률 오류: {pct} (기대: {exp_pct})" print(f" OK 예산 상태: OK/WARNING/OVER/NO_BUDGET 모두 정확") # variance 계산 actual = 1150000 budget = 1000000 variance = round(actual - budget, 2) assert variance == 150000, f"variance 오류: {variance}" remaining = round(budget - actual, 2) assert remaining == -150000, f"remaining 오류: {remaining}" print(f" OK variance={variance:,}원, remaining={remaining:,}원 (초과)") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 7. 이상 탐지 로직 검증 ===") try: def detect_anomalies(curr_amt, prev_amt, threshold=30.0): if prev_amt <= 0: return None change_pct = round((curr_amt - prev_amt) / prev_amt * 100, 1) if abs(change_pct) >= threshold: return { "change_pct": change_pct, "direction": "UP" if change_pct > 0 else "DOWN", "severity": "CRITICAL" if abs(change_pct) >= 50 else "WARNING", } return None # 50% 급증 → CRITICAL anomaly = detect_anomalies(1500000, 1000000, 30.0) assert anomaly is not None, "50% 급증 탐지 실패" assert anomaly["severity"] == "CRITICAL", f"CRITICAL 기대: {anomaly['severity']}" assert anomaly["direction"] == "UP", "방향 오류" print(f" OK 50% 급증 탐지: {anomaly['change_pct']}% CRITICAL") # 35% 증가 → WARNING anomaly2 = detect_anomalies(1350000, 1000000, 30.0) assert anomaly2 is not None, "35% 증가 탐지 실패" assert anomaly2["severity"] == "WARNING", f"WARNING 기대: {anomaly2['severity']}" print(f" OK 35% 증가 탐지: {anomaly2['change_pct']}% WARNING") # 10% 변동 → 탐지 안 함 anomaly3 = detect_anomalies(1100000, 1000000, 30.0) assert anomaly3 is None, "10% 변동 오탐" print(f" OK 10% 변동 미탐지 (정상 범위)") # 급감도 탐지 anomaly4 = detect_anomalies(400000, 1000000, 30.0) assert anomaly4 is not None assert anomaly4["direction"] == "DOWN", "급감 방향 오류" print(f" OK 60% 급감 탐지: {anomaly4['change_pct']}% DOWN") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 8. 비용 트렌드 방향 판정 검증 ===") try: def trend_direction(first_amt, last_amt): if first_amt <= 0: return "STABLE" trend_pct = round((last_amt - first_amt) / first_amt * 100, 1) return "UP" if trend_pct > 5 else "DOWN" if trend_pct < -5 else "STABLE" assert trend_direction(1000000, 1200000) == "UP", "20% 증가 UP 기대" assert trend_direction(1000000, 800000) == "DOWN", "20% 감소 DOWN 기대" assert trend_direction(1000000, 1030000) == "STABLE", "3% 변화 STABLE 기대" assert trend_direction(0, 1000000) == "STABLE", "첫달 0 STABLE 기대" print(f" OK 트렌드 방향 판정 UP/DOWN/STABLE 모두 정확") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 9. 비용 ID 포맷 검증 ===") try: import re as re_mod from datetime import datetime as dt now = dt.utcnow() fake_id = f"COST-{now.strftime('%Y%m%d')}-ABCDEF" pattern = re_mod.compile(r"COST-\d{8}-[A-Z0-9]{6}") assert pattern.match(fake_id), f"ID 포맷 불일치: {fake_id}" assert fake_id.startswith("COST-"), "접두사 오류" print(f" OK 비용 ID 포맷: {fake_id}") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 10. 보안 정책 확인 ===") sec_checks = [ ("localhost:11434" in finops_src, "Ollama 내부 LLM 사용"), ("openai" not in finops_src.lower(), "외부 OpenAI 미사용"), ("anthropic" not in finops_src.lower(), "외부 Anthropic 미사용"), ("ip_addr" not in finops_src, "IP 원본 미포함"), ("os_pw" not in finops_src, "서버 자격증명 미포함"), ("disclaimer" in finops_src, "AI 분석 면책 조항 포함"), ("UserRole.ADMIN" in finops_src, "ADMIN 전용 기능 존재"), ] for check, desc in sec_checks: status = "OK" if check else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== E-5 FinOps 비용 분석 테스트 완료 ===") if ok: print("모든 검사 통과") else: sys.exit(1)