zioinfo-mail/itsm/test_f2f3_cache.py
DESKTOP-TKLFCPR\ython e228faabf5 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현
G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건)
G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST)
G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직
G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트
G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트
G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API
G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트
G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트
G-10: core/push_notify.py + routers/push.py + PushSubscription 모델
G-11: approvals 다중승인 (위임/서명/기한초과/마감연장)
G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh

하네스: guardia-orchestrator 확장기능 Phase 반영
봇명령어: /sr /status /license /bulk 슬래시 명령어 추가
설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:18:52 +09:00

159 lines
5.0 KiB
Python

"""F-2 Redis 캐시 / F-3 Rate Limiting 테스트"""
import sys, ast, os, asyncio
os.environ.setdefault("GUARDIA_SECRET_KEY", "test-cache-secret-32bytes-pad!!!")
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_cache.db")
os.environ["CACHE_ENABLED"] = "true"
print("=== 1. 구문 검사 ===")
files = ["core/cache.py", "core/ratelimit.py", "routers/analytics.py", "main.py"]
ok = True
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
if not ok:
sys.exit(1)
print("\n=== 2. 인메모리 캐시 단위 테스트 ===")
from core.cache import _MemoryCache
mc = _MemoryCache(maxsize=5)
# set / get
mc.set("key1", {"data": 42}, ttl=60)
val = mc.get("key1")
assert val == {"data": 42}, f"Expected dict, got {val}"
print(" OK set/get")
# TTL 만료 테스트 (0초 TTL)
import time
mc.set("key_exp", "will expire", ttl=0)
time.sleep(0.01)
assert mc.get("key_exp") is None, "TTL 0 항목이 만료되지 않음"
print(" OK TTL 만료")
# delete
mc.set("del_key", "to delete", ttl=60)
mc.delete("del_key")
assert mc.get("del_key") is None
print(" OK delete")
# LRU 초과 시 가장 오래된 항목 제거 (maxsize=5)
for i in range(6):
mc.set(f"lru_{i}", i, ttl=60)
# lru_0이 제거됨
assert mc.get("lru_0") is None, "LRU 항목이 제거되지 않음"
assert mc.get("lru_5") == 5, "최신 항목이 제거됨"
print(" OK LRU 제거")
# prefix 키 조회
for i in range(3):
mc.set(f"prefix:item:{i}", i, ttl=60)
keys = mc.keys_with_prefix("prefix:")
assert len(keys) == 3, f"Expected 3 keys, got {len(keys)}"
print(f" OK prefix 키 조회: {len(keys)}")
print("\n=== 3. 비동기 캐시 API 테스트 (인메모리) ===")
# Redis 없이 메모리 캐시만 사용
os.environ["REDIS_URL"] = "redis://localhost:99999/0" # 연결 불가 주소
async def test_async_cache():
from core.cache import cache_get, cache_set, cache_delete, cache_invalidate_prefix, make_cache_key
# set/get
await cache_set("test:item1", {"hello": "world"}, ttl=60)
val = await cache_get("test:item1")
assert val == {"hello": "world"}, f"Expected dict, got {val}"
print(" OK async cache_set/cache_get")
# 없는 키
val_none = await cache_get("nonexistent:key")
assert val_none is None
print(" OK cache_get None for missing key")
# delete
await cache_set("test:del", "value", ttl=60)
await cache_delete("test:del")
assert await cache_get("test:del") is None
print(" OK async cache_delete")
# invalidate prefix
for i in range(3):
await cache_set(f"test:pfx:{i}", i, ttl=60)
count = await cache_invalidate_prefix("test:pfx:")
assert count == 3, f"Expected 3 deletions, got {count}"
print(f" OK cache_invalidate_prefix: {count}개 삭제")
# make_cache_key
k1 = make_cache_key("tasks", status="OPEN", skip=0)
k2 = make_cache_key("tasks", status="OPEN", skip=0)
k3 = make_cache_key("tasks", status="CLOSED", skip=0)
assert k1 == k2, "동일 인자 → 동일 키"
assert k1 != k3, "다른 인자 → 다른 키"
print(f" OK make_cache_key: {k1}")
# cache_info
from core.cache import cache_info
info = await cache_info()
assert "backend" in info
assert "app_stats" in info
print(f" OK cache_info: backend={info['backend']}")
asyncio.run(test_async_cache())
print("\n=== 4. Rate Limiter 임포트/초기화 테스트 ===")
from core.ratelimit import (
create_limiter, limiter, setup_rate_limiting,
DEFAULT_LIMIT, LOGIN_LIMIT, AI_LIMIT, UPLOAD_LIMIT,
_get_user_key, _DummyLimiter,
)
print(f" OK limiter type: {type(limiter).__name__}")
assert DEFAULT_LIMIT == "120/minute"
assert LOGIN_LIMIT == "10/minute"
assert AI_LIMIT == "10/minute"
print(f" OK 제한 상수: DEFAULT={DEFAULT_LIMIT}, LOGIN={LOGIN_LIMIT}, AI={AI_LIMIT}")
# 더미 리미터 작동 확인
dummy = _DummyLimiter()
@dummy.limit("10/minute")
async def dummy_fn():
return "ok"
result = asyncio.run(dummy_fn())
assert result == "ok", "DummyLimiter 데코레이터 실패"
print(f" OK DummyLimiter 데코레이터 (no-op)")
print("\n=== 5. analytics.py 캐시/레이트리밋 엔드포인트 확인 ===")
with open("routers/analytics.py", encoding="utf-8") as f:
src = f.read()
for endpoint in ["/admin/cache/info", "/admin/cache/flush", "/admin/ratelimit/info"]:
status = "OK" if endpoint in src else "ERR"
if status == "ERR":
ok = False
print(f" {status} {endpoint}")
print("\n=== 6. main.py 통합 확인 ===")
with open("main.py", encoding="utf-8") as f:
main_src = f.read()
checks = [
("setup_rate_limiting", "Rate Limiting 미들웨어"),
("close_redis", "Redis 종료 훅"),
]
for sym, desc in checks:
status = "OK" if sym in main_src else "ERR"
if status == "ERR":
ok = False
print(f" {status} {desc} ({sym})")
print("\n=== F-2/F-3 테스트 완료 ===")
if ok:
print("모든 검사 통과")
else:
sys.exit(1)