zioinfo-mail/workspace/guardia-itsm/test_f2f3_cache.py
DESKTOP-TKLFCPR\ython cfe2901a55 refactor(structure): consolidate all projects under workspace/
- itsm/    -> workspace/guardia-itsm/
- manager/ -> workspace/guardia-manager/
- app/     -> workspace/guardia-messenger/
- manual/  -> workspace/guardia-docs/

workspace/zioinfo-web/ unchanged.
git mv preserves full commit history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:50:56 +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)