test_webhook_handler.rb

  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