From aafb9484dcb2a9955e4fabe445465d50a6c48870 Mon Sep 17 00:00:00 2001
From: Christopher Vollick <0@psycoti.ca>
Date: Mon, 13 Feb 2023 12:17:57 -0500
Subject: [PATCH] Track Call Reachability
This intercepts calls that are from the reachability numbers and checks
their status.
I had to extend the reachability repo a bit because I realized that
unlike texts which I just want to do nothing when they're intercepted,
for calls I actually have to _return_ something in that case.
Due to my short-circuit none of the existing tests needed to be changed,
because the calls weren't coming from a reachability number. But then I
added two more tests, one to make sure a call from the reachability
number works normally if reachability is not being tested for that user,
and another that verifies that if we're testing reachability it hangs up
and increments the counter, which is the actual point of this patch.
---
lib/reachability_repo.rb | 6 +--
test/test_helper.rb | 7 +++
test/test_web.rb | 95 ++++++++++++++++++++++++++++++++++++++++
web.rb | 56 +++++++++++++++--------
4 files changed, 143 insertions(+), 21 deletions(-)
diff --git a/lib/reachability_repo.rb b/lib/reachability_repo.rb
index f9ed9e563a3fb3182b7d2b9881ed428dc1875364..db074b7b364e390b910ff88151fb3bd86494a129 100644
--- a/lib/reachability_repo.rb
+++ b/lib/reachability_repo.rb
@@ -21,7 +21,7 @@ class ReachabilityRepo
end
class NotTest
- def filter
+ def filter(**)
EMPromise.resolve(yield)
end
end
@@ -32,8 +32,8 @@ class ReachabilityRepo
@key = key
end
- def filter
- @redis.incr(@key).then { nil }
+ def filter(if_yes: ->(_) {}, **)
+ @redis.incr(@key).then(&if_yes)
end
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index f68171e4ccf09afab54898a287b94e0a172c6a98..94ca984f6e85b5ce8312100758234d5402a70470 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -274,6 +274,13 @@ class FakeRedis
def lindex(key, index)
get(key).then { |v| v&.fetch(index) }
end
+
+ def incr(key)
+ get(key).then { |v|
+ n = v ? v + 1 : 0
+ set(key, n).then { n }
+ }
+ end
end
class FakeDB
diff --git a/test/test_web.rb b/test/test_web.rb
index a57d1704c88593627a97f003297e6549e23c376b..f7c7573c65ab2c04d65bf785b6a53e083a17e5cf 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -12,6 +12,8 @@ CustomerFwd::BANDWIDTH_VOICE = Minitest::Mock.new
Web::BANDWIDTH_VOICE = Minitest::Mock.new
LowBalance::AutoTopUp::CreditCardSale = Minitest::Mock.new
+ReachableRedis = Minitest::Mock.new
+
class WebTest < Minitest::Test
include Rack::Test::Methods
@@ -28,6 +30,8 @@ class WebTest < Minitest::Test
"catapult_jid-+15551234562" => "customer_customerid_topup@component",
"jmp_customer_jid-customerid_limit" => "customer@example.com",
"catapult_jid-+15551234561" => "customer_customerid_limit@component",
+ "jmp_customer_jid-customerid_reach" => "customerid_reach@example.com",
+ "catapult_jid-+15551234563" => "customer_customerid_reach@component",
"jmp_customer_jid-customerid2" => "customer2@example.com",
"catapult_jid-+15551230000" => "customer_customerid2@component"
),
@@ -52,6 +56,11 @@ class WebTest < Minitest::Test
"plan_name" => "test_usd",
"expires_at" => Time.now + 100
}],
+ ["customerid_reach"] => [{
+ "balance" => BigDecimal(10),
+ "plan_name" => "test_usd",
+ "expires_at" => Time.now + 100
+ }],
["customerid_limit"] => [{
"balance" => BigDecimal(10),
"plan_name" => "test_usd",
@@ -66,6 +75,8 @@ class WebTest < Minitest::Test
"catapult_fwd_timeout-customer_customerid_low@component" => "30",
"catapult_fwd-+15551234561" => "xmpp:customer@example.com",
"catapult_fwd_timeout-customer_customerid_limit@component" => "30",
+ "catapult_fwd-+15551234563" => "xmpp:customer@example.com",
+ "catapult_fwd_timeout-customer_customerid_reach@component" => "30",
"catapult_fwd-+15551230000" => "xmpp:customer2@example.com",
"catapult_fwd_timeout-customer_customerid2@component" => "30"
),
@@ -83,6 +94,10 @@ class WebTest < Minitest::Test
Blather::Stanza::Iq::IBR.new.tap do |ibr|
ibr.phone = "+15551234567"
end,
+ "customer_customerid_reach@component" =>
+ Blather::Stanza::Iq::IBR.new.tap do |ibr|
+ ibr.phone = "+15551234563"
+ end,
"customer_customerid_limit@component" =>
Blather::Stanza::Iq::IBR.new.tap do |ibr|
ibr.phone = "+15551234567"
@@ -96,6 +111,7 @@ class WebTest < Minitest::Test
db: FakeDB.new(
["test_usd", "+15557654321", :outbound] => [{ "rate" => 0.01 }],
["test_usd", "+15557654321", :inbound] => [{ "rate" => 0.01 }],
+ ["test_usd", "+14445556666", :inbound] => [{ "rate" => 0.01 }],
["customerid_limit"] => FakeDB::MultiResult.new(
[{ "a" => 1000 }],
[{ "settled_amount" => 15 }]
@@ -111,6 +127,10 @@ class WebTest < Minitest::Test
)
)
Web.opts[:common_logger] = FakeLog.new
+ Web.opts[:reachability_repo] = ReachabilityRepo::Voice.new(
+ redis: ReachableRedis,
+ senders: ["+14445556666"]
+ )
Web.instance_variable_set(:@outbound_transfers, { "bcall" => "oocall" })
Web.app
end
@@ -351,6 +371,47 @@ class WebTest < Minitest::Test
end
em :test_inbound
+ def test_inbound_from_reachability
+ CustomerFwd::BANDWIDTH_VOICE.expect(
+ :create_call,
+ OpenStruct.new(data: OpenStruct.new(call_id: "ocall")),
+ ["test_bw_account"],
+ body: Matching.new do |arg|
+ assert_equal(
+ "http://example.org/inbound/calls/acall?customer_id=customerid",
+ arg.answer_url
+ )
+ end
+ )
+
+ ReachableRedis.expect(
+ :exists,
+ EMPromise.resolve(0),
+ ["jmp_customer_reachability_voice-customerid"]
+ )
+
+ post(
+ "/inbound/calls",
+ {
+ from: "+14445556666",
+ to: "+15551234567",
+ callId: "acall"
+ }.to_json,
+ { "CONTENT_TYPE" => "application/json" }
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ "" \
+ "" \
+ "",
+ last_response.body
+ )
+ assert_mock CustomerFwd::BANDWIDTH_VOICE
+ assert_mock ReachableRedis
+ end
+ em :test_inbound_from_reachability
+
def test_inbound_no_bwmsgsv2
CustomerFwd::BANDWIDTH_VOICE.expect(
:create_call,
@@ -645,4 +706,38 @@ class WebTest < Minitest::Test
)
end
em :test_voicemail_no_customer
+
+ def test_inbound_from_reachability_during_reachability
+ ReachableRedis.expect(
+ :exists,
+ EMPromise.resolve(1),
+ ["jmp_customer_reachability_voice-customerid_reach"]
+ )
+ ReachableRedis.expect(
+ :incr,
+ EMPromise.resolve(1),
+ ["jmp_customer_reachability_voice-customerid_reach"]
+ )
+
+ post(
+ "/inbound/calls",
+ {
+ from: "+14445556666",
+ to: "+15551234563",
+ callId: "acall"
+ }.to_json,
+ { "CONTENT_TYPE" => "application/json" }
+ )
+
+ assert last_response.ok?
+ assert_equal(
+ "" \
+ "" \
+ "",
+ last_response.body
+ )
+ assert_mock CustomerFwd::BANDWIDTH_VOICE
+ assert_mock ReachableRedis
+ end
+ em :test_inbound_from_reachability_during_reachability
end
diff --git a/web.rb b/web.rb
index 52dff555cb9b9973c83a6bc64f8062efd641b237..a4cd33dd2784222b4b06ac40bbc8dda354e5f6a8 100644
--- a/web.rb
+++ b/web.rb
@@ -16,6 +16,7 @@ require_relative "lib/rev_ai"
require_relative "lib/roda_capture"
require_relative "lib/roda_em_promise"
require_relative "lib/rack_fiber"
+require_relative "lib/reachability_repo"
class OGMDownload
def initialize(url)
@@ -107,6 +108,10 @@ class Web < Roda
opts[:customer_repo] || CustomerRepo.new(**kwargs)
end
+ def reachability_repo(**kwargs)
+ opts[:reachability_repo] || ReachabilityRepo::Voice.new(**kwargs)
+ end
+
def find_by_tel_with_fallback(sgx_repo:, **kwargs)
customer_repo(sgx_repo: sgx_repo).find_by_tel(params["to"]).catch { |e|
next EMPromise.reject(e) if e.is_a?(CustomerRepo::NotFound)
@@ -193,6 +198,24 @@ class Web < Roda
)
end
+ def call_inputs(customer, from, call_id)
+ EMPromise.all([
+ customer.customer_id, customer.fwd,
+ call_attempt_repo.find_inbound(customer, from, call_id: call_id)
+ ])
+ end
+
+ def create_call(customer, from, call_id, application_id)
+ call_inputs(customer, from, call_id).then do |(customer_id, fwd, ca)|
+ ca.create_call(fwd, CONFIG[:creds][:account]) do |cc|
+ cc.from = from
+ cc.application_id = application_id
+ cc.answer_url = url inbound_calls_path(nil, customer_id)
+ cc.disconnect_url = url inbound_calls_path(:transfer_complete)
+ end
+ end
+ end
+
route do |r|
r.on "inbound" do
r.on "calls" do
@@ -330,24 +353,21 @@ class Web < Roda
customer_repo(
sgx_repo: Bwmsgsv2Repo.new
).find_by_tel(params["to"]).then { |customer|
- EMPromise.all([
- customer.customer_id, customer.fwd,
- call_attempt_repo.find_inbound(
- customer, params["from"], call_id: params["callId"]
- )
- ])
- }.then { |(customer_id, fwd, ca)|
- call = ca.create_call(fwd, CONFIG[:creds][:account]) { |cc|
- cc.from = params["from"]
- cc.application_id = params["applicationId"]
- cc.answer_url = url inbound_calls_path(nil, customer_id)
- cc.disconnect_url = url inbound_calls_path(:transfer_complete)
- }
-
- next EMPromise.reject(:voicemail) unless call
-
- outbound_transfers[params["callId"]] = call
- render :ring, locals: { duration: 300 }
+ reachability_repo.find(customer, params["from"]).then do |reach|
+ reach.filter(if_yes: ->(_) { render :hangup }) do
+ create_call(
+ customer,
+ params["from"],
+ params["callId"],
+ params["applicationId"]
+ ).then { |call|
+ next EMPromise.reject(:voicemail) unless call
+
+ outbound_transfers[params["callId"]] = call
+ render :ring, locals: { duration: 300 }
+ }
+ end
+ end
}.catch_only(CustomerFwd::InfiniteTimeout) { |e|
render :forward, locals: { fwd: e.fwd, from: params["from"] }
}.catch { |e|