AtlatestRepositorysigil-postmark

sigil-postmark / tree / testpostmark-test.sgl

1;;; Tests for sigil-postmark
2
3(import (sigil core)
4 (sigil dict)
5 (sigil string)
6 (sigil struct)
7 (sigil json)
8 (sigil test)
9 (postmark api)
10 (postmark webhook))
12;; ---------------------------------------------------------------
13;; Client construction
14;; ---------------------------------------------------------------
16(test-group "client construction"
18 (test "default api-base-url"
19 (let ((c (postmark-client server-token: "tok-1")))
20 (assert-true (postmark-client? c))
21 (assert-equal "tok-1" (postmark-client-server-token c))
22 (assert-equal "https://api.postmarkapp.com"
23 (postmark-client-api-base-url c))))
25 (test "custom api-base-url"
26 (let ((c (postmark-client server-token: "tok-2"
27 api-base-url: "https://mock.test")))
28 (assert-equal "https://mock.test"
29 (postmark-client-api-base-url c)))))
31;; ---------------------------------------------------------------
32;; Request payload building
33;; ---------------------------------------------------------------
35(test-group "build-send-payload — required fields"
37 (test "text body only"
38 (let ((p (build-send-payload "[email protected]" "[email protected]" "Hi" text: "hello")))
39 (assert-equal "[email protected]" (dict-ref p From:))
40 (assert-equal "[email protected]" (dict-ref p To:))
41 (assert-equal "Hi" (dict-ref p Subject:))
42 (assert-equal "hello" (dict-ref p TextBody:))
43 (assert-equal "outbound" (dict-ref p MessageStream:))))
45 (test "html body only"
46 (let ((p (build-send-payload "[email protected]" "[email protected]" "Hi" html: "<p>hi</p>")))
47 (assert-equal "<p>hi</p>" (dict-ref p HtmlBody:))))
49 (test "both text and html"
50 (let ((p (build-send-payload "[email protected]" "[email protected]" "Hi"
51 text: "plain" html: "<p>rich</p>")))
52 (assert-equal "plain" (dict-ref p TextBody:))
53 (assert-equal "<p>rich</p>" (dict-ref p HtmlBody:))))
55 (test "to list joined with commas"
56 (let ((p (build-send-payload "[email protected]"
58 "Hi" text: "hello")))
59 (assert-equal "[email protected], [email protected]" (dict-ref p To:)))))
61(test-group "build-send-payload — optional fields"
63 (test "cc, bcc, reply-to as strings"
64 (let ((p (build-send-payload "[email protected]" "[email protected]" "Hi"
65 text: "hi"
68 reply-to: "[email protected]")))
69 (assert-equal "[email protected]" (dict-ref p Cc:))
70 (assert-equal "[email protected]" (dict-ref p Bcc:))
71 (assert-equal "[email protected]" (dict-ref p ReplyTo:))))
73 (test "cc as list"
74 (let ((p (build-send-payload "[email protected]" "[email protected]" "Hi"
75 text: "hi"
77 (assert-equal "[email protected], [email protected]" (dict-ref p Cc:))))
79 (test "tag + metadata + stream"
80 (let ((p (build-send-payload "[email protected]" "[email protected]" "Hi"
81 text: "hi"
82 tag: "newsletter"
83 metadata: #{ campaign: "2026-04" }
84 message-stream: "broadcast")))
85 (assert-equal "newsletter" (dict-ref p Tag:))
86 (assert-equal "2026-04" (dict-ref (dict-ref p Metadata:) campaign:))
87 (assert-equal "broadcast" (dict-ref p MessageStream:))))
89 (test "omitted optional fields are absent"
90 (let ((p (build-send-payload "[email protected]" "[email protected]" "Hi" text: "hi")))
91 (assert-equal #f (dict-ref p Cc: #f))
92 (assert-equal #f (dict-ref p Bcc: #f))
93 (assert-equal #f (dict-ref p ReplyTo: #f))
94 (assert-equal #f (dict-ref p Tag: #f))
95 (assert-equal #f (dict-ref p Metadata: #f))
96 (assert-equal #f (dict-ref p HtmlBody: #f)))))
98(test-group "build-send-payload encodes to JSON"
100 (test "round-trips through json-encode/json-decode"
101 (let* ((p (build-send-payload "[email protected]" "[email protected]" "Hi"
102 text: "hello"
103 tag: "tag1"))
104 (round (json-decode (json-encode p))))
105 (assert-equal "[email protected]" (dict-ref round From:))
106 (assert-equal "hello" (dict-ref round TextBody:))
107 (assert-equal "tag1" (dict-ref round Tag:)))))
109;; ---------------------------------------------------------------
110;; Response parsing
111;; ---------------------------------------------------------------
113(test-group "parse-email-result — success"
115 (test "success response"
116 (let ((r (parse-email-result
118 SubmittedAt: "2026-04-21T10:00:00.000Z"
119 MessageID: "abc-123"
120 ErrorCode: 0
121 Message: "OK" })))
122 (assert-true (postmark-email-result? r))
123 (assert-equal "abc-123" (postmark-email-result-message-id r))
124 (assert-equal "2026-04-21T10:00:00.000Z"
125 (postmark-email-result-submitted-at r))
126 (assert-equal 0 (postmark-email-result-error-code r))
127 (assert-equal "OK" (postmark-email-result-message r))
128 (assert-equal "[email protected]" (postmark-email-result-to r)))))
130(test-group "parse-email-result — error"
132 (test "error response still parses"
133 (let ((r (parse-email-result
134 #{ ErrorCode: 406
135 Message: "Inactive recipient"
137 (assert-equal 406 (postmark-email-result-error-code r))
138 (assert-equal "Inactive recipient"
139 (postmark-email-result-message r))
140 (assert-equal "" (postmark-email-result-message-id r)))))
142(test-group "parse-bounce"
144 (test "parse bounce entry"
145 (let ((b (parse-bounce
146 #{ ID: 12345
147 Type: "HardBounce"
149 Description: "The server could not deliver your message"
150 Inactive: #t
151 CanActivate: #t
152 BouncedAt: "2026-04-20T12:00:00Z" })))
153 (assert-true (postmark-bounce? b))
154 (assert-equal 12345 (postmark-bounce-id b))
155 (assert-equal "HardBounce" (postmark-bounce-type b))
156 (assert-equal "[email protected]" (postmark-bounce-email b))
157 (assert-equal #t (postmark-bounce-inactive? b))
158 (assert-equal #t (postmark-bounce-can-activate? b))
159 (assert-equal "2026-04-20T12:00:00Z"
160 (postmark-bounce-bounced-at b)))))
162;; ---------------------------------------------------------------
163;; Webhook parsing
164;; ---------------------------------------------------------------
166(define bounce-json
167 (string-append
168 "{\"RecordType\": \"Bounce\","
169 " \"MessageID\": \"msg-1\","
170 " \"Email\": \"[email protected]\","
171 " \"Type\": \"HardBounce\","
172 " \"TypeCode\": 1,"
173 " \"Description\": \"gone\","
174 " \"Inactive\": true,"
175 " \"CanActivate\": true,"
176 " \"BouncedAt\": \"2026-04-20T12:00:00Z\"}"))
178(define spam-json
179 (string-append
180 "{\"RecordType\": \"SpamComplaint\","
181 " \"MessageID\": \"msg-2\","
182 " \"Email\": \"[email protected]\","
183 " \"BouncedAt\": \"2026-04-20T12:00:00Z\"}"))
185(define delivery-json
186 (string-append
187 "{\"RecordType\": \"Delivery\","
188 " \"MessageID\": \"msg-3\","
189 " \"Recipient\": \"[email protected]\","
190 " \"DeliveredAt\": \"2026-04-20T12:00:00Z\"}"))
192(define open-json
193 (string-append
194 "{\"RecordType\": \"Open\","
195 " \"MessageID\": \"msg-4\","
196 " \"Recipient\": \"[email protected]\","
197 " \"FirstOpen\": true,"
198 " \"Platform\": \"WebMail\","
199 " \"UserAgent\": \"Mozilla/5.0\","
200 " \"ReceivedAt\": \"2026-04-20T12:00:00Z\"}"))
202(define click-json
203 (string-append
204 "{\"RecordType\": \"Click\","
205 " \"MessageID\": \"msg-5\","
206 " \"Recipient\": \"[email protected]\","
207 " \"OriginalLink\": \"https://example.com\","
208 " \"ClickLocation\": \"HTML\","
209 " \"ReceivedAt\": \"2026-04-20T12:00:00Z\"}"))
211(test-group "webhook parsing — bounce"
213 (test "parses bounce webhook"
214 (let ((ev (postmark-parse-webhook bounce-json)))
215 (assert-true (postmark-webhook-event? ev))
216 (assert-equal 'bounce (postmark-webhook-event-type ev))
217 (assert-equal "msg-1" (postmark-webhook-event-message-id ev))
218 (assert-equal "[email protected]" (postmark-webhook-event-recipient ev))
219 (let ((d (postmark-webhook-event-details ev)))
220 (assert-equal "HardBounce" (dict-ref d bounce-type:))
221 (assert-equal 1 (dict-ref d type-code:))
222 (assert-equal #t (dict-ref d inactive?:))
223 (assert-equal "2026-04-20T12:00:00Z" (dict-ref d bounced-at:)))))
225 (test "bounce predicates"
226 (let ((ev (postmark-parse-webhook bounce-json)))
227 (assert-true (postmark-bounce-event? ev))
228 (assert-true (postmark-hard-bounce-event? ev))
229 (assert-false (postmark-spam-complaint-event? ev))
230 (assert-false (postmark-delivery-event? ev)))))
232(test-group "webhook parsing — spam complaint"
234 (test "parses spam complaint webhook"
235 (let ((ev (postmark-parse-webhook spam-json)))
236 (assert-true (postmark-spam-complaint-event? ev))
237 (assert-equal 'spam-complaint (postmark-webhook-event-type ev))
238 (assert-equal "[email protected]" (postmark-webhook-event-recipient ev)))))
240(test-group "webhook parsing — delivery"
242 (test "parses delivery webhook"
243 (let ((ev (postmark-parse-webhook delivery-json)))
244 (assert-true (postmark-delivery-event? ev))
245 (assert-equal 'delivery (postmark-webhook-event-type ev))
246 (assert-equal "[email protected]" (postmark-webhook-event-recipient ev))
247 (assert-equal "2026-04-20T12:00:00Z"
248 (dict-ref (postmark-webhook-event-details ev)
249 delivered-at:)))))
251(test-group "webhook parsing — open"
253 (test "parses open webhook"
254 (let ((ev (postmark-parse-webhook open-json)))
255 (assert-true (postmark-open-event? ev))
256 (let ((d (postmark-webhook-event-details ev)))
257 (assert-equal #t (dict-ref d first-open?:))
258 (assert-equal "WebMail" (dict-ref d platform:))
259 (assert-equal "Mozilla/5.0" (dict-ref d user-agent:))))))
261(test-group "webhook parsing — click"
263 (test "parses click webhook"
264 (let ((ev (postmark-parse-webhook click-json)))
265 (assert-true (postmark-click-event? ev))
266 (let ((d (postmark-webhook-event-details ev)))
267 (assert-equal "https://example.com" (dict-ref d original-link:))
268 (assert-equal "HTML" (dict-ref d click-location:))))))
270(test-group "webhook parsing — pre-parsed dicts"
272 (test "accepts already-parsed dict"
273 (let* ((dict (json-decode bounce-json))
274 (ev (postmark-parse-webhook dict)))
275 (assert-true (postmark-bounce-event? ev))
276 (assert-equal "[email protected]" (postmark-webhook-event-recipient ev)))))
278(test-group "webhook parsing — unknown"
280 (test "unknown record type returns #f"
281 (assert-false
282 (postmark-parse-webhook "{\"RecordType\": \"Nonsense\"}"))))
284(test-group "record construction"
286 (test "postmark-email-result"
287 (let ((r (postmark-email-result message-id: "id"
288 submitted-at: "t"
289 error-code: 0
290 message: "ok"
292 (assert-true (postmark-email-result? r))
293 (assert-equal "id" (postmark-email-result-message-id r))))
295 (test "postmark-webhook-event"
296 (let ((ev (postmark-webhook-event type: 'bounce
297 message-id: "m"
298 recipient: "r"
299 details: #{}
300 raw: #{})))
301 (assert-true (postmark-webhook-event? ev))
302 (assert-equal 'bounce (postmark-webhook-event-type ev)))))
304(run-tests)