#!/usr/bin/env python3 """Test Redis stock cache integration. Validates: 1. Redis connectivity 2. Cache miss → PostgreSQL fallback → cache set 3. Cache hit (sub-millisecond) 4. Invalidation on stock mutation 5. Graceful degradation when Redis is unavailable """ import os import sys import time import warnings sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # Must set env vars before importing config os.environ.setdefault('MASTER_DB_URL', 'postgresql://postgres@/nexus_autoparts') os.environ.setdefault('TENANT_DB_URL_TEMPLATE', 'postgresql://postgres@/{db_name}') os.environ.setdefault('POS_JWT_SECRET', 'test-secret-12345678901234567890123456789012') from services.redis_stock_cache import ( get_cached_stock, set_cached_stock, invalidate_stock, invalidate_all_stock, health_check, _get_redis ) from services.inventory_engine import get_stock, record_sale, record_purchase from tenant_db import get_tenant_conn_by_dbname RED = '\033[91m' GREEN = '\033[92m' YELLOW = '\033[93m' RESET = '\033[0m' def print_result(name, passed, detail=""): status = f"{GREEN}PASS{RESET}" if passed else f"{RED}FAIL{RESET}" print(f" [{status}] {name}" + (f" — {detail}" if detail else "")) def main(): print("=" * 60) print("REDIS STOCK CACHE — VALIDATION SUITE") print("=" * 60) passed = 0 failed = 0 # ── Test 1: Redis connectivity ────────────────────────────── print("\n[1] Redis Connectivity") if health_check(): print_result("Redis PING", True, "responding") passed += 1 else: print_result("Redis PING", False, "no response") failed += 1 print(f"\n{RED}Redis unavailable — aborting remaining tests{RESET}") return passed, failed # ── Test 2: Basic cache operations ────────────────────────── print("\n[2] Basic Cache Operations") set_cached_stock(99999, 42, branch_id=1) cached = get_cached_stock(99999, branch_id=1) if cached == 42: print_result("SET + GET", True, f"value={cached}") passed += 1 else: print_result("SET + GET", False, f"expected 42, got {cached}") failed += 1 invalidate_stock(99999, branch_id=1) cached_after = get_cached_stock(99999, branch_id=1) if cached_after is None: print_result("Invalidation", True, "key removed") passed += 1 else: print_result("Invalidation", False, f"expected None, got {cached_after}") failed += 1 # ── Test 3: get_stock cache miss / hit ────────────────────── print("\n[3] get_stock() with PostgreSQL fallback") conn = get_tenant_conn_by_dbname('tenant_acct_test') # Ensure we have at least one inventory item with operations cur = conn.cursor() cur.execute("SELECT id FROM inventory WHERE is_active = true LIMIT 1") row = cur.fetchone() cur.close() if not row: print(f" {YELLOW}SKIP{RESET} No inventory items in tenant_acct_test") else: inv_id = row[0] # Clear any existing cache invalidate_stock(inv_id, None) # Miss (should query PostgreSQL) t0 = time.perf_counter() stock_miss = get_stock(conn, inv_id) t_miss = (time.perf_counter() - t0) * 1000 # Hit (should read from Redis) t0 = time.perf_counter() stock_hit = get_stock(conn, inv_id) t_hit = (time.perf_counter() - t0) * 1000 if stock_miss == stock_hit: print_result("Consistency", True, f"PG={stock_miss}, Redis={stock_hit}") passed += 1 else: print_result("Consistency", False, f"PG={stock_miss}, Redis={stock_hit}") failed += 1 print(f" Cache miss: {t_miss:.3f} ms") print(f" Cache hit: {t_hit:.3f} ms") if t_hit < t_miss: print_result("Performance", True, f"hit {t_hit:.3f}ms < miss {t_miss:.3f}ms") passed += 1 else: print_result("Performance", False, "cache hit not faster") failed += 1 conn.close() # ── Test 4: Invalidation on mutation ──────────────────────── print("\n[4] Invalidation on stock mutation") conn = get_tenant_conn_by_dbname('tenant_acct_test') cur = conn.cursor() cur.execute("SELECT id FROM inventory WHERE is_active = true LIMIT 1") row = cur.fetchone() cur.close() if not row: print(f" {YELLOW}SKIP{RESET} No inventory items available") else: inv_id = row[0] # Pre-populate cache invalidate_stock(inv_id, None) stock_before = get_stock(conn, inv_id) set_cached_stock(inv_id, stock_before) # Verify cache hit cached_before = get_cached_stock(inv_id) if cached_before != stock_before: print_result("Pre-populate", False, f"cache mismatch") failed += 1 else: # Record a sale (negative operation) # Need a valid branch_id cur = conn.cursor() cur.execute("SELECT id FROM branches LIMIT 1") branch_row = cur.fetchone() cur.close() branch_id = branch_row[0] if branch_row else None if branch_id: record_sale(conn, inv_id, branch_id, 1) conn.commit() # Cache should be invalidated cached_after = get_cached_stock(inv_id) if cached_after is None: print_result("Auto-invalidation", True, "cleared on record_sale") passed += 1 else: print_result("Auto-invalidation", False, f"still cached: {cached_after}") failed += 1 else: print(f" {YELLOW}SKIP{RESET} No branches available") conn.close() # ── Test 5: Bulk stock population ─────────────────────────── print("\n[5] get_stock_bulk() populates Redis") conn = get_tenant_conn_by_dbname('tenant_acct_test') from services.inventory_engine import get_stock_bulk stock_map = get_stock_bulk(conn) if stock_map: sample_id = list(stock_map.keys())[0] cached = get_cached_stock(sample_id) if cached is not None: print_result("Bulk populate", True, f"{len(stock_map)} items cached") passed += 1 else: print_result("Bulk populate", False, "sample not in cache") failed += 1 else: print(f" {YELLOW}SKIP{RESET} No stock data") conn.close() # ── Summary ───────────────────────────────────────────────── print("\n" + "=" * 60) print(f"RESULTS: {GREEN}{passed} passed{RESET}, {RED}{failed} failed{RESET}") print("=" * 60) return passed, failed if __name__ == '__main__': passed, failed = main() sys.exit(0 if failed == 0 else 1)