aboutsummaryrefslogtreecommitdiff
path: root/tests/test-wttrin-set-location-from-geolocation.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-22 00:07:51 -0500
committerCraig Jennings <c@cjennings.net>2026-04-22 00:07:51 -0500
commit9958ec4c4396ae8435f7e1818ff383c05df47a14 (patch)
tree9835229246368cca582f669837cd2859a79c8862 /tests/test-wttrin-set-location-from-geolocation.el
parent603f70d78f771c0a14c7f312aee6da68060b5d8b (diff)
downloademacs-wttrin-9958ec4c4396ae8435f7e1818ff383c05df47a14.tar.gz
emacs-wttrin-9958ec4c4396ae8435f7e1818ff383c05df47a14.zip
feat: add IP geolocation command for setting wttrin-favorite-location
Lets users set `wttrin-favorite-location` by IP lookup instead of typing a city by hand. `M-x wttrin-set-location-from-geolocation` runs the lookup, shows the detected "City, Region" in a yes/no prompt, and on confirmation sets the variable for the session. The docstring points at `M-x customize-save-variable` for persistence across restarts. The new `wttrin-geolocation.el` module provides the provider layer. Three providers come built in: ipapi.co (the default), ipinfo.io, and ipwho.is. All three are HTTPS, need no API key, and have free tiers large enough for interactive use. The module has three layers. Pure JSON parsers handle the per-provider quirks: ipapi's `error: true` flag, ipwho.is's `success: false` flag, ipinfo's HTTP-status-only signalling. A small fetch helper extracts the HTTP body. `wttrin-geolocation-detect` wires them together and calls back with "City, Region" on success, or nil on any failure (network error, HTTP 4xx or 5xx, malformed response, rate-limit signal). Providers live in an alist keyed by symbol, with plist values for :name, :url, and :parser. To use a different provider, push an entry onto `wttrin-geolocation--providers` and select it via `wttrin-geolocation-provider`. No code change needed. README gains a subsection under Mode-line Weather Display covering the command, how to persist the result, provider selection with free-tier limits, and the accuracy caveat for VPN or mobile-hotspot users. 39 new tests across the parser layer (10 ipapi, 6 ipinfo, 6 ipwhois), fetch-and-dispatch (11), and interactive command (6). Each suite covers Normal, Boundary, and Error categories. Tests mock `url-retrieve` and `yes-or-no-p` at their boundaries and run the real extract-and-parse pipeline underneath. Test suite: 333 → 373 passing.
Diffstat (limited to 'tests/test-wttrin-set-location-from-geolocation.el')
-rw-r--r--tests/test-wttrin-set-location-from-geolocation.el122
1 files changed, 122 insertions, 0 deletions
diff --git a/tests/test-wttrin-set-location-from-geolocation.el b/tests/test-wttrin-set-location-from-geolocation.el
new file mode 100644
index 0000000..170d0fb
--- /dev/null
+++ b/tests/test-wttrin-set-location-from-geolocation.el
@@ -0,0 +1,122 @@
+;;; test-wttrin-set-location-from-geolocation.el --- Tests for wttrin-set-location-from-geolocation -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;;; Commentary:
+;; Unit tests for the interactive `wttrin-set-location-from-geolocation'
+;; command. Mocks `wttrin-geolocation-detect' (to invoke the callback
+;; synchronously with a chosen value) and `yes-or-no-p' (to simulate
+;; user consent). Does not hit the network and does not prompt.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'wttrin)
+(require 'wttrin-geolocation)
+
+;;; Setup and Teardown
+
+(defvar test-wttrin-set-location-from-geolocation--saved-favorite nil
+ "Snapshot of `wttrin-favorite-location' restored in teardown.")
+
+(defun test-wttrin-set-location-from-geolocation-setup ()
+ "Snapshot `wttrin-favorite-location' and clear it for the test."
+ (setq test-wttrin-set-location-from-geolocation--saved-favorite
+ wttrin-favorite-location)
+ (setq wttrin-favorite-location nil))
+
+(defun test-wttrin-set-location-from-geolocation-teardown ()
+ "Restore `wttrin-favorite-location' to its pre-test value."
+ (setq wttrin-favorite-location
+ test-wttrin-set-location-from-geolocation--saved-favorite))
+
+;;; Helpers
+
+(defmacro test-wttrin-set-location--with-detected (location confirm &rest body)
+ "Run BODY with `wttrin-geolocation-detect' returning LOCATION and `yes-or-no-p' returning CONFIRM."
+ (declare (indent 2))
+ `(cl-letf (((symbol-function 'wttrin-geolocation-detect)
+ (lambda (callback) (funcall callback ,location)))
+ ((symbol-function 'yes-or-no-p)
+ (lambda (&rest _) ,confirm)))
+ ,@body))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin-set-location-from-geolocation-normal-confirm-sets-variable ()
+ "Successful detection followed by user confirmation sets the favorite."
+ (test-wttrin-set-location-from-geolocation-setup)
+ (unwind-protect
+ (progn
+ (test-wttrin-set-location--with-detected "Berkeley, California" t
+ (wttrin-set-location-from-geolocation))
+ (should (string= "Berkeley, California" wttrin-favorite-location)))
+ (test-wttrin-set-location-from-geolocation-teardown)))
+
+(ert-deftest test-wttrin-set-location-from-geolocation-normal-decline-leaves-variable-unchanged ()
+ "Successful detection followed by user declining leaves the favorite untouched."
+ (test-wttrin-set-location-from-geolocation-setup)
+ (setq wttrin-favorite-location "Pre-existing, Place")
+ (unwind-protect
+ (progn
+ (test-wttrin-set-location--with-detected "Berkeley, California" nil
+ (wttrin-set-location-from-geolocation))
+ (should (string= "Pre-existing, Place" wttrin-favorite-location)))
+ (test-wttrin-set-location-from-geolocation-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin-set-location-from-geolocation-boundary-unicode-location ()
+ "A Unicode location string round-trips into the favorite variable."
+ (test-wttrin-set-location-from-geolocation-setup)
+ (unwind-protect
+ (progn
+ (test-wttrin-set-location--with-detected "München, Bayern" t
+ (wttrin-set-location-from-geolocation))
+ (should (string= "München, Bayern" wttrin-favorite-location)))
+ (test-wttrin-set-location-from-geolocation-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-wttrin-set-location-from-geolocation-error-nil-detection-leaves-variable-unchanged ()
+ "When detection returns nil, the favorite variable is not modified."
+ (test-wttrin-set-location-from-geolocation-setup)
+ (setq wttrin-favorite-location "Pre-existing, Place")
+ (unwind-protect
+ (progn
+ (test-wttrin-set-location--with-detected nil t
+ (wttrin-set-location-from-geolocation))
+ (should (string= "Pre-existing, Place" wttrin-favorite-location)))
+ (test-wttrin-set-location-from-geolocation-teardown)))
+
+(ert-deftest test-wttrin-set-location-from-geolocation-error-nil-detection-does-not-prompt ()
+ "When detection returns nil, the user is not prompted for confirmation."
+ (test-wttrin-set-location-from-geolocation-setup)
+ (unwind-protect
+ (let ((prompt-called nil))
+ (cl-letf (((symbol-function 'wttrin-geolocation-detect)
+ (lambda (callback) (funcall callback nil)))
+ ((symbol-function 'yes-or-no-p)
+ (lambda (&rest _) (setq prompt-called t) t)))
+ (wttrin-set-location-from-geolocation))
+ (should-not prompt-called))
+ (test-wttrin-set-location-from-geolocation-teardown)))
+
+(ert-deftest test-wttrin-set-location-from-geolocation-error-detection-failure-shows-message ()
+ "When detection returns nil, the user sees a diagnostic message."
+ (test-wttrin-set-location-from-geolocation-setup)
+ (unwind-protect
+ (let ((messages nil))
+ (cl-letf (((symbol-function 'wttrin-geolocation-detect)
+ (lambda (callback) (funcall callback nil)))
+ ((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (push (apply #'format fmt args) messages))))
+ (wttrin-set-location-from-geolocation))
+ (should (cl-some (lambda (m) (string-match-p "[Cc]ould not detect" m))
+ messages)))
+ (test-wttrin-set-location-from-geolocation-teardown)))
+
+(provide 'test-wttrin-set-location-from-geolocation)
+;;; test-wttrin-set-location-from-geolocation.el ends here