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