diff options
| -rw-r--r-- | tests/test-wttrin--fetch-url.el | 205 | ||||
| -rw-r--r-- | wttrin.el | 54 |
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 @@ -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. |
