"""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)