FlyRates / scratch /test_fx_service.py
Sadeep Sachintha
feat: implement key-less FX service with native CBSL web scraping and fallback cache logic
f53ddba
import asyncio
import os
import json
import time
from services.fx_service import fx_service, CACHE_FILE
async def test_fx_service():
print("==================================================")
print(" VERIFYING NEW FXSERVICE ARCHITECTURE")
print("==================================================")
# 1. Clean up existing rate cache to test cold startup
if os.path.exists(CACHE_FILE):
print(f"Removing existing cache file: {CACHE_FILE}")
os.remove(CACHE_FILE)
# Reload empty cache
fx_service.load_cache()
print("Cache has been cleared in memory and on disk.\n")
# 2. Test cold fetch of USD -> LKR (should trigger CBSL scraping)
print("--- 1. Cold Fetch USD -> LKR (CBSL Scraper check) ---")
start_time = time.time()
rate_usd = await fx_service.get_rate("USD", "LKR")
elapsed = time.time() - start_time
print(f"USD -> LKR Rate: {rate_usd} (Fetched in {elapsed:.4f} seconds)")
assert rate_usd is not None and rate_usd > 200, f"Expected valid LKR rate, got: {rate_usd}"
print("Cold fetch PASSED.\n")
# 3. Test subsequent fetches for other currencies (should all be instant Cache HITs!)
print("--- 2. Instant Cache Hits for EUR, GBP, AUD, JPY ---")
currencies = ["EUR", "GBP", "AUD", "JPY", "INR", "CNY", "SAR", "AED"]
for cur in currencies:
start_time = time.time()
rate = await fx_service.get_rate(cur, "LKR")
elapsed = time.time() - start_time
print(f" {cur} -> LKR Rate: {rate} (Cache HIT resolved in {elapsed * 1000:.4f} ms)")
assert rate is not None and rate > 0, f"Expected valid rate for {cur}, got: {rate}"
assert elapsed < 0.05, f"Expected cache hit to resolve instantly, took {elapsed:.4f}s"
print("Instant Cache Hits PASSED.\n")
# 4. Check persistence on disk
print("--- 3. Disk Cache Persistence (rate_cache.json) ---")
assert os.path.exists(CACHE_FILE), "Cache file should be saved on disk"
with open(CACHE_FILE, "r") as f:
cache_data = json.load(f)
print(f"Cache file contains {len(cache_data)} entries.")
print("Sample cache content keys:", list(cache_data.keys())[:5])
print("Cache Persistence PASSED.\n")
# 5. Test cross-rate bridge derivation (USD -> EUR)
print("--- 4. Bridge Rate Calculation (USD -> EUR) ---")
start_time = time.time()
cross_rate = await fx_service.get_rate("USD", "EUR")
elapsed = time.time() - start_time
# Should be mathematically derived: USD_LKR / EUR_LKR = 324.7184 / 378.3294 = ~0.858
usd_lkr = cache_data.get("USD_LKR", {}).get("rate")
eur_lkr = cache_data.get("EUR_LKR", {}).get("rate")
expected_cross = usd_lkr / eur_lkr
print(f"Derived USD -> EUR Rate: {cross_rate} (Expected: {expected_cross})")
print(f"Resolved in {elapsed * 1000:.4f} ms")
assert abs(cross_rate - expected_cross) < 1e-4, "Derived rate mismatch"
print("Bridge Rate Calculation PASSED.\n")
# 6. Test Cache Expiration and Stale Fallback
print("--- 5. Cache Expiration & Stale Fallback ---")
# Manually expire the cache in memory by setting timestamps to 0
for key in fx_service.cache:
fx_service.cache[key]["timestamp"] = 0
# We will simulate a network/scraping failure by temporarily breaking CBSL URL
original_fetch = fx_service.fetch_cbsl_rates
async def broken_fetch():
print(" [Simulated Scraper Failure]")
return False
fx_service.fetch_cbsl_rates = broken_fetch
print("Cache is expired. External scraper is simulated as down.")
start_time = time.time()
stale_rate = await fx_service.get_rate("USD", "LKR")
elapsed = time.time() - start_time
print(f"Stale Fallback USD -> LKR Rate: {stale_rate} (Resolved in {elapsed:.4f}s)")
assert stale_rate == rate_usd, "Stale rate should equal last fetched rate"
print("Stale Fallback PASSED.\n")
# Restore original methods
fx_service.fetch_cbsl_rates = original_fetch
print("==================================================")
print(" ALL FXSERVICE TESTS PASSED!")
print("==================================================")
if __name__ == "__main__":
asyncio.run(test_fx_service())