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