summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-11-08 10:26:17 -0600
committerCraig Jennings <c@cjennings.net>2025-11-08 10:26:17 -0600
commit44d48bad4d317ce8fd9361314d349b1256e6c25b (patch)
tree90eeae6fd6512dc5418da1600e198a52cd29ee35
parent7673e72ed4dc4af9c3db9a5962c4673bd1ce90e3 (diff)
refactor: fetch: extract duplicate URL fetching logic into wttrin--fetch-url
- Created wttrin--fetch-url helper to eliminate code duplication - Refactored wttrin-fetch-raw-string to use helper (27 lines -> 3 lines) - Refactored wttrin--mode-line-fetch-weather to use helper (~30 lines -> ~10 lines) - Added comprehensive ERT test suite with 9 tests covering normal, boundary, and error cases - All tests passing This refactoring provides a single point of truth for async URL fetching, making the code more maintainable and reducing duplication by ~40 lines.
-rw-r--r--tests/test-wttrin--fetch-url.el205
-rw-r--r--wttrin.el54
2 files changed, 224 insertions, 35 deletions
diff --git a/tests/test-wttrin--fetch-url.el b/tests/test-wttrin--fetch-url.el
new file mode 100644
index 0000000..bbec115
--- /dev/null
+++ b/tests/test-wttrin--fetch-url.el
@@ -0,0 +1,205 @@
+;;; test-wttrin--fetch-url.el --- Tests for wttrin--fetch-url -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024 Craig Jennings
+
+;;; Commentary:
+;; Unit tests for wttrin--fetch-url function.
+;; Tests the common async URL fetching logic extracted during refactoring.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--fetch-url-normal-success-calls-callback-with-data ()
+ "Test that successful fetch calls callback with decoded data."
+ (let ((callback-called nil)
+ (callback-data nil))
+ ;; Mock url-retrieve to simulate successful response
+ (cl-letf (((symbol-function 'url-retrieve)
+ (lambda (url callback)
+ ;; Simulate successful HTTP response
+ (with-temp-buffer
+ (insert "HTTP/1.1 200 OK\r\n")
+ (insert "Content-Type: text/plain\r\n")
+ (insert "\r\n")
+ (insert "Weather data here")
+ (funcall callback nil)))))
+
+ (wttrin--fetch-url
+ "http://example.com/weather"
+ (lambda (data)
+ (setq callback-called t)
+ (setq callback-data data)))
+
+ ;; Verify callback was called with correct data
+ (should callback-called)
+ (should (string= "Weather data here" callback-data)))))
+
+(ert-deftest test-wttrin--fetch-url-normal-utf8-decoding ()
+ "Test that UTF-8 content is properly decoded."
+ (let ((callback-data nil))
+ (cl-letf (((symbol-function 'url-retrieve)
+ (lambda (url callback)
+ (with-temp-buffer
+ (insert "HTTP/1.1 200 OK\r\n\r\n")
+ (insert "Weather: ☀️ Sunny 中文")
+ (funcall callback nil)))))
+
+ (wttrin--fetch-url
+ "http://example.com/weather"
+ (lambda (data)
+ (setq callback-data data)))
+
+ (should (string-match-p "☀️" callback-data))
+ (should (string-match-p "中文" callback-data)))))
+
+(ert-deftest test-wttrin--fetch-url-normal-headers-stripped ()
+ "Test that HTTP headers are correctly stripped from response."
+ (let ((callback-data nil))
+ (cl-letf (((symbol-function 'url-retrieve)
+ (lambda (url callback)
+ (with-temp-buffer
+ (insert "HTTP/1.1 200 OK\r\n")
+ (insert "Content-Type: text/plain\r\n")
+ (insert "Content-Length: 17\r\n")
+ (insert "\r\n")
+ (insert "Weather data here")
+ (funcall callback nil)))))
+
+ (wttrin--fetch-url
+ "http://example.com/weather"
+ (lambda (data)
+ (setq callback-data data)))
+
+ ;; Headers should not be in the data
+ (should-not (string-match-p "HTTP/1.1" callback-data))
+ (should-not (string-match-p "Content-Type" callback-data))
+ (should (string= "Weather data here" callback-data)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--fetch-url-boundary-empty-response ()
+ "Test handling of empty response body."
+ (let ((callback-data nil))
+ (cl-letf (((symbol-function 'url-retrieve)
+ (lambda (url callback)
+ (with-temp-buffer
+ (insert "HTTP/1.1 200 OK\r\n\r\n")
+ ;; Empty body
+ (funcall callback nil)))))
+
+ (wttrin--fetch-url
+ "http://example.com/weather"
+ (lambda (data)
+ (setq callback-data data)))
+
+ (should (string= "" callback-data)))))
+
+(ert-deftest test-wttrin--fetch-url-boundary-large-response ()
+ "Test handling of large response body."
+ (let ((callback-data nil)
+ (large-body (make-string 10000 ?x)))
+ (cl-letf (((symbol-function 'url-retrieve)
+ (lambda (url callback)
+ (with-temp-buffer
+ (insert "HTTP/1.1 200 OK\r\n\r\n")
+ (insert large-body)
+ (funcall callback nil)))))
+
+ (wttrin--fetch-url
+ "http://example.com/weather"
+ (lambda (data)
+ (setq callback-data data)))
+
+ (should (= 10000 (length callback-data))))))
+
+(ert-deftest test-wttrin--fetch-url-boundary-unix-line-endings ()
+ "Test handling of Unix-style line endings (LF only)."
+ (let ((callback-data nil))
+ (cl-letf (((symbol-function 'url-retrieve)
+ (lambda (url callback)
+ (with-temp-buffer
+ (insert "HTTP/1.1 200 OK\n")
+ (insert "Content-Type: text/plain\n")
+ (insert "\n")
+ (insert "Weather data")
+ (funcall callback nil)))))
+
+ (wttrin--fetch-url
+ "http://example.com/weather"
+ (lambda (data)
+ (setq callback-data data)))
+
+ (should (string= "Weather data" callback-data)))))
+
+;;; Error Cases
+
+(ert-deftest test-wttrin--fetch-url-error-network-error-calls-callback-with-nil ()
+ "Test that network errors result in callback being called with nil."
+ (let ((callback-called nil)
+ (callback-data 'not-nil))
+ (cl-letf (((symbol-function 'url-retrieve)
+ (lambda (url callback)
+ ;; Simulate network error
+ (with-temp-buffer
+ (funcall callback '(:error (error "Network unreachable")))))))
+
+ (wttrin--fetch-url
+ "http://example.com/weather"
+ (lambda (data)
+ (setq callback-called t)
+ (setq callback-data data)))
+
+ (should callback-called)
+ (should (null callback-data)))))
+
+(ert-deftest test-wttrin--fetch-url-error-processing-error-calls-callback-with-nil ()
+ "Test that processing errors result in callback being called with nil."
+ (let ((callback-called nil)
+ (callback-data 'not-nil))
+ (cl-letf (((symbol-function 'url-retrieve)
+ (lambda (url callback)
+ (with-temp-buffer
+ ;; Simulate a real error by having decode-coding-string fail
+ ;; Make buffer-substring-no-properties return invalid data
+ (cl-letf (((symbol-function 'decode-coding-string)
+ (lambda (string coding-system)
+ (error "Decoding error"))))
+ (insert "HTTP/1.1 200 OK\r\n\r\ndata")
+ (funcall callback nil))))))
+
+ (wttrin--fetch-url
+ "http://example.com/weather"
+ (lambda (data)
+ (setq callback-called t)
+ (setq callback-data data)))
+
+ ;; Should still call callback even on error
+ (should callback-called)
+ ;; But data should be nil due to error
+ (should (null callback-data)))))
+
+(ert-deftest test-wttrin--fetch-url-error-buffer-killed-after-processing ()
+ "Test that response buffer is properly killed after processing."
+ (let ((buffers-before (buffer-list))
+ (callback-called nil))
+ (cl-letf (((symbol-function 'url-retrieve)
+ (lambda (url callback)
+ (with-temp-buffer
+ (insert "HTTP/1.1 200 OK\r\n\r\ndata")
+ (funcall callback nil)))))
+
+ (wttrin--fetch-url
+ "http://example.com/weather"
+ (lambda (data)
+ (setq callback-called t)))
+
+ (should callback-called)
+ ;; Buffer list should be the same (no leaked buffers)
+ (should (= (length buffers-before) (length (buffer-list)))))))
+
+(provide 'test-wttrin--fetch-url)
+;;; test-wttrin--fetch-url.el ends here
diff --git a/wttrin.el b/wttrin.el
index 162b747..1104281 100644
--- a/wttrin.el
+++ b/wttrin.el
@@ -183,12 +183,12 @@ This is a pure function with no side effects, suitable for testing."
(wttrin-additional-url-params)
"A"))
-(defun wttrin-fetch-raw-string (query callback)
- "Asynchronously fetch weather information for QUERY.
-CALLBACK is called with the weather data string when ready, or nil on error."
- (let* ((url (wttrin--build-url query))
- (url-request-extra-headers (list wttrin-default-languages))
- (url-user-agent "curl"))
+(defun wttrin--fetch-url (url callback)
+ "Asynchronously fetch URL and call CALLBACK with decoded response.
+CALLBACK is called with the weather data string when ready, or nil on error.
+Handles header skipping, UTF-8 decoding, and error handling automatically."
+ (let ((url-request-extra-headers (list wttrin-default-languages))
+ (url-user-agent "curl"))
(url-retrieve
url
(lambda (status)
@@ -212,6 +212,11 @@ CALLBACK is called with the weather data string when ready, or nil on error."
(setq data nil)))
(funcall callback data))))))
+(defun wttrin-fetch-raw-string (query callback)
+ "Asynchronously fetch weather information for QUERY.
+CALLBACK is called with the weather data string when ready, or nil on error."
+ (wttrin--fetch-url (wttrin--build-url query) callback))
+
(defun wttrin-exit ()
"Exit the wttrin buffer."
(interactive)
@@ -401,38 +406,17 @@ Uses wttr.in custom format for concise weather with emoji."
"?format=%l:+%c+%t+%C"))
(url (concat "https://wttr.in/"
(url-hexify-string location)
- format-params))
- (url-request-extra-headers (list wttrin-default-languages))
- (url-user-agent "curl"))
+ format-params)))
(when (featurep 'wttrin-debug)
(message "wttrin mode-line: URL = %s" url))
- (url-retrieve
+ (wttrin--fetch-url
url
- (lambda (status)
- (let ((data nil))
- (condition-case err
- (if (plist-get status :error)
- (progn
- (message "wttrin mode-line: Network error - %s"
- (cdr (plist-get status :error)))
- (setq data nil))
- (unwind-protect
- (progn
- ;; Skip HTTP headers
- (goto-char (point-min))
- (re-search-forward "\r?\n\r?\n" nil t)
- (setq data (string-trim
- (decode-coding-string
- (buffer-substring-no-properties (point) (point-max))
- 'utf-8)))
- (when (featurep 'wttrin-debug)
- (message "wttrin mode-line: Received data = %S" data)))
- (kill-buffer (current-buffer))))
- (error
- (message "wttrin mode-line: Error - %s" (error-message-string err))
- (setq data nil)))
- (when data
- (wttrin--mode-line-update-display data))))))))
+ (lambda (data)
+ (when data
+ (let ((trimmed-data (string-trim data)))
+ (when (featurep 'wttrin-debug)
+ (message "wttrin mode-line: Received data = %S" trimmed-data))
+ (wttrin--mode-line-update-display trimmed-data))))))))
(defun wttrin--mode-line-update-display (weather-string)
"Update mode-line display with WEATHER-STRING.