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