diff options
| author | Craig Jennings <c@cjennings.net> | 2026-04-22 00:07:51 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-04-22 00:07:51 -0500 |
| commit | 9958ec4c4396ae8435f7e1818ff383c05df47a14 (patch) | |
| tree | 9835229246368cca582f669837cd2859a79c8862 /tests | |
| parent | 603f70d78f771c0a14c7f312aee6da68060b5d8b (diff) | |
| download | emacs-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')
| -rw-r--r-- | tests/test-wttrin-geolocation--parse-ipapi.el | 83 | ||||
| -rw-r--r-- | tests/test-wttrin-geolocation--parse-ipinfo.el | 61 | ||||
| -rw-r--r-- | tests/test-wttrin-geolocation--parse-ipwhois.el | 63 | ||||
| -rw-r--r-- | tests/test-wttrin-geolocation-detect.el | 179 | ||||
| -rw-r--r-- | tests/test-wttrin-set-location-from-geolocation.el | 122 |
5 files changed, 508 insertions, 0 deletions
diff --git a/tests/test-wttrin-geolocation--parse-ipapi.el b/tests/test-wttrin-geolocation--parse-ipapi.el new file mode 100644 index 0000000..26d67bc --- /dev/null +++ b/tests/test-wttrin-geolocation--parse-ipapi.el @@ -0,0 +1,83 @@ +;;; test-wttrin-geolocation--parse-ipapi.el --- Tests for ipapi.co response parser -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;;; Commentary: +;; Unit tests for `wttrin-geolocation--parse-ipapi'. Pure function — no +;; network, no async. Covers normal, boundary, and error cases per the +;; Normal/Boundary/Error discipline. + +;;; Code: + +(require 'ert) +(require 'wttrin-geolocation) + +;;; Setup and Teardown + +(defun test-wttrin-geolocation--parse-ipapi-setup () + "Setup for ipapi parser tests." + nil) + +(defun test-wttrin-geolocation--parse-ipapi-teardown () + "Teardown for ipapi parser tests." + nil) + +;;; Normal Cases + +(ert-deftest test-wttrin-geolocation--parse-ipapi-normal-full-response-returns-formatted-string () + "A full ipapi.co response yields \"City, Region\"." + (let ((json "{\"ip\":\"1.2.3.4\",\"city\":\"Berkeley\",\"region\":\"California\",\"country_name\":\"United States\",\"latitude\":37.87,\"longitude\":-122.27}")) + (should (string= "Berkeley, California" + (wttrin-geolocation--parse-ipapi json))))) + +(ert-deftest test-wttrin-geolocation--parse-ipapi-normal-unicode-preserved () + "Unicode city and region names round-trip through the parser." + (let ((json "{\"city\":\"São Paulo\",\"region\":\"São Paulo\"}")) + (should (string= "São Paulo, São Paulo" + (wttrin-geolocation--parse-ipapi json))))) + +;;; Boundary Cases + +(ert-deftest test-wttrin-geolocation--parse-ipapi-boundary-extra-fields-ignored () + "Unknown fields in the response do not affect the parsed result." + (let ((json "{\"city\":\"Paris\",\"region\":\"Ile-de-France\",\"currency\":\"EUR\",\"asn\":\"AS1234\"}")) + (should (string= "Paris, Ile-de-France" + (wttrin-geolocation--parse-ipapi json))))) + +(ert-deftest test-wttrin-geolocation--parse-ipapi-boundary-multi-word-city-preserved () + "Multi-word city names are preserved verbatim." + (let ((json "{\"city\":\"New Orleans\",\"region\":\"Louisiana\"}")) + (should (string= "New Orleans, Louisiana" + (wttrin-geolocation--parse-ipapi json))))) + +;;; Error Cases + +(ert-deftest test-wttrin-geolocation--parse-ipapi-error-nil-input-returns-nil () + "A nil input string returns nil." + (should-not (wttrin-geolocation--parse-ipapi nil))) + +(ert-deftest test-wttrin-geolocation--parse-ipapi-error-empty-string-returns-nil () + "An empty input string returns nil." + (should-not (wttrin-geolocation--parse-ipapi ""))) + +(ert-deftest test-wttrin-geolocation--parse-ipapi-error-malformed-json-returns-nil () + "Malformed JSON returns nil rather than signalling." + (should-not (wttrin-geolocation--parse-ipapi "{not valid json"))) + +(ert-deftest test-wttrin-geolocation--parse-ipapi-error-missing-city-returns-nil () + "A response without a city field returns nil." + (let ((json "{\"region\":\"California\"}")) + (should-not (wttrin-geolocation--parse-ipapi json)))) + +(ert-deftest test-wttrin-geolocation--parse-ipapi-error-missing-region-returns-nil () + "A response without a region field returns nil." + (let ((json "{\"city\":\"Berkeley\"}")) + (should-not (wttrin-geolocation--parse-ipapi json)))) + +(ert-deftest test-wttrin-geolocation--parse-ipapi-error-api-error-flag-returns-nil () + "An ipapi rate-limit / error response returns nil even if city/region are absent." + (let ((json "{\"error\":true,\"reason\":\"RateLimited\",\"message\":\"wait\"}")) + (should-not (wttrin-geolocation--parse-ipapi json)))) + +(provide 'test-wttrin-geolocation--parse-ipapi) +;;; test-wttrin-geolocation--parse-ipapi.el ends here diff --git a/tests/test-wttrin-geolocation--parse-ipinfo.el b/tests/test-wttrin-geolocation--parse-ipinfo.el new file mode 100644 index 0000000..4543e32 --- /dev/null +++ b/tests/test-wttrin-geolocation--parse-ipinfo.el @@ -0,0 +1,61 @@ +;;; test-wttrin-geolocation--parse-ipinfo.el --- Tests for ipinfo.io response parser -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;;; Commentary: +;; Unit tests for `wttrin-geolocation--parse-ipinfo'. Pure function — no +;; network, no async. + +;;; Code: + +(require 'ert) +(require 'wttrin-geolocation) + +;;; Setup and Teardown + +(defun test-wttrin-geolocation--parse-ipinfo-setup () + "Setup for ipinfo parser tests." + nil) + +(defun test-wttrin-geolocation--parse-ipinfo-teardown () + "Teardown for ipinfo parser tests." + nil) + +;;; Normal Cases + +(ert-deftest test-wttrin-geolocation--parse-ipinfo-normal-full-response-returns-formatted-string () + "A full ipinfo.io response yields \"City, Region\"." + (let ((json "{\"ip\":\"8.8.8.8\",\"city\":\"Mountain View\",\"region\":\"California\",\"country\":\"US\",\"loc\":\"37.3860,-122.0838\",\"org\":\"AS15169 Google LLC\",\"postal\":\"94035\",\"timezone\":\"America/Los_Angeles\"}")) + (should (string= "Mountain View, California" + (wttrin-geolocation--parse-ipinfo json))))) + +;;; Boundary Cases + +(ert-deftest test-wttrin-geolocation--parse-ipinfo-boundary-minimal-fields-works () + "A response with only city and region parses correctly." + (let ((json "{\"city\":\"Berlin\",\"region\":\"Berlin\"}")) + (should (string= "Berlin, Berlin" + (wttrin-geolocation--parse-ipinfo json))))) + +;;; Error Cases + +(ert-deftest test-wttrin-geolocation--parse-ipinfo-error-nil-input-returns-nil () + "A nil input string returns nil." + (should-not (wttrin-geolocation--parse-ipinfo nil))) + +(ert-deftest test-wttrin-geolocation--parse-ipinfo-error-malformed-json-returns-nil () + "Malformed JSON returns nil rather than signalling." + (should-not (wttrin-geolocation--parse-ipinfo "not json at all"))) + +(ert-deftest test-wttrin-geolocation--parse-ipinfo-error-missing-city-returns-nil () + "A response without a city field returns nil." + (let ((json "{\"region\":\"California\",\"country\":\"US\"}")) + (should-not (wttrin-geolocation--parse-ipinfo json)))) + +(ert-deftest test-wttrin-geolocation--parse-ipinfo-error-missing-region-returns-nil () + "A response without a region field returns nil." + (let ((json "{\"city\":\"Mountain View\",\"country\":\"US\"}")) + (should-not (wttrin-geolocation--parse-ipinfo json)))) + +(provide 'test-wttrin-geolocation--parse-ipinfo) +;;; test-wttrin-geolocation--parse-ipinfo.el ends here diff --git a/tests/test-wttrin-geolocation--parse-ipwhois.el b/tests/test-wttrin-geolocation--parse-ipwhois.el new file mode 100644 index 0000000..14c8119 --- /dev/null +++ b/tests/test-wttrin-geolocation--parse-ipwhois.el @@ -0,0 +1,63 @@ +;;; test-wttrin-geolocation--parse-ipwhois.el --- Tests for ipwho.is response parser -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;;; Commentary: +;; Unit tests for `wttrin-geolocation--parse-ipwhois'. Pure function — no +;; network, no async. The ipwho.is response carries a `success' flag which +;; is false on rate-limit / lookup failure even when the response is +;; well-formed JSON. + +;;; Code: + +(require 'ert) +(require 'wttrin-geolocation) + +;;; Setup and Teardown + +(defun test-wttrin-geolocation--parse-ipwhois-setup () + "Setup for ipwho.is parser tests." + nil) + +(defun test-wttrin-geolocation--parse-ipwhois-teardown () + "Teardown for ipwho.is parser tests." + nil) + +;;; Normal Cases + +(ert-deftest test-wttrin-geolocation--parse-ipwhois-normal-success-response-returns-formatted-string () + "A successful ipwho.is response yields \"City, Region\"." + (let ((json "{\"ip\":\"8.8.8.8\",\"success\":true,\"type\":\"IPv4\",\"country\":\"United States\",\"country_code\":\"US\",\"region\":\"California\",\"region_code\":\"CA\",\"city\":\"Mountain View\",\"latitude\":37.386,\"longitude\":-122.0838}")) + (should (string= "Mountain View, California" + (wttrin-geolocation--parse-ipwhois json))))) + +;;; Boundary Cases + +(ert-deftest test-wttrin-geolocation--parse-ipwhois-boundary-unicode-preserved () + "Unicode city and region names round-trip through the parser." + (let ((json "{\"success\":true,\"city\":\"München\",\"region\":\"Bayern\"}")) + (should (string= "München, Bayern" + (wttrin-geolocation--parse-ipwhois json))))) + +;;; Error Cases + +(ert-deftest test-wttrin-geolocation--parse-ipwhois-error-nil-input-returns-nil () + "A nil input string returns nil." + (should-not (wttrin-geolocation--parse-ipwhois nil))) + +(ert-deftest test-wttrin-geolocation--parse-ipwhois-error-success-false-returns-nil () + "A response with `success: false' returns nil even if city/region are present." + (let ((json "{\"success\":false,\"message\":\"rate limit reached\",\"city\":\"Anywhere\",\"region\":\"Anywhere\"}")) + (should-not (wttrin-geolocation--parse-ipwhois json)))) + +(ert-deftest test-wttrin-geolocation--parse-ipwhois-error-missing-city-returns-nil () + "A success response without a city field returns nil." + (let ((json "{\"success\":true,\"region\":\"California\"}")) + (should-not (wttrin-geolocation--parse-ipwhois json)))) + +(ert-deftest test-wttrin-geolocation--parse-ipwhois-error-malformed-json-returns-nil () + "Malformed JSON returns nil rather than signalling." + (should-not (wttrin-geolocation--parse-ipwhois "{\"success\":true,\"city\":"))) + +(provide 'test-wttrin-geolocation--parse-ipwhois) +;;; test-wttrin-geolocation--parse-ipwhois.el ends here diff --git a/tests/test-wttrin-geolocation-detect.el b/tests/test-wttrin-geolocation-detect.el new file mode 100644 index 0000000..ccd113f --- /dev/null +++ b/tests/test-wttrin-geolocation-detect.el @@ -0,0 +1,179 @@ +;;; test-wttrin-geolocation-detect.el --- Tests for wttrin-geolocation-detect -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;;; Commentary: +;; Unit tests for `wttrin-geolocation-detect'. Mocks `url-retrieve' at +;; the boundary — exercises the real extract/parse logic via the selected +;; provider. No network, no async timing. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'wttrin-geolocation) + +;;; Setup and Teardown + +(defun test-wttrin-geolocation-detect-setup () + "Setup for detect tests — pin provider to ipapi for determinism." + (setq wttrin-geolocation-provider 'ipapi)) + +(defun test-wttrin-geolocation-detect-teardown () + "Teardown for detect tests — restore default provider." + (setq wttrin-geolocation-provider 'ipapi)) + +;;; Helpers + +(defmacro test-wttrin-geolocation-detect--with-http (status body-string &rest body) + "Run BODY with `url-retrieve' mocked to return HTTP STATUS and BODY-STRING. +The mock writes a full HTTP response into a temp buffer and invokes the +retrieval callback with a nil status plist (success)." + (declare (indent 2)) + `(cl-letf (((symbol-function 'url-retrieve) + (lambda (_url callback) + (with-temp-buffer + (insert (format "HTTP/1.1 %d OK\r\n" ,status)) + (insert "Content-Type: application/json\r\n\r\n") + (insert ,body-string) + (funcall callback nil))))) + ,@body)) + +(defmacro test-wttrin-geolocation-detect--with-network-error (&rest body) + "Run BODY with `url-retrieve' mocked to report a network-level error." + (declare (indent 0)) + `(cl-letf (((symbol-function 'url-retrieve) + (lambda (_url callback) + (with-temp-buffer + (funcall callback '(:error (error "Network unreachable"))))))) + ,@body)) + +;;; Normal Cases + +(ert-deftest test-wttrin-geolocation-detect-normal-ipapi-success-callback-receives-location () + "A successful ipapi response leads to the callback receiving \"City, Region\"." + (test-wttrin-geolocation-detect-setup) + (unwind-protect + (let ((result 'unset)) + (test-wttrin-geolocation-detect--with-http 200 + "{\"city\":\"Berkeley\",\"region\":\"California\"}" + (wttrin-geolocation-detect (lambda (loc) (setq result loc)))) + (should (string= "Berkeley, California" result))) + (test-wttrin-geolocation-detect-teardown))) + +(ert-deftest test-wttrin-geolocation-detect-normal-selected-provider-used () + "Switching `wttrin-geolocation-provider' routes through that provider's URL and parser." + (let ((wttrin-geolocation-provider 'ipinfo) + (requested-url nil) + (result nil)) + (cl-letf (((symbol-function 'url-retrieve) + (lambda (url callback) + (setq requested-url url) + (with-temp-buffer + (insert "HTTP/1.1 200 OK\r\n\r\n") + (insert "{\"city\":\"Mountain View\",\"region\":\"California\",\"loc\":\"37.4,-122.0\"}") + (funcall callback nil))))) + (wttrin-geolocation-detect (lambda (loc) (setq result loc)))) + (should (string= "https://ipinfo.io/json" requested-url)) + (should (string= "Mountain View, California" result)))) + +;;; Boundary Cases + +(ert-deftest test-wttrin-geolocation-detect-boundary-parser-returns-nil-callback-gets-nil () + "If the parser rejects the response (missing fields), the callback receives nil." + (test-wttrin-geolocation-detect-setup) + (unwind-protect + (let ((result 'unset)) + (test-wttrin-geolocation-detect--with-http 200 + "{\"city\":\"Berkeley\"}" + (wttrin-geolocation-detect (lambda (loc) (setq result loc)))) + (should-not result)) + (test-wttrin-geolocation-detect-teardown))) + +(ert-deftest test-wttrin-geolocation-detect-boundary-ipapi-rate-limit-error-flag-returns-nil () + "An ipapi rate-limit response (HTTP 200 with error flag) yields nil." + (test-wttrin-geolocation-detect-setup) + (unwind-protect + (let ((result 'unset)) + (test-wttrin-geolocation-detect--with-http 200 + "{\"error\":true,\"reason\":\"RateLimited\"}" + (wttrin-geolocation-detect (lambda (loc) (setq result loc)))) + (should-not result)) + (test-wttrin-geolocation-detect-teardown))) + +;;; Error Cases + +(ert-deftest test-wttrin-geolocation-detect-error-network-failure-calls-callback-with-nil () + "A network-level error surfaces as nil to the callback." + (test-wttrin-geolocation-detect-setup) + (unwind-protect + (let ((called nil) + (result 'unset)) + (test-wttrin-geolocation-detect--with-network-error + (wttrin-geolocation-detect (lambda (loc) + (setq called t) + (setq result loc)))) + (should called) + (should-not result)) + (test-wttrin-geolocation-detect-teardown))) + +(ert-deftest test-wttrin-geolocation-detect-error-http-429-returns-nil () + "An HTTP 429 rate-limit response yields nil." + (test-wttrin-geolocation-detect-setup) + (unwind-protect + (let ((result 'unset)) + (test-wttrin-geolocation-detect--with-http 429 + "{\"error\":\"rate limit\"}" + (wttrin-geolocation-detect (lambda (loc) (setq result loc)))) + (should-not result)) + (test-wttrin-geolocation-detect-teardown))) + +(ert-deftest test-wttrin-geolocation-detect-error-http-500-returns-nil () + "An HTTP 500 server error yields nil." + (test-wttrin-geolocation-detect-setup) + (unwind-protect + (let ((result 'unset)) + (test-wttrin-geolocation-detect--with-http 500 + "Internal Server Error" + (wttrin-geolocation-detect (lambda (loc) (setq result loc)))) + (should-not result)) + (test-wttrin-geolocation-detect-teardown))) + +(ert-deftest test-wttrin-geolocation-detect-error-empty-body-returns-nil () + "An empty response body yields nil." + (test-wttrin-geolocation-detect-setup) + (unwind-protect + (let ((result 'unset)) + (test-wttrin-geolocation-detect--with-http 200 "" + (wttrin-geolocation-detect (lambda (loc) (setq result loc)))) + (should-not result)) + (test-wttrin-geolocation-detect-teardown))) + +(ert-deftest test-wttrin-geolocation-detect-error-malformed-json-returns-nil () + "Malformed JSON in the response body yields nil." + (test-wttrin-geolocation-detect-setup) + (unwind-protect + (let ((result 'unset)) + (test-wttrin-geolocation-detect--with-http 200 "{not valid json" + (wttrin-geolocation-detect (lambda (loc) (setq result loc)))) + (should-not result)) + (test-wttrin-geolocation-detect-teardown))) + +(ert-deftest test-wttrin-geolocation-detect-error-buffer-cleanup-after-success () + "The response buffer is killed after a successful fetch (no leaks)." + (test-wttrin-geolocation-detect-setup) + (unwind-protect + (let ((buffers-before (length (buffer-list)))) + (test-wttrin-geolocation-detect--with-http 200 + "{\"city\":\"Paris\",\"region\":\"IDF\"}" + (wttrin-geolocation-detect #'ignore)) + (should (= buffers-before (length (buffer-list))))) + (test-wttrin-geolocation-detect-teardown))) + +(ert-deftest test-wttrin-geolocation-detect-error-unknown-provider-signals-error () + "Selecting an unknown provider signals a clear error." + (let ((wttrin-geolocation-provider 'nonexistent-provider)) + (should-error (wttrin-geolocation-detect #'ignore) :type 'error))) + +(provide 'test-wttrin-geolocation-detect) +;;; test-wttrin-geolocation-detect.el ends here 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 |
