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
298end