1# frozen_string_literal: true
2
3require "test_helper"
4require_relative "../sgx-bwmsgsv2"
5
6def panic(e)
7 $panic = e
8end
9
10class ComponentTest < 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 xmpp_error_name(error)
32 error.find_first(
33 "child::*[name()!='text']",
34 Blather::StanzaError::STANZA_ERR_NS
35 ).element_name
36 end
37
38 def xmpp_error_text(error)
39 error.find_first("ns:text", ns: Blather::StanzaError::STANZA_ERR_NS)&.text
40 end
41
42 def process_stanza(s)
43 SGXbwmsgsv2.send(:client).receive_data(s)
44 raise $panic if $panic
45 end
46
47 def test_message_unregistered
48 m = Blather::Stanza::Message.new("+15551234567@component", "a"*4096)
49 m.from = "unknown@example.com"
50 process_stanza(m)
51
52 assert_equal 1, written.length
53
54 stanza = Blather::XMPPNode.parse(written.first.to_xml)
55 assert stanza.error?
56 error = stanza.find_first("error")
57 assert_equal "auth", error["type"]
58 assert_equal "registration-required", xmpp_error_name(error)
59 end
60 em :test_message_unregistered
61
62 def test_message_too_long
63 req = stub_request(
64 :post,
65 "https://messaging.bandwidth.com/api/v2/users/account/messages"
66 ).with(body: {
67 from: "+15550000000",
68 to: "+15551234567",
69 text: "a"*4096,
70 applicationId: nil,
71 tag: " "
72 }).to_return(status: 400, body: JSON.dump(
73 description: "Bad text.",
74 fieldErrors: [{ description: "4096 not allowed" }]
75 ))
76
77 m = Blather::Stanza::Message.new("+15551234567@component", "a"*4096)
78 m.from = "test@example.com"
79 process_stanza(m)
80
81 assert_requested req
82 assert_equal 1, written.length
83
84 stanza = Blather::XMPPNode.parse(written.first.to_xml)
85 assert stanza.error?
86 error = stanza.find_first("error")
87 assert_equal "cancel", error["type"]
88 assert_equal "internal-server-error", xmpp_error_name(error)
89 assert_equal "Bad text. 4096 not allowed", xmpp_error_text(error)
90 end
91 em :test_message_too_long
92
93 def test_message_to_component_not_group
94 m = Blather::Stanza::Message.new("component", "a"*4096)
95 m.from = "test@example.com"
96 process_stanza(m)
97
98 assert_equal 1, written.length
99
100 stanza = Blather::XMPPNode.parse(written.first.to_xml)
101 assert stanza.error?
102 error = stanza.find_first("error")
103 assert_equal "cancel", error["type"]
104 assert_equal "item-not-found", xmpp_error_name(error)
105 end
106 em :test_message_to_component_not_group
107
108 def test_message_to_invalid_num
109 m = Blather::Stanza::Message.new("123@component", "a"*4096)
110 m.from = "test@example.com"
111 process_stanza(m)
112
113 assert_equal 1, written.length
114
115 stanza = Blather::XMPPNode.parse(written.first.to_xml)
116 assert stanza.error?
117 error = stanza.find_first("error")
118 assert_equal "cancel", error["type"]
119 assert_equal "item-not-found", xmpp_error_name(error)
120 end
121 em :test_message_to_invalid_num
122
123 def test_message_to_anonymous
124 m = Blather::Stanza::Message.new(
125 "1;phone-context=anonymous.phone-context.soprani.ca@component",
126 "a"*4096
127 )
128 m.from = "test@example.com"
129 process_stanza(m)
130
131 assert_equal 1, written.length
132
133 stanza = Blather::XMPPNode.parse(written.first.to_xml)
134 assert stanza.error?
135 error = stanza.find_first("error")
136 assert_equal "cancel", error["type"]
137 assert_equal "gone", xmpp_error_name(error)
138 end
139 em :test_message_to_anonymous
140
141 def test_blank_message
142 m = Blather::Stanza::Message.new("+15551234567@component", " ")
143 m.from = "test@example.com"
144 process_stanza(m)
145
146 assert_equal 1, written.length
147
148 stanza = Blather::XMPPNode.parse(written.first.to_xml)
149 assert stanza.error?
150 error = stanza.find_first("error")
151 assert_equal "modify", error["type"]
152 assert_equal "policy-violation", xmpp_error_name(error)
153 end
154 em :test_blank_message
155
156 def test_ibr_bad_tel
157 iq = Blather::Stanza::Iq::IBR.new(:set, "component")
158 iq.from = "newuser@example.com"
159 iq.phone = "5551234567"
160 process_stanza(iq)
161
162 assert_equal 1, written.length
163
164 stanza = Blather::XMPPNode.parse(written.first.to_xml)
165 assert stanza.error?
166 error = stanza.find_first("error")
167 assert_equal "cancel", error["type"]
168 assert_equal "item-not-found", xmpp_error_name(error)
169 end
170 em :test_ibr_bad_tel
171
172 def test_ibr_bad_creds
173 stub_request(
174 :get,
175 "https://messaging.bandwidth.com/api/v2/users/acct/media"
176 ).with(basic_auth: ["user", "pw"]).to_return(status: 401)
177
178 iq = Blather::Stanza::Iq::IBR.new(:set, "component")
179 iq.from = "newuser@example.com"
180 iq.phone = "+15551234567"
181 iq.nick = "acct"
182 iq.username = "user"
183 iq.password = "pw"
184 process_stanza(iq)
185
186 assert_equal 1, written.length
187
188 stanza = Blather::XMPPNode.parse(written.first.to_xml)
189 assert stanza.error?
190 error = stanza.find_first("error")
191 assert_equal "auth", error["type"]
192 assert_equal "not-authorized", xmpp_error_name(error)
193 end
194 em :test_ibr_bad_creds
195
196 def test_ibr_number_not_found
197 stub_request(
198 :get,
199 "https://messaging.bandwidth.com/api/v2/users/acct/media"
200 ).with(basic_auth: ["user", "pw"]).to_return(status: 404)
201
202 iq = Blather::Stanza::Iq::IBR.new(:set, "component")
203 iq.from = "newuser@example.com"
204 iq.phone = "+15551234567"
205 iq.nick = "acct"
206 iq.username = "user"
207 iq.password = "pw"
208 process_stanza(iq)
209
210 assert_equal 1, written.length
211
212 stanza = Blather::XMPPNode.parse(written.first.to_xml)
213 assert stanza.error?
214 error = stanza.find_first("error")
215 assert_equal "cancel", error["type"]
216 assert_equal "item-not-found", xmpp_error_name(error)
217 end
218 em :test_ibr_number_not_found
219
220 def test_ibr_other_error
221 stub_request(
222 :get,
223 "https://messaging.bandwidth.com/api/v2/users/acct/media"
224 ).with(basic_auth: ["user", "pw"]).to_return(status: 400)
225
226 iq = Blather::Stanza::Iq::IBR.new(:set, "component")
227 iq.from = "newuser@example.com"
228 iq.phone = "+15551234567"
229 iq.nick = "acct"
230 iq.username = "user"
231 iq.password = "pw"
232 process_stanza(iq)
233
234 assert_equal 1, written.length
235
236 stanza = Blather::XMPPNode.parse(written.first.to_xml)
237 assert stanza.error?
238 error = stanza.find_first("error")
239 assert_equal "modify", error["type"]
240 assert_equal "not-acceptable", xmpp_error_name(error)
241 end
242 em :test_ibr_other_error
243
244 def test_ibr_new
245 stub_request(
246 :get,
247 "https://messaging.bandwidth.com/api/v2/users/acct/media"
248 ).with(basic_auth: ["user", "pw"]).to_return(status: 200, body: "[]")
249
250 iq = Blather::Stanza::Iq::IBR.new(:set, "component")
251 iq.from = "test9@example.com"
252 iq.phone = "+15550000009"
253 iq.nick = "acct"
254 iq.username = "user"
255 iq.password = "pw"
256 process_stanza(iq)
257
258 assert_equal 1, written.length
259
260 stanza = Blather::XMPPNode.parse(written.first.to_xml)
261 refute stanza.error?
262 assert_equal(
263 ["acct", "user", "pw", "+15550000009"],
264 REDIS.get("catapult_cred-test9@example.com").sync
265 )
266 assert_equal "test9@example.com", REDIS.get("catapult_jid-+15550000009").sync
267 assert REDIS.get("catapult_jid-").sync
268 end
269 em :test_ibr_new
270
271 def test_ibr_update
272 stub_request(
273 :get,
274 "https://messaging.bandwidth.com/api/v2/users/acct/media"
275 ).with(basic_auth: ["user", "pw"]).to_return(status: 200, body: "[]")
276
277 iq = Blather::Stanza::Iq::IBR.new(:set, "component")
278 iq.from = "test@example.com"
279 iq.phone = "+15550000009"
280 iq.nick = "acct"
281 iq.username = "user"
282 iq.password = "pw"
283 process_stanza(iq)
284
285 assert_equal 1, written.length
286
287 stanza = Blather::XMPPNode.parse(written.first.to_xml)
288 refute stanza.error?
289 assert_equal(
290 ["acct", "user", "pw", "+15550000009"],
291 REDIS.get("catapult_cred-test@example.com").sync
292 )
293 assert_equal "test@example.com", REDIS.get("catapult_jid-+15550000009").sync
294 refute REDIS.get("catapult_jid-+15550000000").sync
295 end
296 em :test_ibr_update
297
298 def test_ibr_conflict
299 stub_request(
300 :get,
301 "https://messaging.bandwidth.com/api/v2/users/acct/media"
302 ).with(basic_auth: ["user", "pw"]).to_return(status: 200, body: "[]")
303
304 iq = Blather::Stanza::Iq::IBR.new(:set, "component")
305 iq.from = "test2@example.com"
306 iq.phone = "+15550000000"
307 iq.nick = "acct"
308 iq.username = "user"
309 iq.password = "pw"
310 process_stanza(iq)
311
312 assert_equal 1, written.length
313
314 stanza = Blather::XMPPNode.parse(written.first.to_xml)
315 assert stanza.error?
316 error = stanza.find_first("error")
317 assert_equal "cancel", error["type"]
318 assert_equal "conflict", xmpp_error_name(error)
319 assert_equal(
320 "Another user exists for +15550000000",
321 xmpp_error_text(error)
322 )
323 end
324 em :test_ibr_conflict
325
326 def test_ibr_remove
327 iq = Blather::Stanza::Iq::IBR.new(:set, "component")
328 iq.from = "test@example.com"
329 iq.remove!
330 process_stanza(iq)
331
332 refute REDIS.get("catapult_cred-test@example.com").sync
333
334 assert_equal 1, written.length
335
336 stanza = Blather::XMPPNode.parse(written.first.to_xml)
337 assert stanza.result?
338 end
339 em :test_ibr_remove
340
341 def test_ibr_form
342 stub_request(
343 :get,
344 "https://messaging.bandwidth.com/api/v2/users/acct/media"
345 ).with(basic_auth: ["user", "pw"]).to_return(status: 200, body: "[]")
346
347 iq = Blather::Stanza::Iq::IBR.new(:set, "component")
348 iq.from = "formuser@example.com"
349 form = Blather::Stanza::X.find_or_create(iq.query)
350 form.fields = [
351 {
352 var: "nick",
353 value: "acct"
354 },
355 {
356 var: "username",
357 value: "user"
358 },
359 {
360 var: "password",
361 value: "pw"
362 },
363 {
364 var: "phone",
365 value: "+15551234567"
366 }
367 ]
368 process_stanza(iq)
369
370 assert_equal(
371 ["acct", "user", "pw", "+15551234567"],
372 REDIS.get("catapult_cred-formuser@example.com").sync
373 )
374
375 assert_equal(
376 "formuser@example.com",
377 REDIS.get("catapult_jid-+15551234567").sync
378 )
379
380 assert_equal 1, written.length
381 stanza = Blather::XMPPNode.parse(written.first.to_xml)
382 assert stanza.result?
383 end
384 em :test_ibr_form
385
386 def test_ibr_get_form_registered
387 iq = Blather::Stanza::Iq::IBR.new(:get, "component")
388 iq.from = "test@example.com"
389 process_stanza(iq)
390
391 assert_equal 1, written.length
392 stanza = Blather::XMPPNode.parse(written.first.to_xml)
393 assert stanza.result?
394 assert stanza.registered?
395 assert_equal(
396 ["nick", "username", "password", "phone"],
397 stanza.form.fields.map(&:var)
398 )
399 assert stanza.instructions
400 assert stanza.nick
401 assert stanza.username
402 assert stanza.password
403 assert stanza.phone
404 refute stanza.email
405 end
406 em :test_ibr_get_form_registered
407
408 def test_port_out_pin
409 iq = Blather::Stanza::Iq::Command.new(:set, 'component').tap do |iq|
410 iq.from = 'test@example.com'
411 iq.node = 'set-port-out-pin'
412 iq.sessionid = 'test-session-123'
413 iq.action = :complete
414 iq.form.type = :submit
415 iq.form.fields = [
416 {
417 var: 'pin',
418 value: '1234'
419 },
420 {
421 var: 'confirm_pin',
422 value: '1234'
423 }
424 ]
425 end
426
427 tn_mock = Minitest::Mock.new
428 tn_mock.expect(:get_details, { tier: 0.0, on_net_vendor: true })
429 with_stubs([
430 [
431 BandwidthIris::TnOptions,
432 :create_tn_option_order,
433 ->(client, data) { {order_id: 'test-order-123', processing_status: 'RECEIVED', error_list: {}} }
434 ],
435 [
436 BandwidthIris::TnOptions,
437 :get_tn_option_order,
438 ->(client, order_id) { {order_id: order_id, order_status: 'COMPLETE', error_list: {}} }
439 ],
440 [
441 BandwidthIris::Tn,
442 :get,
443 tn_mock
444 ]
445 ]) do
446 process_stanza(iq)
447
448 assert_equal 1, written.length
449
450 stanza = Blather::XMPPNode.parse(written.first.to_xml)
451 refute stanza.error?
452 assert_mock tn_mock
453 end
454 end
455 em :test_port_out_pin
456
457 def test_port_out_pin_mismatch
458 iq = Blather::Stanza::Iq::Command.new(:set, 'component').tap do |iq|
459 iq.from = 'test@example.com'
460 iq.node = 'set-port-out-pin'
461 iq.sessionid = 'test-session-mismatch'
462 iq.action = :complete
463 iq.form.type = :submit
464 iq.form.fields = [
465 {
466 var: 'pin',
467 value: '1234'
468 },
469 {
470 var: 'confirm_pin',
471 value: '5678'
472 }
473 ]
474 end
475
476 process_stanza(iq)
477
478 assert_equal 1, written.length
479
480 stanza = Blather::XMPPNode.parse(written.first.to_xml)
481 assert_equal :error, stanza.type
482 error = stanza.find_first("error")
483 assert_equal "modify", error["type"]
484 assert_equal "bad-request", xmpp_error_name(error)
485 assert_equal "PIN confirmation does not match", xmpp_error_text(error)
486 end
487 em :test_port_out_pin_mismatch
488
489 def test_port_out_pin_validation
490 [
491 ['123', 'PIN must be 4-10 alphanumeric characters'],
492 ['12345678901', 'PIN must be 4-10 alphanumeric characters'],
493 ['123!', 'PIN must be 4-10 alphanumeric characters'],
494 ['pin with spaces', 'PIN must be 4-10 alphanumeric characters']
495 ].each do |invalid_pin, expected_error|
496 iq = Blather::Stanza::Iq::Command.new(:set, 'component').tap do |iq|
497 iq.from = 'test@example.com'
498 iq.node = 'set-port-out-pin'
499 iq.sessionid = "test-session-validation-#{invalid_pin.gsub(/[^a-zA-Z0-9]/, '')}"
500 iq.action = :complete
501 iq.form.type = :submit
502 iq.form.fields = [
503 {
504 var: 'pin',
505 value: invalid_pin
506 },
507 {
508 var: 'confirm_pin',
509 value: invalid_pin
510 }
511 ]
512 end
513
514 process_stanza(iq)
515
516 assert_equal 1, written.length, "Failed for PIN: #{invalid_pin}"
517
518 stanza = Blather::XMPPNode.parse(written.first.to_xml)
519 assert_equal :error, stanza.type, "Expected error for PIN: #{invalid_pin}"
520 error = stanza.find_first("error")
521 assert_equal "modify", error["type"]
522 assert_equal "bad-request", xmpp_error_name(error)
523 assert_equal expected_error, xmpp_error_text(error),
524 "Wrong error message for PIN: #{invalid_pin}"
525
526 SGXbwmsgsv2.instance_variable_set(:@written, [])
527 end
528 end
529 em :test_port_out_pin_validation
530
531 def test_outbound_message_emits_to_stream
532 stub_request(
533 :post,
534 "https://messaging.bandwidth.com/api/v2/users/account/messages"
535 ).with(body: hash_including(
536 from: "+15550000000",
537 to: "+15551234567",
538 text: "Hello world"
539 )).to_return(status: 201, body: JSON.dump(id: "bw-msg-123"))
540
541 m = Blather::Stanza::Message.new("+15551234567@component", "Hello world")
542 m.from = "test@example.com/resource"
543 m['id'] = "stanza-123"
544 process_stanza(m)
545
546 entries = REDIS.stream_entries("messages").sync
547 assert_equal 1, entries.length
548
549 event = entries.first[:fields]
550 assert_equal "out", event["event"]
551 assert_equal "+15550000000", event["from"]
552 assert_equal "+15550000000", event["owner"]
553 assert_equal JSON.dump(["+15551234567"]), event["to"]
554 assert_equal "stanza-123", event["stanza_id"]
555 assert_equal "bw-msg-123", event["bandwidth_id"]
556 assert_equal "Hello world", event["body"]
557 end
558 em :test_outbound_message_emits_to_stream
559
560 def test_passthrough_message_emits_to_stream
561 REDIS.set("catapult_jid-+15559999999", "other@example.com")
562 REDIS.set("catapult_cred-other@example.com", [
563 'other_acct', 'other_user', 'other_pw', '+15559999999'
564 ])
565
566 m = Blather::Stanza::Message.new("+15559999999@component", "Pass through")
567 m.from = "test@example.com/resource"
568 m['id'] = "passthru-stanza-456"
569 process_stanza(m)
570
571 entries = REDIS.stream_entries("messages").sync
572 assert_equal 1, entries.length
573
574 event = entries.first[:fields]
575 assert_equal "thru", event["event"]
576 assert_equal "+15550000000", event["from"]
577 assert_equal JSON.dump(["+15559999999"]), event["to"]
578 assert_equal "passthru-stanza-456", event["stanza_id"]
579 assert_equal "Pass through", event["body"]
580 refute event.key?("timestamp"), "Thru events should not have a timestamp field"
581 end
582 em :test_passthrough_message_emits_to_stream
583
584 def test_sentry_captures_handler_exception
585 captured_exceptions = []
586 repo = SGXbwmsgsv2.instance_variable_get(:@registration_repo)
587
588 Sentry.stub :capture_exception, ->(*args, **) { captured_exceptions << args.first } do
589 # repo.find is called during message processing to look up creds;
590 # raising here bubbles up to SGXClient#handle_error
591 repo.stub :find, ->(_) { raise StandardError, "test error" } do
592 m = Blather::Stanza::Message.new("+15551234567@component", "hello")
593 m.from = "test@example.com"
594 process_stanza(m)
595 end
596 end
597
598 assert_equal 1, captured_exceptions.length
599 assert_instance_of StandardError, captured_exceptions.first
600 assert_equal "test error", captured_exceptions.first.message
601
602 assert_equal 1, written.length
603 stanza = Blather::XMPPNode.parse(written.first.to_xml)
604 assert stanza.error?
605 error = stanza.find_first("error")
606 assert_equal "cancel", error["type"]
607 assert_equal "internal-server-error", xmpp_error_name(error)
608
609 SGXbwmsgsv2.instance_variable_set(:@written, [])
610 end
611 em :test_sentry_captures_handler_exception
612end