AtlatestRepositorysigil-postmark
sigil-postmark / tree / testpostmark-test.sgl
1
;;; Tests for sigil-postmark3
(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 construction14
;; ---------------------------------------------------------------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 building33
;; ---------------------------------------------------------------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]"57
'("[email protected]" "[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"66
cc: "[email protected]"67
bcc: "[email protected]"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"76
cc: '("[email protected]" "[email protected]"))))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 parsing111
;; ---------------------------------------------------------------113
(test-group "parse-email-result — success"115
(test "success response"116
(let ((r (parse-email-result117
#{ To: "[email protected]"118
SubmittedAt: "2026-04-21T10:00:00.000Z"119
MessageID: "abc-123"120
ErrorCode: 0121
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-result134
#{ ErrorCode: 406135
Message: "Inactive recipient"136
To: "[email protected]" })))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-bounce146
#{ ID: 12345147
Type: "HardBounce"148
Email: "[email protected]"149
Description: "The server could not deliver your message"150
Inactive: #t151
CanActivate: #t152
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 parsing164
;; ---------------------------------------------------------------166
(define bounce-json167
(string-append168
"{\"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-json179
(string-append180
"{\"RecordType\": \"SpamComplaint\","181
" \"MessageID\": \"msg-2\","182
" \"Email\": \"[email protected]\","183
" \"BouncedAt\": \"2026-04-20T12:00:00Z\"}"))185
(define delivery-json186
(string-append187
"{\"RecordType\": \"Delivery\","188
" \"MessageID\": \"msg-3\","189
" \"Recipient\": \"[email protected]\","190
" \"DeliveredAt\": \"2026-04-20T12:00:00Z\"}"))192
(define open-json193
(string-append194
"{\"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-json203
(string-append204
"{\"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-false282
(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: 0290
message: "ok"291
to: "[email protected]")))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: 'bounce297
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)