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