blob: 56421fdbffbf5412a85dd1ff81756091710d26e4 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
|
;;; test-wttrin-error-propagation.el --- Tests for error info through callbacks -*- lexical-binding: t; -*-
;; Copyright (C) 2025-2026 Craig Jennings
;;; Commentary:
;; Tests that error information flows from the fetch layer through callbacks
;; to the display layer, so users see specific error messages instead of
;; "Perhaps the location was misspelled?" for every failure.
;;; Code:
(require 'ert)
(require 'wttrin)
(require 'testutil-wttrin)
;;; Setup and Teardown
(defun test-wttrin-error-propagation-setup ()
"Setup for error propagation tests."
(testutil-wttrin-setup)
(when (get-buffer "*wttr.in*")
(kill-buffer "*wttr.in*")))
(defun test-wttrin-error-propagation-teardown ()
"Teardown for error propagation tests."
(testutil-wttrin-teardown)
(when (get-buffer "*wttr.in*")
(kill-buffer "*wttr.in*")))
;;; --------------------------------------------------------------------------
;;; handle-fetch-callback passes error info to callback
;;; --------------------------------------------------------------------------
(ert-deftest test-wttrin-error-propagation-network-error-reaches-callback ()
"Network error description should be passed as second callback argument."
(let ((received-error nil))
(cl-letf (((symbol-function 'wttrin--extract-response-body)
(lambda () nil))
((symbol-function 'message) #'ignore))
(wttrin--handle-fetch-callback
'(:error (error "Connection refused"))
(lambda (_data &optional error-msg)
(setq received-error error-msg)))
(should received-error)
(should (string-match-p "network" (downcase received-error))))))
(ert-deftest test-wttrin-error-propagation-http-error-reaches-callback ()
"HTTP error description should be passed as second callback argument."
(let ((received-error nil))
(cl-letf (((symbol-function 'wttrin--extract-response-body)
(lambda () nil))
((symbol-function 'wttrin--extract-http-status)
(lambda () 404))
((symbol-function 'message) #'ignore))
(wttrin--handle-fetch-callback
nil
(lambda (_data &optional error-msg)
(setq received-error error-msg)))
(should received-error)
(should (string-match-p "not found\\|404" (downcase received-error))))))
(ert-deftest test-wttrin-error-propagation-success-no-error ()
"Successful fetch should pass nil as the error argument."
(let ((received-error 'not-called))
(cl-letf (((symbol-function 'wttrin--extract-response-body)
(lambda () "weather data"))
((symbol-function 'wttrin--extract-http-status)
(lambda () 200)))
(wttrin--handle-fetch-callback
nil
(lambda (_data &optional error-msg)
(setq received-error error-msg)))
(should-not received-error))))
;;; --------------------------------------------------------------------------
;;; get-cached-or-fetch forwards error to caller's callback
;;; --------------------------------------------------------------------------
(ert-deftest test-wttrin-error-propagation-cache-miss-forwards-error ()
"When fetch fails with no cache, the error message should reach the caller."
(test-wttrin-error-propagation-setup)
(unwind-protect
(let ((received-error nil))
(cl-letf (((symbol-function 'wttrin-fetch-raw-string)
(lambda (_query callback)
(funcall callback nil "Network error — check your connection"))))
(let ((wttrin--force-refresh t))
(wttrin--get-cached-or-fetch
"Paris"
(lambda (_data &optional error-msg)
(setq received-error error-msg)))
(should received-error)
(should (string-match-p "network" (downcase received-error))))))
(test-wttrin-error-propagation-teardown)))
(ert-deftest test-wttrin-error-propagation-stale-cache-no-error ()
"When fetch fails but stale cache exists, serve the data without an error."
(test-wttrin-error-propagation-setup)
(unwind-protect
(let ((received-data nil)
(received-error 'not-called))
(testutil-wttrin-add-to-cache "Paris" "old weather" 9999)
(cl-letf (((symbol-function 'wttrin-fetch-raw-string)
(lambda (_query callback)
(funcall callback nil "Network error")))
((symbol-function 'message) #'ignore))
(let ((wttrin--force-refresh t))
(wttrin--get-cached-or-fetch
"Paris"
(lambda (data &optional error-msg)
(setq received-data data)
(setq received-error error-msg)))
;; Should get stale data, not an error
(should received-data)
(should-not received-error))))
(test-wttrin-error-propagation-teardown)))
;;; --------------------------------------------------------------------------
;;; display-weather shows specific error message
;;; --------------------------------------------------------------------------
(ert-deftest test-wttrin-error-propagation-display-shows-specific-error ()
"When an error message is provided, display-weather should show it
instead of the generic 'Perhaps the location was misspelled?' text."
(test-wttrin-error-propagation-setup)
(unwind-protect
(let ((displayed-message nil))
(cl-letf (((symbol-function 'message)
(lambda (fmt &rest args)
(setq displayed-message (apply #'format fmt args)))))
(wttrin--display-weather "Paris" nil "Network error — check your connection")
(should displayed-message)
(should (string-match-p "network" (downcase displayed-message)))
;; Should NOT show the generic message
(should-not (string-match-p "misspelled" (downcase displayed-message)))))
(test-wttrin-error-propagation-teardown)))
(ert-deftest test-wttrin-error-propagation-display-falls-back-to-generic ()
"When no error message is provided, display-weather should show the
generic message for backward compatibility."
(test-wttrin-error-propagation-setup)
(unwind-protect
(let ((displayed-message nil))
(cl-letf (((symbol-function 'message)
(lambda (fmt &rest args)
(setq displayed-message (apply #'format fmt args)))))
(wttrin--display-weather "Paris" nil)
(should displayed-message)
(should (string-match-p "Cannot retrieve" displayed-message))))
(test-wttrin-error-propagation-teardown)))
;;; --------------------------------------------------------------------------
;;; End-to-end: full query chain
;;; --------------------------------------------------------------------------
(ert-deftest test-wttrin-error-propagation-e2e-404-shows-not-found ()
"Full chain: user queries a bad location, API returns 404, user sees
'not found' — not the generic 'misspelled' message."
(test-wttrin-error-propagation-setup)
(unwind-protect
(let ((displayed-message nil))
(cl-letf (((symbol-function 'url-retrieve)
(lambda (_url callback)
(with-current-buffer (generate-new-buffer " *test-404*")
(insert "HTTP/1.1 404 Not Found\r\n\r\nUnknown location")
(funcall callback nil))))
((symbol-function 'message)
(lambda (fmt &rest args)
(setq displayed-message (apply #'format fmt args)))))
(wttrin-query "Nonexistent Place")
(should displayed-message)
(should (string-match-p "not found\\|404" (downcase displayed-message)))))
(test-wttrin-error-propagation-teardown)))
(ert-deftest test-wttrin-error-propagation-e2e-network-error-shows-network ()
"Full chain: network is down, user sees 'network error'."
(test-wttrin-error-propagation-setup)
(unwind-protect
(let ((displayed-message nil))
(cl-letf (((symbol-function 'url-retrieve)
(lambda (_url callback)
(with-temp-buffer
(funcall callback '(:error (error "Connection refused"))))))
((symbol-function 'message)
(lambda (fmt &rest args)
(setq displayed-message (apply #'format fmt args)))))
(let ((wttrin--force-refresh t))
(wttrin-query "Paris"))
(should displayed-message)
(should (string-match-p "network" (downcase displayed-message)))))
(test-wttrin-error-propagation-teardown)))
;;; --------------------------------------------------------------------------
;;; Passthrough layers
;;; --------------------------------------------------------------------------
(ert-deftest test-wttrin-error-propagation-fetch-raw-string-forwards-error ()
"wttrin-fetch-raw-string is a passthrough — error-msg from the fetch
layer must not be dropped."
(let ((received-error nil))
(cl-letf (((symbol-function 'wttrin--fetch-url)
(lambda (_url callback)
(funcall callback nil "test error message"))))
(wttrin-fetch-raw-string
"Paris"
(lambda (_data &optional error-msg)
(setq received-error error-msg)))
;; fetch-raw-string calls fetch-url which calls handle-fetch-callback
;; which calls our callback — but we mocked fetch-url to call directly,
;; so the error-msg should pass through
(should (equal received-error "test error message")))))
(ert-deftest test-wttrin-error-propagation-http-3xx-shows-unexpected ()
"HTTP 3xx that reaches us (unusual — url-retrieve follows redirects)
should still produce an error message, not silently return nil."
(let ((received-error nil))
(cl-letf (((symbol-function 'wttrin--extract-response-body)
(lambda () nil))
((symbol-function 'wttrin--extract-http-status)
(lambda () 301))
((symbol-function 'message) #'ignore))
(wttrin--handle-fetch-callback
nil
(lambda (_data &optional error-msg)
(setq received-error error-msg)))
(should received-error)
(should (string-match-p "301\\|unexpected" (downcase received-error))))))
(provide 'test-wttrin-error-propagation)
;;; test-wttrin-error-propagation.el ends here
|