aboutsummaryrefslogtreecommitdiff
path: root/tests/test-wttrin-geolocation-command.el
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test-wttrin-geolocation-command.el')
-rw-r--r--tests/test-wttrin-geolocation-command.el161
1 files changed, 161 insertions, 0 deletions
diff --git a/tests/test-wttrin-geolocation-command.el b/tests/test-wttrin-geolocation-command.el
new file mode 100644
index 0000000..40ea278
--- /dev/null
+++ b/tests/test-wttrin-geolocation-command.el
@@ -0,0 +1,161 @@
+;;; test-wttrin-geolocation-command.el --- Tests for the external-command geolocation provider -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024-2026 Craig Jennings
+
+;;; Commentary:
+;; Unit tests for the generic external-command geolocation provider:
+;; `wttrin-geolocation--parse-coordinates' (pure JSON -> "LAT,LNG"), the
+;; routing/fallback in `wttrin-geolocation-detect' when
+;; `wttrin-geolocation-command' is set, and an end-to-end run of
+;; `wttrin-geolocation--detect-via-command' against a real shell command.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'wttrin)
+(require 'wttrin-geolocation)
+
+;;; wttrin-geolocation--parse-coordinates
+
+(ert-deftest test-wttrin-geolocation-command-normal-parse-coordinates ()
+ "Normal: numeric lat/lng become a \"LAT,LNG\" string."
+ (should (equal "41.32,-71.81"
+ (wttrin-geolocation--parse-coordinates
+ "{\"lat\": 41.32, \"lng\": -71.81}"))))
+
+(ert-deftest test-wttrin-geolocation-command-boundary-parse-ignores-extra-keys ()
+ "Boundary: extra keys (accuracy, address) are ignored."
+ (should (equal "1.5,2.5"
+ (wttrin-geolocation--parse-coordinates
+ "{\"lat\":1.5,\"lng\":2.5,\"accuracy_m\":11.5,\"address\":\"x\"}"))))
+
+(ert-deftest test-wttrin-geolocation-command-error-parse-malformed-json ()
+ "Error: malformed JSON returns nil."
+ (should (null (wttrin-geolocation--parse-coordinates "not json")))
+ (should (null (wttrin-geolocation--parse-coordinates "")))
+ (should (null (wttrin-geolocation--parse-coordinates nil))))
+
+(ert-deftest test-wttrin-geolocation-command-error-parse-missing-or-nonnumeric ()
+ "Error: missing or non-numeric coordinates return nil."
+ (should (null (wttrin-geolocation--parse-coordinates "{\"lat\":1.0}")))
+ (should (null (wttrin-geolocation--parse-coordinates
+ "{\"lat\":\"x\",\"lng\":\"y\"}"))))
+
+;;; wttrin-geolocation--parse-address
+
+(ert-deftest test-wttrin-geolocation-command-normal-parse-address ()
+ "Normal: the address key is returned verbatim."
+ (should (equal "Westerly, Rhode Island, USA"
+ (wttrin-geolocation--parse-address
+ "{\"lat\":1.0,\"lng\":2.0,\"address\":\"Westerly, Rhode Island, USA\"}"))))
+
+(ert-deftest test-wttrin-geolocation-command-boundary-parse-address-label-synonym ()
+ "Boundary: a `label' key is accepted when `address' is absent."
+ (should (equal "Westerly, RI"
+ (wttrin-geolocation--parse-address
+ "{\"lat\":1.0,\"lng\":2.0,\"label\":\"Westerly, RI\"}"))))
+
+(ert-deftest test-wttrin-geolocation-command-error-parse-address-absent ()
+ "Error: no address or label, malformed JSON, or empty string returns nil."
+ (should (null (wttrin-geolocation--parse-address "{\"lat\":1.0,\"lng\":2.0}")))
+ (should (null (wttrin-geolocation--parse-address "not json")))
+ (should (null (wttrin-geolocation--parse-address
+ "{\"lat\":1.0,\"lng\":2.0,\"address\":\"\"}"))))
+
+;;; wttrin-geolocation-detect — routing and fallback
+
+(ert-deftest test-wttrin-geolocation-command-normal-detect-uses-command ()
+ "Normal: with a command set that succeeds, its coordinates are used."
+ (let ((got 'none)
+ (ip-called nil)
+ (wttrin-geolocation-command "ignored-in-mock"))
+ (cl-letf (((symbol-function 'wttrin-geolocation--detect-via-command)
+ (lambda (cb) (funcall cb "1.0,2.0")))
+ ((symbol-function 'wttrin-geolocation--detect-via-ip)
+ (lambda (cb) (setq ip-called t) (funcall cb "City, ST"))))
+ (wttrin-geolocation-detect (lambda (r &optional _a) (setq got r))))
+ (should (equal "1.0,2.0" got))
+ (should-not ip-called)))
+
+(ert-deftest test-wttrin-geolocation-command-boundary-detect-falls-back-to-ip ()
+ "Boundary: with a command that fails (nil), detection falls back to IP."
+ (let ((got 'none)
+ (wttrin-geolocation-command "ignored-in-mock"))
+ (cl-letf (((symbol-function 'wttrin-geolocation--detect-via-command)
+ (lambda (cb) (funcall cb nil)))
+ ((symbol-function 'wttrin-geolocation--detect-via-ip)
+ (lambda (cb) (funcall cb "City, ST"))))
+ (wttrin-geolocation-detect (lambda (r &optional _a) (setq got r))))
+ (should (equal "City, ST" got))))
+
+(ert-deftest test-wttrin-geolocation-command-boundary-detect-no-command-uses-ip ()
+ "Boundary: with no command set, the IP path runs directly."
+ (let ((got 'none)
+ (cmd-called nil)
+ (wttrin-geolocation-command nil))
+ (cl-letf (((symbol-function 'wttrin-geolocation--detect-via-command)
+ (lambda (cb) (setq cmd-called t) (funcall cb "9.0,9.0")))
+ ((symbol-function 'wttrin-geolocation--detect-via-ip)
+ (lambda (cb) (funcall cb "City, ST"))))
+ (wttrin-geolocation-detect (lambda (r &optional _a) (setq got r))))
+ (should (equal "City, ST" got))
+ (should-not cmd-called)))
+
+;;; wttrin-geolocation--detect-via-command — real process
+
+(defun test-wttrin-geolocation-command--run-sync (command)
+ "Run `wttrin-geolocation--detect-via-command' with COMMAND, wait, return coords.
+The address (second callback argument) is ignored here; a separate test covers it."
+ (let ((result 'pending)
+ (wttrin-geolocation-command command))
+ (wttrin-geolocation--detect-via-command
+ (lambda (r &optional _a) (setq result r)))
+ (with-timeout (5 (error "detect-via-command timed out"))
+ (while (eq result 'pending)
+ (accept-process-output nil 0.05)))
+ result))
+
+(ert-deftest test-wttrin-geolocation-command-integration-real-command-success ()
+ "Integration: a command printing lat/lng JSON resolves to \"LAT,LNG\".
+Components: wttrin-geolocation--detect-via-command (real make-process),
+the shell (real), wttrin-geolocation--parse-coordinates (real)."
+ (should (equal "1.5,2.5"
+ (test-wttrin-geolocation-command--run-sync
+ "echo '{\"lat\":1.5,\"lng\":2.5}'"))))
+
+(ert-deftest test-wttrin-geolocation-command-integration-real-command-passes-address ()
+ "Integration: a command printing lat/lng plus an address passes both to the callback.
+Components: detect-via-command (real make-process), the shell, the coord and
+address parsers (real)."
+ (let ((coords 'pending)
+ (address 'pending)
+ (wttrin-geolocation-command
+ "echo '{\"lat\":1.5,\"lng\":2.5,\"address\":\"Westerly, RI\"}'"))
+ (wttrin-geolocation--detect-via-command
+ (lambda (c &optional a) (setq coords c address a)))
+ (with-timeout (5 (error "detect-via-command timed out"))
+ (while (eq coords 'pending)
+ (accept-process-output nil 0.05)))
+ (should (equal "1.5,2.5" coords))
+ (should (equal "Westerly, RI" address))))
+
+(ert-deftest test-wttrin-geolocation-command-integration-real-command-nonzero-exit ()
+ "Integration: a command exiting non-zero yields nil (caller falls back to IP)."
+ (should (null (test-wttrin-geolocation-command--run-sync "exit 1"))))
+
+(ert-deftest test-wttrin-geolocation-command-integration-real-command-bad-output ()
+ "Integration: a zero-exit command printing non-JSON yields nil."
+ (should (null (test-wttrin-geolocation-command--run-sync "echo not-json"))))
+
+(ert-deftest test-wttrin-geolocation-command-error-spawn-failure-yields-nil ()
+ "Error: when the process cannot spawn, the callback receives nil."
+ (let ((result 'pending)
+ (wttrin-geolocation-command "whatever"))
+ (cl-letf (((symbol-function 'make-process)
+ (lambda (&rest _) (error "spawn failed"))))
+ (wttrin-geolocation--detect-via-command (lambda (r) (setq result r))))
+ (should (null result))))
+
+(provide 'test-wttrin-geolocation-command)
+;;; test-wttrin-geolocation-command.el ends here