diff options
Diffstat (limited to 'tests/test-wttrin-geolocation-command.el')
| -rw-r--r-- | tests/test-wttrin-geolocation-command.el | 161 |
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 |
