1# frozen_string_literal: true
2
3require "test_helper"
4require_relative "../sgx-bwmsgsv2"
5
6def panic(e)
7 $panic = e
8end
9
10class WebhookHandlerTest < Minitest::Test
11 def setup
12 reset_stanzas!
13 reset_redis!
14 end
15
16 def test_inbound_sms_emits_to_stream
17 payload = {
18 "type" => "message-received",
19 "to" => "+15550000000",
20 "message" => {
21 "id" => "bw-in-123",
22 "direction" => "in",
23 "owner" => "+15550000000",
24 "from" => "+15551234567",
25 "to" => ["+15550000000"],
26 "time" => "2025-01-13T10:00:00Z",
27 "text" => "Hello from outside"
28 }
29 }
30
31 invoke_webhook(payload)
32
33 entries = REDIS.stream_entries("messages").sync
34 assert_equal 1, entries.length
35
36 event = entries.first[:fields]
37 assert_equal "in", event["event"]
38 assert_equal "+15551234567", event["from"]
39 assert_equal JSON.dump(["+15550000000"]), event["to"]
40 assert_equal "bw-in-123", event["bandwidth_id"]
41 assert_equal "+15550000000", event["owner"]
42 assert_equal "Hello from outside", event["body"]
43 assert_equal JSON.dump([]), event["media_urls"]
44 end
45 em :test_inbound_sms_emits_to_stream
46
47 def test_inbound_mms_emits_to_stream_and_filters_smil
48 payload = {
49 "type" => "message-received",
50 "to" => "+15550000000",
51 "message" => {
52 "id" => "bw-mms-456",
53 "direction" => "in",
54 "owner" => "+15550000000",
55 "from" => "+15551234567",
56 "to" => ["+15550000000"],
57 "time" => "2025-01-13T10:05:00Z",
58 "text" => "Check this out",
59 "media" => [
60 "https://example.com/image.jpg",
61 "https://example.com/file.smil",
62 "https://example.com/data.txt",
63 "https://example.com/meta.xml"
64 ]
65 }
66 }
67
68 invoke_webhook(payload)
69
70 entries = REDIS.stream_entries("messages").sync
71 assert_equal 1, entries.length
72
73 event = entries.first[:fields]
74 assert_equal "in", event["event"]
75 assert_equal JSON.dump(["https://example.com/image.jpg"]), event["media_urls"]
76 end
77 em :test_inbound_mms_emits_to_stream_and_filters_smil
78
79 def test_message_delivered_emits_to_stream
80 payload = {
81 "type" => "message-delivered",
82 "to" => "+15550000000",
83 "message" => {
84 "id" => "bw-out-789",
85 "direction" => "out",
86 "owner" => "+15550000000",
87 "from" => "+15550000000",
88 "to" => ["+15551234567"],
89 "time" => "2025-01-13T10:10:00Z",
90 "tag" => "stanza-id-abc extra-data"
91 }
92 }
93
94 invoke_webhook(payload)
95
96 entries = REDIS.stream_entries("messages").sync
97 assert_equal 1, entries.length
98
99 event = entries.first[:fields]
100 assert_equal "delivered", event["event"]
101 assert_equal "stanza-id-abc", event["stanza_id"]
102 assert_equal "bw-out-789", event["bandwidth_id"]
103 assert_equal "2025-01-13T10:10:00Z", event["timestamp"]
104 end
105 em :test_message_delivered_emits_to_stream
106
107 def test_message_failed_emits_to_stream
108 payload = {
109 "type" => "message-failed",
110 "to" => "+15551234567",
111 "message" => {
112 "id" => "bw-out-999",
113 "direction" => "out",
114 "owner" => "+15550000000",
115 "from" => "+15550000000",
116 "to" => ["+15551234567"],
117 "time" => "2025-01-13T10:15:00Z",
118 "tag" => "failed-stanza-xyz extra",
119 "errorCode" => 4720,
120 "description" => "Carrier rejected message"
121 }
122 }
123
124 invoke_webhook(payload)
125
126 entries = REDIS.stream_entries("messages").sync
127 assert_equal 1, entries.length
128
129 event = entries.first[:fields]
130 assert_equal "failed", event["event"]
131 assert_equal "failed-stanza-xyz", event["stanza_id"]
132 assert_equal "bw-out-999", event["bandwidth_id"]
133 assert_equal "4720", event["error_code"]
134 assert_equal "Carrier rejected message", event["error_description"]
135 assert_equal "2025-01-13T10:15:00Z", event["timestamp"]
136 end
137 em :test_message_failed_emits_to_stream
138
139 def test_resend_emits_resend_event_instead_of_in
140 payload = {
141 "type" => "message-received",
142 "message" => {
143 "id" => "bw-in-resend-001",
144 "direction" => "in",
145 "owner" => "+15550000000",
146 "from" => "+15551234567",
147 "to" => ["+15550000000"],
148 "time" => "2025-01-13T10:00:00Z",
149 "text" => "Resent message"
150 }
151 }
152
153 invoke_webhook(
154 payload,
155 extra_env: { "HTTP_X_JMP_RESEND_OF" => "1736762400000-0" }
156 )
157
158 entries = REDIS.stream_entries("messages").sync
159 assert_equal 1, entries.length
160
161 event = entries.first[:fields]
162 assert_equal "resend", event["event"]
163 assert_equal "bwmsgsv2", event["source"]
164 assert_equal "+15550000000", event["owner"]
165 assert_equal "1736762400000-0", event["original_stream_id"]
166 assert_equal "bw-in-resend-001", event["original_bandwidth_id"]
167 refute event.key?("from"), "Resend events should not duplicate message fields"
168 refute event.key?("body"), "Resend events should not duplicate message fields"
169 end
170 em :test_resend_emits_resend_event_instead_of_in
171
172 def test_message_received_empty_params_writes_no_stanza
173 result = invoke_webhook(nil, extra_env: { "params" => {} })
174 assert_equal [200, {}, "OK"], result
175 assert_empty written
176 entries = REDIS.stream_entries("messages").sync
177 assert_equal 0, entries.length
178 end
179 em :test_message_received_empty_params_writes_no_stanza
180
181 def test_message_received_unregistered_jid_writes_no_stanza
182 payload = {
183 "type" => "message-received",
184 "to" => "+15559999999",
185 "message" => {
186 "id" => "bw-unreg-001",
187 "direction" => "in",
188 "owner" => "+15559999999",
189 "from" => "+15551234567",
190 "to" => ["+15559999999"],
191 "time" => "2025-01-13T10:00:00Z",
192 "text" => "Hello"
193 }
194 }
195
196 result = invoke_webhook(payload)
197 assert_equal [403, {}, "Customer not found\n"], result
198 assert_empty written
199 entries = REDIS.stream_entries("messages").sync
200 assert_equal 0, entries.length
201 end
202 em :test_message_received_unregistered_jid_writes_no_stanza
203
204 def test_message_received_single_recipient_text_stanza
205 payload = {
206 "type" => "message-received",
207 "to" => "+15550000000",
208 "message" => {
209 "id" => "bw-in-txt-001",
210 "direction" => "in",
211 "owner" => "+15550000000",
212 "from" => "+15551234567",
213 "to" => ["+15550000000"],
214 "time" => "2025-01-13T10:00:00Z",
215 "text" => "Hello from outside"
216 }
217 }
218
219 invoke_webhook(payload)
220
221 assert_equal 1, written.length
222 msg = written.first
223 assert_instance_of Blather::Stanza::Message, msg
224 assert_equal "test@example.com", msg.to.to_s
225 assert_equal "+15551234567@component", msg.from.to_s
226 assert_equal "Hello from outside", msg.body
227 refute msg.find_first("ns:x", ns: "jabber:x:oob")
228 refute msg.find_first(
229 "ns:addresses",
230 ns: "http://jabber.org/protocol/address"
231 )
232 end
233 em :test_message_received_single_recipient_text_stanza
234
235 def test_message_received_zero_recipients_writes_no_stanza
236 payload = {
237 "type" => "message-received",
238 "to" => "+15550000000",
239 "message" => {
240 "id" => "bw-in-zero-001",
241 "direction" => "in",
242 "owner" => "+15550000000",
243 "from" => "+15551234567",
244 "to" => [],
245 "time" => "2025-01-13T10:00:00Z",
246 "text" => "Hello with empty to"
247 }
248 }
249
250 result = invoke_webhook(payload)
251 assert_equal [400, {}, "Missing params\n"], result
252 assert_empty written
253 entries = REDIS.stream_entries("messages").sync
254 assert_equal 0, entries.length
255 end
256 em :test_message_received_zero_recipients_writes_no_stanza
257
258 def test_message_received_group_stanza_has_addresses
259 payload = {
260 "type" => "message-received",
261 "to" => "+15550000000",
262 "message" => {
263 "id" => "bw-in-grp-001",
264 "direction" => "in",
265 "owner" => "+15550000000",
266 "from" => "+15551234567",
267 "to" => ["+15550000000", "+15559999999"],
268 "time" => "2025-01-13T10:00:00Z",
269 "text" => "Hello group"
270 }
271 }
272
273 invoke_webhook(payload)
274
275 assert_equal 1, written.length
276 msg = written.first
277 assert_equal "example.com", msg.to.to_s
278 assert_equal "+15551234567@component", msg.from.to_s
279 assert_equal "Hello group", msg.body
280
281 addrs = msg.find_first("addresses")
282 refute_nil addrs
283 assert_equal(
284 "http://jabber.org/protocol/address", addrs['xmlns']
285 )
286
287 address_nodes = addrs.find("address")
288 assert_equal 2, address_nodes.length
289
290 jid_addr = address_nodes.detect { |a| a['jid'] }
291 assert_equal "to", jid_addr['type']
292 assert_equal "test@example.com", jid_addr['jid']
293
294 uri_addr = address_nodes.detect { |a| a['uri'] }
295 assert_equal "to", uri_addr['type']
296 assert_equal "sms:+15559999999", uri_addr['uri']
297 assert_equal "true", uri_addr['delivered']
298 end
299 em :test_message_received_group_stanza_has_addresses
300
301 def test_message_received_single_recipient_with_media_stanza
302 payload = {
303 "type" => "message-received",
304 "to" => "+15550000000",
305 "message" => {
306 "id" => "bw-in-media-001",
307 "direction" => "in",
308 "owner" => "+15550000000",
309 "from" => "+15551234567",
310 "to" => ["+15550000000"],
311 "time" => "2025-01-13T10:00:00Z",
312 "text" => "Check this",
313 "media" => ["https://example.com/image.jpg"]
314 }
315 }
316
317 invoke_webhook(payload)
318
319 assert_equal 2, written.length
320
321 oob_msg = written[0]
322 assert_equal "test@example.com", oob_msg.to.to_s
323 assert_equal "+15551234567@component", oob_msg.from.to_s
324 oob_x = oob_msg.find_first("x")
325 refute_nil oob_x
326 assert_equal "jabber:x:oob", oob_x['xmlns']
327 oob_url = oob_x.find_first("url")
328 refute_nil oob_url
329 assert_equal "https://example.com/image.jpg", oob_url.text
330
331 text_msg = written[1]
332 assert_equal "test@example.com", text_msg.to.to_s
333 assert_equal "+15551234567@component", text_msg.from.to_s
334 assert_equal "Check this", text_msg.body
335 end
336 em :test_message_received_single_recipient_with_media_stanza
337
338 def test_message_received_empty_body_no_media_returns_400
339 payload = {
340 "type" => "message-received",
341 "to" => "+15550000000",
342 "message" => {
343 "id" => "bw-in-empty-001",
344 "direction" => "in",
345 "owner" => "+15550000000",
346 "from" => "+15551234567",
347 "to" => ["+15550000000"],
348 "time" => "2025-01-13T10:00:00Z",
349 "text" => ""
350 }
351 }
352
353 result = invoke_webhook(payload)
354 assert_equal [400, {}, "Missing params\n"], result
355 assert_empty written
356 entries = REDIS.stream_entries("messages").sync
357 assert_equal 0, entries.length
358 end
359 em :test_message_received_empty_body_no_media_returns_400
360
361 def test_message_received_missing_body_no_media_returns_400
362 payload = {
363 "type" => "message-received",
364 "to" => "+15550000000",
365 "message" => {
366 "id" => "bw-in-nil-001",
367 "direction" => "in",
368 "owner" => "+15550000000",
369 "from" => "+15551234567",
370 "to" => ["+15550000000"],
371 "time" => "2025-01-13T10:00:00Z"
372 }
373 }
374
375 result = invoke_webhook(payload)
376 assert_equal [400, {}, "Missing params\n"], result
377 assert_empty written
378 entries = REDIS.stream_entries("messages").sync
379 assert_equal 0, entries.length
380 end
381 em :test_message_received_missing_body_no_media_returns_400
382end