Merge branch 'reset-sip-v2'

Stephen Paul Weber created

* reset-sip-v2:
  After SIP reset, offer to change inbound fwd
  Reset sip account using v2 API

Change summary

Gemfile                            |   2 
config-schema.dhall                |   2 
config.dhall.sample                |   5 +
lib/backend_sgx.rb                 |   8 ++
lib/customer.rb                    |   6 -
lib/registration.rb                |   6 
lib/sip_account.rb                 | 110 ++++++++++++++-----------------
sgx_jmp.rb                         |  32 +++++++-
test/data/catapult_create_sip.json |   1 
test/test_customer.rb              |  56 ++++------------
test/test_helper.rb                |   4 +
test/test_registration.rb          |  14 ++-
test/test_sip_account.rb           | 102 +++++++++++++----------------
13 files changed, 165 insertions(+), 183 deletions(-)

Detailed changes

Gemfile 🔗

@@ -18,7 +18,7 @@ gem "multibases"
 gem "multihashes"
 gem "ougai"
 gem "roda"
-gem "ruby-bandwidth-iris"
+gem "ruby-bandwidth-iris", git: "https://github.com/singpolyma/ruby-bandwidth-iris", branch: "sip_credential"
 gem "sentry-ruby", "<= 4.3.1"
 gem "slim"
 gem "statsd-instrument", git: "https://github.com/singpolyma/statsd-instrument.git", branch: "graphite"

config-schema.dhall 🔗

@@ -1,7 +1,6 @@
 { activation_amount : Natural
 , admins : List Text
 , adr : Text
-, bandwidth_app : Text
 , bandwidth_peer : Text
 , bandwidth_site : Text
 , braintree :
@@ -44,6 +43,7 @@
       }
 , server : { host : Text, port : Natural }
 , sgx : Text
+, sip : { app : Text, realm : Text }
 , sip_host : Text
 , upstream_domain : Text
 , web : < Inet : { interface : Text, port : Natural } | Unix : Text >

config.dhall.sample 🔗

@@ -33,7 +33,6 @@ in
 	},
 	bandwidth_site = "",
 	bandwidth_peer = "",
-	bandwidth_app = "", -- This can be any voice app
 	braintree = {
 		environment = "sandbox",
 		merchant_id = "",
@@ -47,6 +46,10 @@ in
 	xep0157 = [
 		{ var = "support-addresses", value = "xmpp:+14169938000@cheogram.com", label = "Support" }
 	],
+	sip = {
+		realm = "",
+		app = ""
+	},
 	notify_admin = "muc@example.com",
 	sip_host = "sip.jmp.chat",
 	plans = [

lib/backend_sgx.rb 🔗

@@ -24,7 +24,9 @@ class BackendSgx
 		ibr.username = creds[:username]
 		ibr.password = creds[:password]
 		ibr.phone = tel
-		IQ_MANAGER.write(ibr)
+		IQ_MANAGER.write(ibr).then do
+			with(registered?: irb)
+		end
 	end
 
 	def stanza(s)
@@ -34,6 +36,10 @@ class BackendSgx
 		end
 	end
 
+	def set_fwd(uri)
+		REDIS.set("catapult_fwd-#{registered?.phone}", uri)
+	end
+
 	def set_fwd_timeout(timeout)
 		REDIS.set("catapult_fwd_timeout-#{from_jid}", timeout)
 	end

lib/customer.rb 🔗

@@ -23,7 +23,7 @@ class Customer
 	def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan,
 	               :currency, :merchant_account, :plan_name, :auto_top_up_amount
 	def_delegators :@sgx, :register!, :registered?, :set_ogm_url,
-	               :set_fwd_timeout, :fwd, :transcription_enabled
+	               :set_fwd, :fwd, :transcription_enabled
 	def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage
 
 	def initialize(
@@ -92,9 +92,7 @@ class Customer
 	end
 
 	def reset_sip_account
-		SipAccount::New.new(username: customer_id).put.catch do
-			sip_account.then { |acct| acct.with_random_password.put }
-		end
+		sip_account.with_random_password.put
 	end
 
 	def btc_addresses

lib/registration.rb 🔗

@@ -459,11 +459,11 @@ class Registration
 		end
 
 		def customer_active_tel_purchased
-			@customer.register!(@tel).catch(&method(:raise_setup_error)).then {
+			@customer.register!(@tel).catch(&method(:raise_setup_error)).then { |sgx|
 				EMPromise.all([
 					REDIS.del("pending_tel_for-#{@customer.jid}"),
-					REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
-					@customer.set_fwd_timeout(25) # ~5 seconds / ring, 5 rings
+					sgx.set_fwd(cheogram_sip_addr),
+					sgx.set_fwd_timeout(25) # ~5 seconds / ring, 5 rings
 				])
 			}.then do
 				Command.finish("Your JMP account has been activated as #{@tel}")

lib/sip_account.rb 🔗

@@ -1,44 +1,38 @@
 # frozen_string_literal: true
 
-require "em-synchrony/em-http" # For aget vs get
+require "digest"
 require "securerandom"
 require "value_semantics/monkey_patched"
 
-require_relative "./catapult"
-require_relative "./mn_words"
+require_relative "mn_words"
 
 class SipAccount
 	def self.find(name)
-		CATAPULT.endpoint_find(name).then do |found|
-			next New.new(username: name) unless found
-
-			new(username: found["name"], url: found["url"])
-		end
+		new(BandwidthIris::SipCredential.get(name))
+	rescue BandwidthIris::Errors::GenericError # 404
+		New.new(BandwidthIris::SipCredential.new(
+			user_name: name,
+			realm: CONFIG[:sip][:realm],
+			http_voice_v2_app_id: CONFIG[:sip][:app]
+		))
 	end
 
-	module Common
-		def with_random_password
-			with(password: MN_WORDS.sample(3).join(" "))
-		end
-
-	protected
-
-		def create
-			CATAPULT.create_endpoint(
-				name: username,
-				credentials: { password: password }
-			).then do |url|
-				with(url: url)
-			end
-		end
+	def initialize(api_object, password: nil)
+		@api_object = api_object
+		@password = password
 	end
 
-	include Common
+	def with(password:)
+		self.class.new(@api_object.class.new(@api_object.to_data.merge(
+			hash1: Digest::MD5.hexdigest("#{username}:#{server}:#{password}"),
+			hash1b: Digest::MD5.hexdigest(
+				"#{username}:#{server}:#{server}:#{password}"
+			)
+		)), password: password)
+	end
 
-	value_semantics do
-		url String
-		username String
-		password Either(String, nil), default: nil
+	def with_random_password
+		with(password: MN_WORDS.sample(3).join(" "))
 	end
 
 	def form
@@ -47,56 +41,50 @@ class SipAccount
 		form.instructions = "These are your new SIP credentials"
 
 		form.fields = [
-			{ var: "username", value: username, label: "Username" },
-			{ var: "password", value: password, label: "Password" },
-			{ var: "server", value: server, label: "Server" }
+			{ var: "username", value: username, label: "Username", type: :fixed },
+			{ var: "password", value: @password, label: "Password", type: :fixed },
+			{ var: "server", value: server, label: "Server", type: :fixed }
 		]
 
 		form
 	end
 
 	def put
-		delete.then { create }
+		@api_object.update(
+			hash1: @api_object.hash1,
+			hash1b: @api_object.hash1b,
+			realm: server,
+			http_voice_v2_app_id: @api_object.http_voice_v2_app_id
+		)
+		self
 	end
 
 	def delete
-		CATAPULT.delete(url).then do |http|
-			unless http.response_header.status == 200
-				raise "Delete old SIP account failed"
-			end
-
-			self
-		end
+		@api_object.delete
 	end
 
-protected
-
-	protected :url, :username, :password
+	def username
+		@api_object.user_name.to_s
+	end
 
 	def server
-		CATAPULT.sip_host
+		@api_object.realm
 	end
 
-	class New
-		include Common
-
-		value_semantics do
-			username String
-			password String, default_generator: -> { MN_WORDS.sample(3).join(" ") }
-		end
+	def uri
+		"sip:#{username}@#{server}"
+	end
 
+	class New < SipAccount
 		def put
-			create
-		end
-
-		def with(**kwargs)
-			if kwargs.key?(:url)
-				SipAccount.new(internal_to_h.merge(kwargs))
-			else
-				super
-			end
+			BandwidthIris::SipCredential.create(
+				user_name: username,
+				hash1: @api_object.hash1,
+				hash1b: @api_object.hash1b,
+				realm: server,
+				http_voice_v2_app_id: @api_object.http_voice_v2_app_id
+			)
+			self
 		end
-
-		protected :username, :password
 	end
 end

sgx_jmp.rb 🔗

@@ -475,7 +475,7 @@ Command.new(
 			CONFIG[:creds][:account],
 			body: customer.fwd.create_call_request do |cc|
 				cc.from = customer.registered?.phone
-				cc.application_id = CONFIG[:bandwidth_app]
+				cc.application_id = CONFIG[:sip][:app]
 				cc.answer_url = "#{CONFIG[:web_root]}/ogm/start?" \
 				                "customer_id=#{customer.customer_id}"
 			end
@@ -599,11 +599,33 @@ Command.new(
 
 Command.new(
 	"reset sip account",
-	"Create or Reset SIP Account"
+	"Create or Reset SIP Account",
+	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
 ) {
-	Command.customer.then(&:reset_sip_account).then do |sip_account|
-		Command.finish do |reply|
-			reply.command << sip_account.form
+	Command.customer.then do |customer|
+		sip_account = customer.reset_sip_account
+		Command.reply { |reply|
+			reply.allowed_actions = [:next]
+			form = sip_account.form
+			form.type = :form
+			form.fields += [{
+				type: :boolean, var: "change_fwd",
+				label: "Should inbound calls forward to this SIP account?"
+			}]
+			reply.command << form
+		}.then do |fwd|
+			if ["1", "true"].include?(fwd.form.field("change_fwd")&.value.to_s)
+				# Migrate location if needed
+				BandwidthIris::SipPeer.new(
+					site_id: CONFIG[:bandwidth_site],
+					id: CONFIG[:bandwidth_peer]
+				).move_tns([customer.registered?.phone])
+				customer.set_fwd(sip_account.uri).then do
+					Command.finish("Inbound calls will now forward to SIP.")
+				end
+			else
+				Command.finish
+			end
 		end
 	end
 }.register(self).then(&CommandList.method(:register))

test/test_customer.rb 🔗

@@ -13,14 +13,6 @@ CustomerPlan::DB = Minitest::Mock.new
 CustomerUsage::REDIS = Minitest::Mock.new
 CustomerUsage::DB = Minitest::Mock.new
 
-class SipAccount
-	public :username, :url
-
-	class New
-		public :username
-	end
-end
-
 class CustomerTest < Minitest::Test
 	def test_bill_plan_activate
 		CustomerPlan::DB.expect(:transaction, nil) do |&block|
@@ -152,14 +144,13 @@ class CustomerTest < Minitest::Test
 	def test_sip_account_new
 		req = stub_request(
 			:get,
-			"https://api.catapult.inetwork.com/v1/users/" \
-			"catapult_user/domains/catapult_domain/endpoints?page=0&size=1000"
+			"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/test"
 		).with(
 			headers: {
-				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0"
+				"Authorization" => "Basic Og=="
 			}
 		).to_return(status: 404)
-		sip = customer.sip_account.sync
+		sip = customer.sip_account
 		assert_kind_of SipAccount::New, sip
 		assert_equal "test", sip.username
 		assert_requested req
@@ -169,52 +160,33 @@ class CustomerTest < Minitest::Test
 	def test_sip_account_existing
 		req1 = stub_request(
 			:get,
-			"https://api.catapult.inetwork.com/v1/users/" \
-			"catapult_user/domains/catapult_domain/endpoints?page=0&size=1000"
+			"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/test"
 		).with(
 			headers: {
-				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0"
+				"Authorization" => "Basic Og=="
 			}
-		).to_return(status: 200, body: [
-			{ name: "NOTtest", domainId: "domain", id: "endpoint" }
-		].to_json)
-
-		req2 = stub_request(
-			:get,
-			"https://api.catapult.inetwork.com/v1/users/" \
-			"catapult_user/domains/catapult_domain/endpoints?page=1&size=1000"
-		).with(
-			headers: {
-				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0"
+		).to_return(status: 200, body: {
+			SipCredential: {
+				UserName: "test",
+				Realm: "sip.example.com"
 			}
-		).to_return(status: 200, body: [
-			{ name: "test", domainId: "domain", id: "endpoint" }
-		].to_json)
+		}.to_xml)
 
-		sip = customer.sip_account.sync
+		sip = customer.sip_account
 		assert_kind_of SipAccount, sip
 		assert_equal "test", sip.username
-		assert_equal(
-			"https://api.catapult.inetwork.com/v1/users/" \
-			"catapult_user/domains/domain/endpoints/endpoint",
-			sip.url
-		)
 
 		assert_requested req1
-		assert_requested req2
 	end
 	em :test_sip_account_existing
 
 	def test_sip_account_error
 		stub_request(
 			:get,
-			"https://api.catapult.inetwork.com/v1/users/" \
-			"catapult_user/domains/catapult_domain/endpoints?page=0&size=1000"
-		).to_return(status: 400)
+			"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/test"
+		).to_return(status: 404)
 
-		assert_raises(RuntimeError) do
-			customer.sip_account.sync
-		end
+		assert_equal "test", customer.sip_account.username
 	end
 	em :test_sip_account_error
 

test/test_helper.rb 🔗

@@ -94,6 +94,10 @@ CONFIG = {
 			USD: "merchant_usd"
 		}
 	},
+	sip: {
+		realm: "sip.example.com",
+		app: "sipappid"
+	},
 	credit_card_url: ->(*) { "http://creditcard.example.com" },
 	electrum_notify_url: ->(*) { "http://notify.example.com" },
 	upstream_domain: "example.net"

test/test_registration.rb 🔗

@@ -564,6 +564,11 @@ class RegistrationTest < Minitest::Test
 				}
 			).to_return(status: 201)
 			Registration::Finish::REDIS.expect(
+				:del,
+				nil,
+				["pending_tel_for-test@example.net"]
+			)
+			BackendSgx::REDIS.expect(
 				:set,
 				nil,
 				[
@@ -571,11 +576,6 @@ class RegistrationTest < Minitest::Test
 					"sip:test%40example.net@sip.cheogram.com"
 				]
 			)
-			Registration::Finish::REDIS.expect(
-				:del,
-				nil,
-				["pending_tel_for-test@example.net"]
-			)
 			BackendSgx::REDIS.expect(
 				:set,
 				nil,
@@ -597,7 +597,9 @@ class RegistrationTest < Minitest::Test
 			execute_command(blather: blather) do
 				@sgx.expect(
 					:register!,
-					EMPromise.resolve(OpenStruct.new(error?: false)),
+					EMPromise.resolve(@sgx.with(registered?: IBR.new.tap do |ibr|
+						ibr.phone = "+15555550000"
+					end)),
 					["+15555550000"]
 				)
 

test/test_sip_account.rb 🔗

@@ -4,21 +4,18 @@ require "test_helper"
 require "sip_account"
 
 class SipAccount
-	public :password, :url
-
-	class New
-		public :password
-	end
+	attr_reader :password
 end
 
 class SipAccountTest < Minitest::Test
 	def setup
 		@sip = SipAccount.new(
-			url: "https://api.catapult.inetwork.com/v1/" \
-			     "users/catapult_user/domains/catapult_domain/endpoints/test",
-			username: "12345",
-			password: "old password"
-		)
+			BandwidthIris::SipCredential.new(
+				user_name: "12345",
+				realm: "sip.example.com",
+				http_voice_v2_app_id: "sipappid"
+			)
+		).with(password: "old password")
 	end
 
 	def test_with_random_password
@@ -32,61 +29,52 @@ class SipAccountTest < Minitest::Test
 		form = @sip.form
 		assert_equal "12345", form.field("username").value
 		assert_equal "old password", form.field("password").value
-		assert_equal "host.bwapp.io.example.com", form.field("server").value
+		assert_equal "sip.example.com", form.field("server").value
 	end
 
 	def test_put
-		delete = stub_request(:delete, @sip.url).with(
-			headers: {
-				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0"
-			}
-		).to_return(status: 200)
-
-		post = stub_request(
-			:post,
-			"https://api.catapult.inetwork.com/v1/users/" \
-			"catapult_user/domains/catapult_domain/endpoints"
+		put = stub_request(
+			:put,
+			"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/12345"
 		).with(
-			body: open(__dir__ + "/data/catapult_create_sip.json").read.chomp,
+			body: {
+				Hash1: "73b05bcaf9096438c978aecff5f7cc45",
+				Hash1b: "2b7fe68f6337ef4db29e752684a18db4",
+				Realm: "sip.example.com",
+				HttpVoiceV2AppId: "sipappid"
+			}.to_xml(indent: 0, root: "SipCredential"),
 			headers: {
-				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0",
-				"Content-Type" => "application/json"
+				"Authorization" => "Basic Og=="
 			}
 		).to_return(
 			status: 201,
 			headers: { "Location" => "http://example.com/endpoint" }
 		)
 
-		new_sip = @sip.put.sync
-		assert_equal "http://example.com/endpoint", new_sip.url
-		assert_requested delete
-		assert_requested post
+		new_sip = @sip.put
+		assert_equal "12345", new_sip.username
+		assert_requested put
 	end
 	em :test_put
 
-	def test_put_delete_fail
-		stub_request(:delete, @sip.url).to_return(status: 400)
-		assert_raises(RuntimeError) { @sip.put.sync }
-	end
-	em :test_put_delete_fail
-
-	def test_put_post_fail
-		stub_request(:delete, @sip.url).to_return(status: 200)
+	def test_put_fail
 		stub_request(
-			:post,
-			"https://api.catapult.inetwork.com/v1/users/" \
-			"catapult_user/domains/catapult_domain/endpoints"
+			:put,
+			"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/12345"
 		).to_return(status: 400)
-		assert_raises(RuntimeError) { @sip.put.sync }
+		assert_raises(BandwidthIris::Errors::GenericError) { @sip.put }
 	end
-	em :test_put_post_fail
+	em :test_put_fail
 
 	class NewTest < Minitest::Test
 		def setup
-			@sip = SipAccount::New.new(
-				username: "12345",
-				password: "old password"
-			)
+			@sip = SipAccount.new(
+				BandwidthIris::SipCredential.new(
+					user_name: "12345",
+					realm: "sip.example.com",
+					http_voice_v2_app_id: "sipappid"
+				)
+			).with(password: "old password")
 		end
 
 		def test_with_random_password
@@ -98,22 +86,22 @@ class SipAccountTest < Minitest::Test
 
 		def test_put
 			post = stub_request(
-				:post,
-				"https://api.catapult.inetwork.com/v1/users/" \
-				"catapult_user/domains/catapult_domain/endpoints"
+				:put,
+				"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/12345"
 			).with(
-				body: open(__dir__ + "/data/catapult_create_sip.json").read.chomp,
+				body: {
+					Hash1: "73b05bcaf9096438c978aecff5f7cc45",
+					Hash1b: "2b7fe68f6337ef4db29e752684a18db4",
+					Realm: "sip.example.com",
+					HttpVoiceV2AppId: "sipappid"
+				}.to_xml(indent: 0, root: "SipCredential"),
 				headers: {
-					"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0",
-					"Content-Type" => "application/json"
+					"Authorization" => "Basic Og=="
 				}
-			).to_return(
-				status: 201,
-				headers: { "Location" => "http://example.com/endpoint" }
-			)
+			).to_return(status: 201)
 
-			new_sip = @sip.put.sync
-			assert_equal "http://example.com/endpoint", new_sip.url
+			new_sip = @sip.put
+			assert_equal "12345", new_sip.username
 			assert_requested post
 		end
 		em :test_put