diff options
| -rw-r--r-- | tests/test-wttrin--extract-response-body.el | 156 | ||||
| -rw-r--r-- | tests/test-wttrin--fetch-url.el | 25 | ||||
| -rw-r--r-- | tests/test-wttrin--handle-fetch-callback.el | 214 | ||||
| -rw-r--r-- | wttrin.el | 79 |
4 files changed, 420 insertions, 54 deletions
diff --git a/tests/test-wttrin--extract-response-body.el b/tests/test-wttrin--extract-response-body.el new file mode 100644 index 0000000..41c3752 --- /dev/null +++ b/tests/test-wttrin--extract-response-body.el @@ -0,0 +1,156 @@ +;;; test-wttrin--extract-response-body.el --- Tests for wttrin--extract-response-body -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Craig Jennings + +;;; Commentary: +;; Unit tests for wttrin--extract-response-body function. +;; Tests HTTP response parsing and UTF-8 decoding in isolation. + +;;; Code: + +(require 'ert) +(require 'wttrin) + +;;; Normal Cases + +(ert-deftest test-wttrin--extract-response-body-normal-simple-response () + "Test extracting body from simple 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") + (let ((result (wttrin--extract-response-body))) + (should (string= "Weather data" result))))) + +(ert-deftest test-wttrin--extract-response-body-normal-utf8-content () + "Test extracting UTF-8 encoded body with emoji and international characters." + (with-temp-buffer + (insert "HTTP/1.1 200 OK\r\n\r\n") + (insert "☀️ Sunny 中文 مرحبا") + (let ((result (wttrin--extract-response-body))) + (should (string-match-p "☀️" result)) + (should (string-match-p "中文" result)) + (should (string-match-p "مرحبا" result))))) + +(ert-deftest test-wttrin--extract-response-body-normal-multiline-body () + "Test extracting multi-line response body." + (with-temp-buffer + (insert "HTTP/1.1 200 OK\r\n\r\n") + (insert "Line 1\n") + (insert "Line 2\n") + (insert "Line 3") + (let ((result (wttrin--extract-response-body))) + (should (string-match-p "Line 1" result)) + (should (string-match-p "Line 2" result)) + (should (string-match-p "Line 3" result))))) + +;;; Boundary Cases + +(ert-deftest test-wttrin--extract-response-body-boundary-empty-body () + "Test extracting empty response body." + (with-temp-buffer + (insert "HTTP/1.1 200 OK\r\n\r\n") + ;; No body content + (let ((result (wttrin--extract-response-body))) + (should (string= "" result))))) + +(ert-deftest test-wttrin--extract-response-body-boundary-large-body () + "Test extracting large response body." + (let ((large-content (make-string 50000 ?x))) + (with-temp-buffer + (insert "HTTP/1.1 200 OK\r\n\r\n") + (insert large-content) + (let ((result (wttrin--extract-response-body))) + (should (= 50000 (length result))) + (should (string= large-content result)))))) + +(ert-deftest test-wttrin--extract-response-body-boundary-unix-line-endings () + "Test extracting body with Unix-style LF line endings." + (with-temp-buffer + (insert "HTTP/1.1 200 OK\n") + (insert "Content-Type: text/plain\n") + (insert "\n") + (insert "Body content") + (let ((result (wttrin--extract-response-body))) + (should (string= "Body content" result))))) + +(ert-deftest test-wttrin--extract-response-body-boundary-windows-line-endings () + "Test extracting body with Windows-style CRLF line endings." + (with-temp-buffer + (insert "HTTP/1.1 200 OK\r\n") + (insert "Content-Type: text/plain\r\n") + (insert "\r\n") + (insert "Body content") + (let ((result (wttrin--extract-response-body))) + (should (string= "Body content" result))))) + +(ert-deftest test-wttrin--extract-response-body-boundary-mixed-line-endings () + "Test extracting body with mixed LF/CRLF line endings in headers." + (with-temp-buffer + (insert "HTTP/1.1 200 OK\r\n") + (insert "Header1: value\n") + (insert "Header2: value\r\n") + (insert "\r\n") + (insert "Body content") + (let ((result (wttrin--extract-response-body))) + (should (string= "Body content" result))))) + +(ert-deftest test-wttrin--extract-response-body-boundary-many-headers () + "Test extracting body with many response headers." + (with-temp-buffer + (insert "HTTP/1.1 200 OK\r\n") + (dotimes (i 20) + (insert (format "Header-%d: value-%d\r\n" i i))) + (insert "\r\n") + (insert "Body content") + (let ((result (wttrin--extract-response-body))) + (should (string= "Body content" result)) + ;; Headers should not be in result + (should-not (string-match-p "Header-" result))))) + +(ert-deftest test-wttrin--extract-response-body-boundary-body-looks-like-headers () + "Test extracting body that contains text resembling HTTP headers." + (with-temp-buffer + (insert "HTTP/1.1 200 OK\r\n\r\n") + (insert "HTTP/1.1 404 Not Found\r\n") + (insert "This looks like headers but it's body content") + (let ((result (wttrin--extract-response-body))) + (should (string-match-p "HTTP/1.1 404" result)) + (should (string-match-p "This looks like headers" result))))) + +;;; Error Cases + +(ert-deftest test-wttrin--extract-response-body-error-no-header-separator () + "Test handling of response with no header/body separator." + (with-temp-buffer + (insert "HTTP/1.1 200 OK\r\n") + (insert "Content-Type: text/plain\r\n") + ;; Missing \r\n\r\n separator + (insert "Body content") + (let ((result (wttrin--extract-response-body))) + ;; Should return whatever comes after attempting to find separator + (should result)))) + +(ert-deftest test-wttrin--extract-response-body-error-empty-buffer () + "Test handling of completely empty buffer." + (with-temp-buffer + ;; Empty buffer + (let ((result (wttrin--extract-response-body))) + ;; Should return empty string or nil without crashing + (should (or (null result) (string= "" result)))))) + +(ert-deftest test-wttrin--extract-response-body-error-buffer-kills-cleanly () + "Test that buffer is killed even when processing succeeds." + (let ((buffers-before (buffer-list)) + result) + (with-temp-buffer + (let ((test-buffer (current-buffer))) + (insert "HTTP/1.1 200 OK\r\n\r\ndata") + (setq result (wttrin--extract-response-body)) + ;; Buffer should be killed after extraction + (should-not (buffer-live-p test-buffer)))) + (should (string= "data" result)))) + +(provide 'test-wttrin--extract-response-body) +;;; test-wttrin--extract-response-body.el ends here diff --git a/tests/test-wttrin--fetch-url.el b/tests/test-wttrin--fetch-url.el index bbec115..e16f787 100644 --- a/tests/test-wttrin--fetch-url.el +++ b/tests/test-wttrin--fetch-url.el @@ -156,31 +156,6 @@ (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." diff --git a/tests/test-wttrin--handle-fetch-callback.el b/tests/test-wttrin--handle-fetch-callback.el new file mode 100644 index 0000000..203a232 --- /dev/null +++ b/tests/test-wttrin--handle-fetch-callback.el @@ -0,0 +1,214 @@ +;;; test-wttrin--handle-fetch-callback.el --- Tests for wttrin--handle-fetch-callback -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Craig Jennings + +;;; Commentary: +;; Unit tests for wttrin--handle-fetch-callback function. +;; Tests callback coordination and error handling logic in isolation. + +;;; Code: + +(require 'ert) +(require 'wttrin) + +;;; Normal Cases + +(ert-deftest test-wttrin--handle-fetch-callback-normal-successful-response () + "Test handling successful response with callback invocation." + (let ((callback-called nil) + (callback-data nil)) + ;; Mock wttrin--extract-response-body to return test data + (cl-letf (((symbol-function 'wttrin--extract-response-body) + (lambda () "Weather: ☀️ Sunny"))) + (wttrin--handle-fetch-callback + nil ;; status with no error + (lambda (data) + (setq callback-called t) + (setq callback-data data))) + + (should callback-called) + (should (string= "Weather: ☀️ Sunny" callback-data))))) + +(ert-deftest test-wttrin--handle-fetch-callback-normal-empty-response () + "Test handling empty but successful response." + (let ((callback-called nil) + (callback-data 'not-nil)) + (cl-letf (((symbol-function 'wttrin--extract-response-body) + (lambda () ""))) + (wttrin--handle-fetch-callback + nil + (lambda (data) + (setq callback-called t) + (setq callback-data data))) + + (should callback-called) + (should (string= "" callback-data))))) + +(ert-deftest test-wttrin--handle-fetch-callback-normal-large-response () + "Test handling large response data." + (let ((callback-called nil) + (callback-data nil) + (large-data (make-string 10000 ?x))) + (cl-letf (((symbol-function 'wttrin--extract-response-body) + (lambda () large-data))) + (wttrin--handle-fetch-callback + nil + (lambda (data) + (setq callback-called t) + (setq callback-data data))) + + (should callback-called) + (should (= 10000 (length callback-data))) + (should (string= large-data callback-data))))) + +;;; Boundary Cases + +(ert-deftest test-wttrin--handle-fetch-callback-boundary-nil-status () + "Test handling nil status (successful response)." + (let ((callback-called nil)) + (cl-letf (((symbol-function 'wttrin--extract-response-body) + (lambda () "data"))) + (wttrin--handle-fetch-callback + nil ;; nil status means success + (lambda (data) + (setq callback-called t))) + + (should callback-called)))) + +(ert-deftest test-wttrin--handle-fetch-callback-boundary-empty-status-plist () + "Test handling empty status plist." + (let ((callback-called nil) + (callback-data nil)) + (cl-letf (((symbol-function 'wttrin--extract-response-body) + (lambda () "data"))) + (wttrin--handle-fetch-callback + '() ;; empty plist, no error key + (lambda (data) + (setq callback-called t) + (setq callback-data data))) + + (should callback-called) + (should (string= "data" callback-data))))) + +(ert-deftest test-wttrin--handle-fetch-callback-boundary-status-with-other-keys () + "Test handling status with various keys but no error." + (let ((callback-called nil)) + (cl-letf (((symbol-function 'wttrin--extract-response-body) + (lambda () "data"))) + (wttrin--handle-fetch-callback + '(:peer "example.com" :redirect nil) ;; status with other keys + (lambda (data) + (setq callback-called t))) + + (should callback-called)))) + +;;; Error Cases + +(ert-deftest test-wttrin--handle-fetch-callback-error-network-error () + "Test handling network error in status." + (let ((callback-called nil) + (callback-data 'not-nil)) + (wttrin--handle-fetch-callback + '(:error (error "Network unreachable")) + (lambda (data) + (setq callback-called t) + (setq callback-data data))) + + (should callback-called) + (should (null callback-data)))) + +(ert-deftest test-wttrin--handle-fetch-callback-error-http-404 () + "Test handling HTTP error status." + (let ((callback-called nil) + (callback-data 'not-nil)) + (wttrin--handle-fetch-callback + '(:error (error "HTTP 404")) + (lambda (data) + (setq callback-called t) + (setq callback-data data))) + + (should callback-called) + (should (null callback-data)))) + +(ert-deftest test-wttrin--handle-fetch-callback-error-timeout () + "Test handling timeout error." + (let ((callback-called nil) + (callback-data 'not-nil)) + (wttrin--handle-fetch-callback + '(:error (error "Request timed out")) + (lambda (data) + (setq callback-called t) + (setq callback-data data))) + + (should callback-called) + (should (null callback-data)))) + +(ert-deftest test-wttrin--handle-fetch-callback-error-callback-throws () + "Test handling errors thrown by user callback." + (let ((callback-called nil) + (error-caught nil)) + (cl-letf (((symbol-function 'wttrin--extract-response-body) + (lambda () "data"))) + ;; Should not propagate error from callback + (condition-case err + (wttrin--handle-fetch-callback + nil + (lambda (data) + (setq callback-called t) + (error "User callback error"))) + (error + (setq error-caught (error-message-string err)))) + + (should callback-called) + ;; Error should be caught and handled, not propagated + (should-not error-caught)))) + +(ert-deftest test-wttrin--handle-fetch-callback-error-extract-body-throws () + "Test that errors during body extraction are not caught and propagate." + (let ((callback-called nil) + (error-caught nil)) + (cl-letf (((symbol-function 'wttrin--extract-response-body) + (lambda () (error "Extraction error")))) + (condition-case err + (wttrin--handle-fetch-callback + nil + (lambda (data) + (setq callback-called t))) + (error + (setq error-caught t))) + + ;; Extraction errors propagate, callback should not be called + (should-not callback-called) + (should error-caught)))) + +(ert-deftest test-wttrin--handle-fetch-callback-error-nil-data-from-extract () + "Test handling nil data returned from extraction." + (let ((callback-called nil) + (callback-data 'not-nil)) + (cl-letf (((symbol-function 'wttrin--extract-response-body) + (lambda () nil))) + (wttrin--handle-fetch-callback + nil + (lambda (data) + (setq callback-called t) + (setq callback-data data))) + + (should callback-called) + (should (null callback-data))))) + +(ert-deftest test-wttrin--handle-fetch-callback-error-multiple-error-keys () + "Test handling status with multiple error indicators." + (let ((callback-called nil) + (callback-data 'not-nil)) + (wttrin--handle-fetch-callback + '(:error (error "First error") :another-error "Second error") + (lambda (data) + (setq callback-called t) + (setq callback-data data))) + + (should callback-called) + ;; Should return nil when :error key is present + (should (null callback-data)))) + +(provide 'test-wttrin--handle-fetch-callback) +;;; test-wttrin--handle-fetch-callback.el ends here @@ -213,6 +213,53 @@ This is a pure function with no side effects, suitable for testing." (wttrin-additional-url-params) "A")) +(defun wttrin--extract-response-body () + "Extract and decode HTTP response body from current buffer. +Skips headers and returns UTF-8 decoded body. +Returns nil on error. Kills buffer when done." + (condition-case err + (unwind-protect + (progn + (goto-char (point-min)) + (re-search-forward "\r?\n\r?\n" nil t) + (let ((body (decode-coding-string + (buffer-substring-no-properties (point) (point-max)) + 'utf-8))) + (when (featurep 'wttrin-debug) + (wttrin--debug-log "wttrin--extract-response-body: Successfully fetched %d bytes" + (length body))) + body)) + (ignore-errors (kill-buffer (current-buffer)))) + (error + (when (featurep 'wttrin-debug) + (wttrin--debug-log "wttrin--extract-response-body: Error - %s" + (error-message-string err))) + (ignore-errors (kill-buffer (current-buffer))) + nil))) + +(defun wttrin--handle-fetch-callback (status callback) + "Handle url-retrieve callback STATUS and invoke CALLBACK with result. +Extracts response body or handles errors, then calls CALLBACK with data or nil." + (when (featurep 'wttrin-debug) + (wttrin--debug-log "wttrin--handle-fetch-callback: Invoked with status = %S" status)) + (let ((data nil)) + (if (plist-get status :error) + (when (featurep 'wttrin-debug) + (wttrin--debug-log "wttrin--handle-fetch-callback: Network error - %s" + (cdr (plist-get status :error)))) + (setq data (wttrin--extract-response-body))) + (condition-case err + (progn + (when (featurep 'wttrin-debug) + (wttrin--debug-log "wttrin--handle-fetch-callback: Calling user callback with %s" + (if data (format "%d bytes" (length data)) "nil"))) + (funcall callback data)) + (error + (when (featurep 'wttrin-debug) + (wttrin--debug-log "wttrin--handle-fetch-callback: Error in user callback - %s" + (error-message-string err))) + (message "wttrin: Error in callback - %s" (error-message-string err)))))) + (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. @@ -221,35 +268,9 @@ Handles header skipping, UTF-8 decoding, and error handling automatically." (wttrin--debug-log "wttrin--fetch-url: Starting fetch for URL: %s" url)) (let ((url-request-extra-headers (list wttrin-default-languages)) (url-user-agent "curl")) - (url-retrieve - url - (lambda (status) - (let ((data nil)) - (condition-case err - (if (plist-get status :error) - (progn - (when (featurep 'wttrin-debug) - (wttrin--debug-log "wttrin--fetch-url: 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 (decode-coding-string - (buffer-substring-no-properties (point) (point-max)) - 'utf-8)) - (when (featurep 'wttrin-debug) - (wttrin--debug-log "wttrin--fetch-url: Successfully fetched %d bytes" - (length data)))) - (kill-buffer (current-buffer)))) - (error - (when (featurep 'wttrin-debug) - (wttrin--debug-log "wttrin--fetch-url: Error processing response - %s" - (error-message-string err))) - (setq data nil))) - (funcall callback data)))))) + (url-retrieve url + (lambda (status) + (wttrin--handle-fetch-callback status callback))))) (defun wttrin-fetch-raw-string (query callback) "Asynchronously fetch weather information for QUERY. |
