aboutsummaryrefslogtreecommitdiff
path: root/tests/test-wttrin-geolocation--parse-ipapi.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-geolocation--parse-ipapi.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-geolocation--parse-ipapi.el')
-rw-r--r--tests/test-wttrin-geolocation--parse-ipapi.el83
1 files changed, 83 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