- 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>
159 lines
5.0 KiB
Python
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)
|