@@ -3,7 +3,7 @@
source 'https://rubygems.org'
gem 'activesupport', '<5.0.0'
-gem 'blather'
+gem 'blather', git: "https://github.com/adhearsion/blather.git"
gem 'em-hiredis'
gem 'em-http-request'
gem 'em_promise.rb'
@@ -15,6 +15,17 @@ gem 'rack', '< 2'
gem 'redis'
gem "sentry-ruby", "<= 4.3.1"
-group :development do
- gem 'rubocop', require: false
+group(:development) do
+ gem "pry-reload"
+ gem "pry-rescue"
+ gem "pry-stack_explorer"
+end
+
+group(:test) do
+ gem 'minitest'
+ gem 'rack-test'
+ gem 'rake'
+ gem 'rubocop'
+ gem 'simplecov', require: false
+ gem 'webmock'
end
@@ -76,6 +76,7 @@ protected
def wrap_handler(*args)
v = yield(*args)
+ v = v.sync if ENV['ENV'] == 'test' && v.is_a?(Promise)
v.catch(&method(:panic)) if v.is_a?(Promise)
true # Do not run other handlers unless throw :pass
rescue Exception => e
@@ -593,7 +594,7 @@ module SGXbwmsgsv2
creds_from_registration_query(qn)
end
}.then { |user_id, api_token, api_secret, phone_num|
- if phone_num[0] == '+'
+ if phone_num && phone_num[0] == '+'
[user_id, api_token, api_secret, phone_num]
else
# TODO: add text re number not (yet) supported
@@ -724,7 +725,7 @@ module SGXbwmsgsv2
# Unknown IQ, ignore for now
EMPromise.reject(:done)
end.catch { |e|
- if e.is_a?(Array) && e.length == 2
+ if e.is_a?(Array) && (e.length == 2 || e.length == 3)
write_to_stream error_msg(i.reply, qn, *e)
elsif e != :done
EMPromise.reject(e)
@@ -1051,4 +1052,4 @@ at_exit do
end
end
end
-end
+end unless ENV['ENV'] == 'test'
@@ -0,0 +1,256 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require_relative "../sgx-bwmsgsv2"
+
+def panic(e)
+ $panic = e
+end
+
+class ComponentTest < Minitest::Test
+ def setup
+ SGXbwmsgsv2.instance_variable_set(:@written, [])
+
+ def SGXbwmsgsv2.write_to_stream(s)
+ @written ||= []
+ @written << s
+ end
+
+ REDIS.set("catapult_cred-test@example.com", [
+ 'account', 'user', 'password', '+15550000000'
+ ])
+ end
+
+ def written
+ SGXbwmsgsv2.instance_variable_get(:@written)
+ end
+
+ def xmpp_error_name(error)
+ error.find_first(
+ "child::*[name()!='text']",
+ Blather::StanzaError::STANZA_ERR_NS
+ ).element_name
+ end
+
+ def process_stanza(s)
+ SGXbwmsgsv2.send(:client).receive_data(s)
+ raise $panic if $panic
+ end
+
+ def test_message_unregistered
+ m = Blather::Stanza::Message.new("+15551234567@component", "a"*4096)
+ m.from = "unknown@example.com"
+ process_stanza(m)
+
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "auth", error["type"]
+ assert_equal "registration-required", xmpp_error_name(error)
+ end
+ em :test_message_unregistered
+
+ def test_message_too_long
+ req = stub_request(
+ :post,
+ "https://messaging.bandwidth.com/api/v2/users/account/messages"
+ ).with(body: {
+ from: "+15550000000",
+ to: "+15551234567",
+ text: "a"*4096,
+ applicationId: nil,
+ tag: " "
+ }).to_return(status: 400)
+
+ m = Blather::Stanza::Message.new("+15551234567@component", "a"*4096)
+ m.from = "test@example.com"
+ process_stanza(m)
+
+ assert_requested req
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "cancel", error["type"]
+ assert_equal "internal-server-error", xmpp_error_name(error)
+ end
+ em :test_message_too_long
+
+ def test_message_to_component_not_group
+ m = Blather::Stanza::Message.new("component", "a"*4096)
+ m.from = "test@example.com"
+ process_stanza(m)
+
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "cancel", error["type"]
+ assert_equal "item-not-found", xmpp_error_name(error)
+ end
+ em :test_message_to_component_not_group
+
+ def test_message_to_invalid_num
+ m = Blather::Stanza::Message.new("123@component", "a"*4096)
+ m.from = "test@example.com"
+ process_stanza(m)
+
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "cancel", error["type"]
+ assert_equal "item-not-found", xmpp_error_name(error)
+ end
+ em :test_message_to_invalid_num
+
+ def test_message_to_anonymous
+ m = Blather::Stanza::Message.new(
+ "1;phone-context=anonymous.phone-context.soprani.ca@component",
+ "a"*4096
+ )
+ m.from = "test@example.com"
+ process_stanza(m)
+
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "cancel", error["type"]
+ assert_equal "gone", xmpp_error_name(error)
+ end
+ em :test_message_to_anonymous
+
+ def test_blank_message
+ m = Blather::Stanza::Message.new("+15551234567@component", " ")
+ m.from = "test@example.com"
+ process_stanza(m)
+
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "modify", error["type"]
+ assert_equal "policy-violation", xmpp_error_name(error)
+ end
+ em :test_blank_message
+
+ def test_ibr_bad_tel
+ iq = Blather::Stanza::Iq::IBR.new(:set, "component")
+ iq.from = "newuser@example.com"
+ iq.phone = "5551234567"
+ process_stanza(iq)
+
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "cancel", error["type"]
+ assert_equal "item-not-found", xmpp_error_name(error)
+ end
+ em :test_ibr_bad_tel
+
+ def test_ibr_bad_creds
+ stub_request(
+ :get,
+ "https://messaging.bandwidth.com/api/v2/users/acct/media"
+ ).with(basic_auth: ["user", "pw"]).to_return(status: 401)
+
+ iq = Blather::Stanza::Iq::IBR.new(:set, "component")
+ iq.from = "newuser@example.com"
+ iq.phone = "+15551234567"
+ iq.nick = "acct"
+ iq.username = "user"
+ iq.password = "pw"
+ process_stanza(iq)
+
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "auth", error["type"]
+ assert_equal "not-authorized", xmpp_error_name(error)
+ end
+ em :test_ibr_bad_creds
+
+ def test_ibr_number_not_found
+ stub_request(
+ :get,
+ "https://messaging.bandwidth.com/api/v2/users/acct/media"
+ ).with(basic_auth: ["user", "pw"]).to_return(status: 404)
+
+ iq = Blather::Stanza::Iq::IBR.new(:set, "component")
+ iq.from = "newuser@example.com"
+ iq.phone = "+15551234567"
+ iq.nick = "acct"
+ iq.username = "user"
+ iq.password = "pw"
+ process_stanza(iq)
+
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "cancel", error["type"]
+ assert_equal "item-not-found", xmpp_error_name(error)
+ end
+ em :test_ibr_number_not_found
+
+ def test_ibr_other_error
+ stub_request(
+ :get,
+ "https://messaging.bandwidth.com/api/v2/users/acct/media"
+ ).with(basic_auth: ["user", "pw"]).to_return(status: 400)
+
+ iq = Blather::Stanza::Iq::IBR.new(:set, "component")
+ iq.from = "newuser@example.com"
+ iq.phone = "+15551234567"
+ iq.nick = "acct"
+ iq.username = "user"
+ iq.password = "pw"
+ process_stanza(iq)
+
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "modify", error["type"]
+ assert_equal "not-acceptable", xmpp_error_name(error)
+ end
+ em :test_ibr_other_error
+
+ def test_ibr_conflict
+ stub_request(
+ :get,
+ "https://messaging.bandwidth.com/api/v2/users/acct/media"
+ ).with(basic_auth: ["user", "pw"]).to_return(status: 200, body: "[]")
+
+ iq = Blather::Stanza::Iq::IBR.new(:set, "component")
+ iq.from = "test@example.com"
+ iq.phone = "+15550000000"
+ iq.nick = "acct"
+ iq.username = "user"
+ iq.password = "pw"
+ process_stanza(iq)
+
+ assert_equal 1, written.length
+
+ stanza = Blather::XMPPNode.parse(written.first.to_xml)
+ assert stanza.error?
+ error = stanza.find_first("error")
+ assert_equal "cancel", error["type"]
+ assert_equal "conflict", xmpp_error_name(error)
+ end
+ em :test_ibr_conflict
+end
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+require "simplecov"
+SimpleCov.start do
+ add_filter "/test/"
+ enable_coverage :branch
+end
+
+require "minitest/autorun"
+require "webmock/minitest"
+
+begin
+ require "pry-rescue/minitest"
+ require "pry-reload"
+
+ module Minitest
+ class Test
+ alias old_capture_exceptions capture_exceptions
+ def capture_exceptions
+ old_capture_exceptions do
+ yield
+ rescue Minitest::Skip => e
+ failures << e
+ end
+ end
+ end
+ end
+rescue LoadError
+ # Just helpers for dev, no big deal if missing
+ nil
+end
+
+$VERBOSE = nil
+ARGV[0] = "component"
+
+class FakeRedis
+ def initialize(values={})
+ @values = values
+ end
+
+ def set(key, value, *)
+ @values[key] = value
+ EMPromise.resolve("OK")
+ end
+
+ def setex(key, _expiry, value)
+ set(key, value)
+ end
+
+ def mget(*keys)
+ EMPromise.all(keys.map(&method(:get)))
+ end
+
+ def get(key)
+ EMPromise.resolve(@values[key])
+ end
+
+ def getbit(key, bit)
+ get(key).then { |v| v.to_i.to_s(2)[bit].to_i }
+ end
+
+ def bitfield(key, *ops)
+ get(key).then do |v|
+ bits = v.to_i.to_s(2)
+ ops.each_slice(3).map do |(op, encoding, offset)|
+ raise "unsupported bitfield op" unless op == "GET"
+ raise "unsupported bitfield op" unless encoding == "u1"
+
+ bits[offset].to_i
+ end
+ end
+ end
+
+ def hget(key, field)
+ @values.dig(key, field)
+ end
+
+ def hincrby(key, field, incrby)
+ @values[key] ||= {}
+ @values[key][field] ||= 0
+ @values[key][field] += incrby
+ end
+
+ def sadd(key, member)
+ @values[key] ||= Set.new
+ @values[key] << member
+ end
+
+ def srem(key, member)
+ @values[key].delete(member)
+ end
+
+ def scard(key)
+ @values[key]&.size || 0
+ end
+
+ def expire(_, _); end
+
+ def exists(*keys)
+ EMPromise.resolve(
+ @values.select { |k, _| keys.include? k }.size.to_s
+ )
+ end
+
+ def lindex(key, index)
+ get(key).then { |v| v&.fetch(index) }
+ end
+
+ def lrange(key, sindex, eindex)
+ get(key).then { |v| v ? v[sindex..eindex] : [] }
+ end
+end
+
+REDIS = FakeRedis.new
+
+module Minitest
+ class Test
+ def self.em(m)
+ alias_method "raw_#{m}", m
+ define_method(m) do
+ $panic = nil
+ e = nil
+ EM.run do
+ Fiber.new {
+ begin
+ send("raw_#{m}")
+ rescue
+ e = $!
+ ensure
+ EM.stop
+ end
+ }.resume
+ end
+ raise e if e
+ end
+ end
+ end
+end