summaryrefslogtreecommitdiff
path: root/tests/test-wttrin--get-cached-or-fetch.el
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test-wttrin--get-cached-or-fetch.el')
-rw-r--r--tests/test-wttrin--get-cached-or-fetch.el403
1 files changed, 403 insertions, 0 deletions
diff --git a/tests/test-wttrin--get-cached-or-fetch.el b/tests/test-wttrin--get-cached-or-fetch.el
new file mode 100644
index 0000000..77a2689
--- /dev/null
+++ b/tests/test-wttrin--get-cached-or-fetch.el
@@ -0,0 +1,403 @@
+;;; test-wttrin--get-cached-or-fetch.el --- Tests for wttrin--get-cached-or-fetch -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin--get-cached-or-fetch function.
+;; Tests the core cache workflow: cache hits, misses, expiration, and error fallback.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Test Data Fixtures
+
+(defconst test-wttrin--get-cached-or-fetch-sample-weather
+ "Weather for Paris: Sunny 22°C"
+ "Sample weather data for testing.")
+
+(defconst test-wttrin--get-cached-or-fetch-new-weather
+ "Weather for Paris: Cloudy 18°C"
+ "Updated weather data for testing cache updates.")
+
+;;; Test Setup and Teardown
+
+(defun test-wttrin--get-cached-or-fetch-setup ()
+ "Setup for get-cached-or-fetch tests."
+ (testutil-wttrin-setup))
+
+(defun test-wttrin--get-cached-or-fetch-teardown ()
+ "Teardown for get-cached-or-fetch tests."
+ (testutil-wttrin-teardown))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--get-cached-or-fetch-normal-cache-hit-returns-cached-data ()
+ "Test that fresh cached data is returned without fetching."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location "Paris")
+ (cache-key (wttrin--make-cache-key location))
+ (now 1000.0)
+ (callback-result nil)
+ (fetch-called nil))
+ ;; Pre-populate cache with fresh data
+ (puthash cache-key (cons now test-wttrin--get-cached-or-fetch-sample-weather)
+ wttrin--cache)
+
+ ;; Mock time to be 100 seconds later (well within TTL of 900)
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () (+ now 100.0)))
+ ((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_location _callback)
+ (setq fetch-called t))))
+
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+
+ ;; Should return cached data immediately
+ (should (equal callback-result test-wttrin--get-cached-or-fetch-sample-weather))
+ ;; Should NOT call fetch
+ (should-not fetch-called)))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+(ert-deftest test-wttrin--get-cached-or-fetch-normal-cache-miss-fetches-new-data ()
+ "Test that missing cache entry triggers fetch and stores result."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location "London")
+ (cache-key (wttrin--make-cache-key location))
+ (now 1000.0)
+ (callback-result nil)
+ (fetch-called nil))
+
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () now))
+ ((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_location callback)
+ (setq fetch-called t)
+ (funcall callback test-wttrin--get-cached-or-fetch-new-weather)))
+ ((symbol-function 'wttrin--cleanup-cache-if-needed)
+ (lambda () nil)))
+
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+
+ ;; Should call fetch
+ (should fetch-called)
+ ;; Should return fresh data
+ (should (equal callback-result test-wttrin--get-cached-or-fetch-new-weather))
+ ;; Should store in cache
+ (let ((cached (gethash cache-key wttrin--cache)))
+ (should cached)
+ (should (equal (car cached) now))
+ (should (equal (cdr cached) test-wttrin--get-cached-or-fetch-new-weather)))))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+(ert-deftest test-wttrin--get-cached-or-fetch-normal-expired-cache-fetches-new-data ()
+ "Test that expired cache triggers fetch and updates cache."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location "Tokyo")
+ (cache-key (wttrin--make-cache-key location))
+ (old-time 1000.0)
+ (new-time (+ old-time 1000.0)) ; 1000 seconds later (> 900 TTL)
+ (callback-result nil)
+ (fetch-called nil))
+
+ ;; Pre-populate cache with old data
+ (puthash cache-key (cons old-time test-wttrin--get-cached-or-fetch-sample-weather)
+ wttrin--cache)
+
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () new-time))
+ ((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_location callback)
+ (setq fetch-called t)
+ (funcall callback test-wttrin--get-cached-or-fetch-new-weather)))
+ ((symbol-function 'wttrin--cleanup-cache-if-needed)
+ (lambda () nil)))
+
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+
+ ;; Should call fetch due to expiration
+ (should fetch-called)
+ ;; Should return new data
+ (should (equal callback-result test-wttrin--get-cached-or-fetch-new-weather))
+ ;; Should update cache timestamp
+ (let ((cached (gethash cache-key wttrin--cache)))
+ (should (equal (car cached) new-time))
+ (should (equal (cdr cached) test-wttrin--get-cached-or-fetch-new-weather)))))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--get-cached-or-fetch-boundary-exactly-at-ttl-fetches ()
+ "Test that cache exactly at TTL boundary triggers fetch."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location "Berlin")
+ (cache-key (wttrin--make-cache-key location))
+ (old-time 1000.0)
+ ;; Exactly at TTL boundary (900 seconds = wttrin-cache-ttl)
+ (new-time (+ old-time wttrin-cache-ttl))
+ (callback-result nil)
+ (fetch-called nil))
+
+ ;; Pre-populate cache
+ (puthash cache-key (cons old-time test-wttrin--get-cached-or-fetch-sample-weather)
+ wttrin--cache)
+
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () new-time))
+ ((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_location callback)
+ (setq fetch-called t)
+ (funcall callback test-wttrin--get-cached-or-fetch-new-weather)))
+ ((symbol-function 'wttrin--cleanup-cache-if-needed)
+ (lambda () nil)))
+
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+
+ ;; At exactly TTL, should fetch (not <)
+ (should fetch-called)
+ (should (equal callback-result test-wttrin--get-cached-or-fetch-new-weather))))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+(ert-deftest test-wttrin--get-cached-or-fetch-boundary-one-second-before-ttl-uses-cache ()
+ "Test that cache one second before TTL uses cached data."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location "Madrid")
+ (cache-key (wttrin--make-cache-key location))
+ (old-time 1000.0)
+ ;; One second before TTL expiration
+ (new-time (+ old-time (- wttrin-cache-ttl 1)))
+ (callback-result nil)
+ (fetch-called nil))
+
+ ;; Pre-populate cache
+ (puthash cache-key (cons old-time test-wttrin--get-cached-or-fetch-sample-weather)
+ wttrin--cache)
+
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () new-time))
+ ((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_location _callback)
+ (setq fetch-called t))))
+
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+
+ ;; Should use cache (still fresh)
+ (should-not fetch-called)
+ (should (equal callback-result test-wttrin--get-cached-or-fetch-sample-weather))))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+(ert-deftest test-wttrin--get-cached-or-fetch-boundary-force-refresh-bypasses-fresh-cache ()
+ "Test that force refresh flag bypasses fresh cache."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location "Rome")
+ (cache-key (wttrin--make-cache-key location))
+ (now 1000.0)
+ (callback-result nil)
+ (fetch-called nil)
+ (wttrin--force-refresh t)) ; Force refresh enabled
+
+ ;; Pre-populate cache with fresh data
+ (puthash cache-key (cons now test-wttrin--get-cached-or-fetch-sample-weather)
+ wttrin--cache)
+
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () (+ now 100.0))) ; Well within TTL
+ ((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_location callback)
+ (setq fetch-called t)
+ (funcall callback test-wttrin--get-cached-or-fetch-new-weather)))
+ ((symbol-function 'wttrin--cleanup-cache-if-needed)
+ (lambda () nil)))
+
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+
+ ;; Should fetch despite fresh cache
+ (should fetch-called)
+ (should (equal callback-result test-wttrin--get-cached-or-fetch-new-weather))))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+(ert-deftest test-wttrin--get-cached-or-fetch-boundary-empty-cache-fetches ()
+ "Test that completely empty cache triggers fetch."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location "Athens")
+ (callback-result nil)
+ (fetch-called nil))
+
+ ;; Cache is already empty from setup
+ (should (= (hash-table-count wttrin--cache) 0))
+
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () 1000.0))
+ ((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_location callback)
+ (setq fetch-called t)
+ (funcall callback test-wttrin--get-cached-or-fetch-new-weather)))
+ ((symbol-function 'wttrin--cleanup-cache-if-needed)
+ (lambda () nil)))
+
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+
+ ;; Should fetch
+ (should fetch-called)
+ (should (equal callback-result test-wttrin--get-cached-or-fetch-new-weather))))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+(ert-deftest test-wttrin--get-cached-or-fetch-boundary-cache-key-includes-unit-system ()
+ "Test that cache keys differentiate by unit system."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location "Oslo")
+ (now 1000.0)
+ (metric-data "Weather: 20°C")
+ (imperial-data "Weather: 68°F")
+ (callback-result nil))
+
+ ;; Cache metric version
+ (let ((wttrin-unit-system "m"))
+ (puthash (wttrin--make-cache-key location)
+ (cons now metric-data)
+ wttrin--cache))
+
+ ;; Cache imperial version
+ (let ((wttrin-unit-system "u"))
+ (puthash (wttrin--make-cache-key location)
+ (cons now imperial-data)
+ wttrin--cache))
+
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () (+ now 100.0))))
+
+ ;; Fetch with metric - should get metric cache
+ (let ((wttrin-unit-system "m"))
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+ (should (equal callback-result metric-data)))
+
+ ;; Fetch with imperial - should get imperial cache
+ (let ((wttrin-unit-system "u"))
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+ (should (equal callback-result imperial-data)))))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-wttrin--get-cached-or-fetch-error-fetch-fails-with-stale-cache-returns-stale ()
+ "Test that fetch failure with stale cache falls back to stale data."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location "Vienna")
+ (cache-key (wttrin--make-cache-key location))
+ (old-time 1000.0)
+ (new-time (+ old-time 2000.0)) ; Well expired
+ (callback-result nil)
+ (message-shown nil))
+
+ ;; Pre-populate cache with expired data
+ (puthash cache-key (cons old-time test-wttrin--get-cached-or-fetch-sample-weather)
+ wttrin--cache)
+
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () new-time))
+ ((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_location callback)
+ ;; Simulate network failure
+ (funcall callback nil)))
+ ((symbol-function 'wttrin--cleanup-cache-if-needed)
+ (lambda () nil))
+ ((symbol-function 'message)
+ (lambda (format-string &rest _args)
+ (setq message-shown format-string))))
+
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+
+ ;; Should fall back to stale cache
+ (should (equal callback-result test-wttrin--get-cached-or-fetch-sample-weather))
+ ;; Should show message about using cached version
+ (should message-shown)
+ (should (string-match-p "cached" message-shown))))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+(ert-deftest test-wttrin--get-cached-or-fetch-error-fetch-fails-with-no-cache-returns-nil ()
+ "Test that fetch failure with no cache returns nil."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location "Dublin")
+ (callback-result 'unset))
+
+ ;; Cache is empty
+ (should (= (hash-table-count wttrin--cache) 0))
+
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () 1000.0))
+ ((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_location callback)
+ ;; Simulate network failure
+ (funcall callback nil)))
+ ((symbol-function 'wttrin--cleanup-cache-if-needed)
+ (lambda () nil)))
+
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+
+ ;; Should return nil (no fallback available)
+ (should (null callback-result))))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+(ert-deftest test-wttrin--get-cached-or-fetch-error-nil-location-creates-cache-key ()
+ "Test that nil location is handled (creates cache key with nil)."
+ (test-wttrin--get-cached-or-fetch-setup)
+ (unwind-protect
+ (let* ((location nil)
+ (callback-result nil)
+ (fetch-called nil))
+
+ (cl-letf (((symbol-function 'float-time)
+ (lambda () 1000.0))
+ ((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_location callback)
+ (setq fetch-called t)
+ (funcall callback test-wttrin--get-cached-or-fetch-new-weather)))
+ ((symbol-function 'wttrin--cleanup-cache-if-needed)
+ (lambda () nil)))
+
+ (wttrin--get-cached-or-fetch
+ location
+ (lambda (data) (setq callback-result data)))
+
+ ;; Should attempt to fetch (nil is a valid location input)
+ (should fetch-called)
+ (should (equal callback-result test-wttrin--get-cached-or-fetch-new-weather))))
+ (test-wttrin--get-cached-or-fetch-teardown)))
+
+(provide 'test-wttrin--get-cached-or-fetch)
+;;; test-wttrin--get-cached-or-fetch.el ends here