test_component.rb

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