"""Tests for scripts/cache.py — SQLite-backed result cache. Covers: cache key stability, key whitelist (format/auth excluded), get/put roundtrip, TTL expiry, caller-shorter-TTL override, clear, stats, and the ttl<=0 disable behavior. """ import sqlite3 import time import cache # ----- _make_key ----- def test_make_key_stable(): p = {"q": "test", "engines": "google,bing", "format": "json"} k1 = cache._make_key(p) k2 = cache._make_key(p) assert k1 == k2 assert len(k1) == 64 # SHA-256 hex digest length def test_make_key_excludes_format(): """format is transient (output shape, not result set) — must not affect key.""" p1 = {"q": "test", "format": "json"} p2 = {"q": "test", "format": "brief"} assert cache._make_key(p1) == cache._make_key(p2) def test_make_key_differs_on_query(): assert cache._make_key({"q": "python"}) != cache._make_key({"q": "rust"}) def test_make_key_differs_on_engines(): assert cache._make_key({"q": "x", "engines": "google"}) != \ cache._make_key({"q": "x", "engines": "bing"}) def test_make_key_differs_on_time_range(): assert cache._make_key({"q": "x", "time_range": "day"}) != \ cache._make_key({"q": "x", "time_range": "year"}) # ----- get / put ----- def test_put_get_roundtrip(isolated_cache): params = {"q": "hello", "format": "json"} result = {"results": [{"title": "Hi", "url": "https://example.com"}], "number_of_results": 1} cache.put(params, result, ttl_seconds=60) got = cache.get(params, ttl_seconds=60) assert got is not None assert got["results"][0]["title"] == "Hi" def test_get_miss_when_empty(isolated_cache): assert cache.get({"q": "nope"}, ttl_seconds=60) is None def test_get_expired(isolated_cache): """Entry older than TTL is a miss.""" params = {"q": "old", "format": "json"} cache.put(params, {"results": []}, ttl_seconds=60) # Backdate the entry so it's past TTL with cache._connect(cache._cache_path()) as conn: conn.execute( "UPDATE search_cache SET created_at = ? WHERE key = ?", (time.time() - 120, cache._make_key(params)), ) conn.commit() assert cache.get(params, ttl_seconds=60) is None def test_get_caller_shorter_ttl_expires(isolated_cache): """Caller's shorter TTL overrides a longer stored TTL (immediate effect).""" params = {"q": "ttl-test", "format": "json"} cache.put(params, {"results": []}, ttl_seconds=3600) with cache._connect(cache._cache_path()) as conn: conn.execute( "UPDATE search_cache SET created_at = ? WHERE key = ?", (time.time() - 120, cache._make_key(params)), ) conn.commit() # stored TTL=3600 (still fresh by stored clock) but caller asks 60s -> expired assert cache.get(params, ttl_seconds=60) is None def test_get_caller_longer_ttl_uses_stored(isolated_cache): """Caller's longer TTL does NOT resurrect an expired-by-stored entry.""" params = {"q": "ttl2", "format": "json"} cache.put(params, {"results": []}, ttl_seconds=60) with cache._connect(cache._cache_path()) as conn: conn.execute( "UPDATE search_cache SET created_at = ? WHERE key = ?", (time.time() - 120, cache._make_key(params)), ) conn.commit() # stored TTL=60 (expired), caller asks 3600 -> effective = min(3600,60)=60 -> expired assert cache.get(params, ttl_seconds=3600) is None # ----- clear ----- def test_clear_removes_entries(isolated_cache): cache.put({"q": "x"}, {"results": []}, ttl_seconds=60) removed = cache.clear() assert removed == 1 assert cache.get({"q": "x"}, ttl_seconds=60) is None def test_clear_empty_returns_zero(isolated_cache): assert cache.clear() == 0 # ----- stats ----- def test_stats_reports_entries(isolated_cache): cache.put({"q": "stats-test"}, {"results": []}, ttl_seconds=60) s = cache.stats() assert s["entries"] == 1 assert s["size_bytes"] > 0 assert "path" in s assert s.get("oldest_created_at") is not None assert s.get("newest_created_at") is not None def test_stats_empty(isolated_cache): s = cache.stats() assert s["entries"] == 0 # ----- ttl<=0 disable ----- def test_get_ttl_zero_is_disabled(isolated_cache): """ttl_seconds=0 means caching off -> always miss even if stored.""" cache.put({"q": "disabled"}, {"results": []}, ttl_seconds=60) assert cache.get({"q": "disabled"}, ttl_seconds=0) is None def test_put_ttl_zero_is_noop(isolated_cache): """put with ttl<=0 should not store anything.""" cache.put({"q": "noop"}, {"results": []}, ttl_seconds=0) assert cache.get({"q": "noop"}, ttl_seconds=60) is None assert cache.stats()["entries"] == 0