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|