enhance AdminAction::NumberChange, can change sgx

Phillip Davis created

Change summary

forms/admin_number_change.rb       |   9 
lib/admin_actions/number_change.rb |  88 ++
lib/trivial_backend_sgx_repo.rb    |  18 
test/test_admin_command.rb         | 957 +++++++++++++++++++++++++++++--
4 files changed, 989 insertions(+), 83 deletions(-)

Detailed changes

forms/admin_number_change.rb 🔗

@@ -15,3 +15,12 @@ field(
 	label: "Should we delete the old number?",
 	value: 0
 )
+
+field(
+	var: "new_backend",
+	type: "text-single",
+	label: "Backend to change to?",
+	description:
+		"Leave blank to keep current backend, or specify new backend JID",
+	value: ""
+)

lib/admin_actions/number_change.rb 🔗

@@ -3,6 +3,7 @@
 require "value_semantics/monkey_patched"
 require_relative "../admin_action"
 require_relative "../form_to_h"
+require_relative "../utils"
 
 class AdminAction
 	class NumberChange < AdminAction
@@ -11,14 +12,17 @@ class AdminAction
 			using FormToH
 
 			def self.for(target_customer, reply:)
+				unless (registration = target_customer.registered?)
+					return EMPromise.reject("Customer not registered")
+				end
+
 				EMPromise.resolve(
 					new(
 						customer_id: target_customer.customer_id,
-						old_tel: target_customer.registered?.phone
+						old_backend: target_customer.sgx.strip!.to_s,
+						old_tel: registration.phone
 					)
-				).then { |x|
-					reply.call(x.form).then(&x.method(:change))
-				}
+				).then { |x| reply.call(x.form).then(&x.method(:change)) }
 			end
 
 			def initialize(**bag)
@@ -33,7 +37,12 @@ class AdminAction
 				AdminAction::NumberChange.for(
 					**@bag,
 					**result.form.to_h
-						.reject { |_k, v| v == "nil" }.transform_keys(&:to_sym)
+						.reject { |_k, v| v == "nil" || v.to_s.empty? }
+						.tap { |form_h|
+							form_h["new_backend"] ||= @bag[:old_backend]
+							form_h["new_tel"] ||= @bag[:old_tel]
+						}
+						.transform_keys(&:to_sym)
 				)
 			end
 		end
@@ -44,6 +53,12 @@ class AdminAction
 			end
 		}
 
+		UnknownBackend = Struct.new(:backend, :expected) {
+			def to_s
+				"Got unknown backend: #{backend}, expected one of #{expected}"
+			end
+		}
+
 		def customer_id
 			@attributes[:customer_id]
 		end
@@ -56,36 +71,50 @@ class AdminAction
 			@attributes[:new_tel]
 		end
 
+		def old_backend
+			@attributes[:old_backend]
+		end
+
+		def new_backend
+			@attributes[:new_backend]
+		end
+
 		def should_delete?
 			["1", "true"].include?(@attributes[:should_delete])
 		end
 
 		def check_forward
-			EMPromise.all([
-				check_noop,
-				check_exist
-			])
+			EMPromise.all([check_noop, check_cat_jid, check_backend])
 		end
 
 		def forward
-			TrivialBackendSgxRepo.new(redis: REDIS).get(customer_id).then do |sgx|
-				EMPromise.all([
-					REDIS.rename("catapult_fwd-#{old_tel}", "catapult_fwd-#{new_tel}"),
-					sgx.register!(new_tel),
-					should_delete? && first_time? ? disconnect_number : nil
-				]).then { self }
-			end
+			EMPromise.all([
+				new_tel != old_tel && change_catapult_fwd!,
+				TrivialBackendSgxRepo.new(
+					redis: REDIS
+				).put(customer_id, new_backend, new_tel),
+				should_disconnect_old_number? && disconnect_number
+			]).then { self }
+		end
+
+		def change_catapult_fwd!
+			REDIS.rename("catapult_fwd-#{old_tel}", "catapult_fwd-#{new_tel}")
 		end
 
 		def to_reverse
 			with(
 				old_tel: new_tel,
-				new_tel: old_tel
+				new_tel: old_tel,
+				old_backend: new_backend,
+				new_backend: old_backend
 			)
 		end
 
 		def to_s
-			"number_change(#{customer_id}): #{old_tel} -> #{new_tel}#{delete_warning}"
+			base = "Change Number\n"
+			base += "	[move backend?]: #{old_backend} -> #{new_backend}\n"
+			base += "	[change number?]: #{old_tel} -> #{new_tel}"
+			base + delete_warning
 		end
 
 	protected
@@ -107,14 +136,33 @@ class AdminAction
 		end
 
 		def check_noop
-			EMPromise.reject(NoOp.new) if new_tel == old_tel
+			if new_tel == old_tel && new_backend == old_backend
+				EMPromise.reject(NoOp.new)
+			else
+				EMPromise.resolve(nil)
+			end
 		end
 
-		def check_exist
+		def check_cat_jid
 			cat_jid = "catapult_jid-#{old_tel}"
 			REDIS.exists(cat_jid).then { |v|
 				EMPromise.reject(NilKey.new(cat_jid)) unless v == 1
 			}
 		end
+
+		def check_backend
+			unless (expected = CONFIG[:sgx_creds].keys.map(
+				&:to_s
+			).push(CONFIG[:sgx])).include?(new_backend)
+				EMPromise.reject(UnknownBackend.new(new_backend, expected))
+			end
+		end
+
+		def should_disconnect_old_number?
+			should_delete? &&
+				first_time? &&
+				old_tel != new_tel &&
+				old_backend == CONFIG[:sgx]
+		end
 	end
 end

lib/trivial_backend_sgx_repo.rb 🔗

@@ -34,8 +34,26 @@ class TrivialBackendSgxRepo
 		end
 	end
 
+	def put(cid, target_sgx, target_num)
+		get(cid).then(&:deregister!).then {
+			put_jid(cid, target_sgx)
+		}.then {
+			get(cid)
+		}.then { |sgx|
+			sgx.register!(target_num)
+		}
+	end
+
 protected
 
+	def put_jid(cid, target_sgx)
+		if target_sgx == CONFIG[:sgx]
+			@redis.del("jmp_customer_backend_sgx-#{cid}")
+		else
+			@redis.set("jmp_customer_backend_sgx-#{cid}", target_sgx)
+		end
+	end
+
 	def default_jid_creds
 		EMPromise.resolve([@jid, @creds])
 	end

test/test_admin_command.rb 🔗

@@ -24,6 +24,85 @@ class AdminCommandTest < Minitest::Test
 		assert iq.note_text = note_text if note_text
 	end
 
+	def assert_change_number_form(iq)
+		assert_equal(
+			iq.form.field("new_tel")&.type,
+			"text-single"
+		)
+		assert_equal(
+			iq.form.field("new_backend")&.type,
+			"text-single"
+		)
+		assert_equal(
+			iq.form.field("should_delete")&.type,
+			"boolean"
+		)
+	end
+
+	def change_number_form(
+		new_tel: nil,
+		new_backend: nil,
+		should_delete: false,
+		from: "test@example.com"
+	)
+		iq = Blather::Stanza::Iq::Command.new
+		iq.form.fields = [
+			{ var: "new_tel", value: new_tel },
+			{ var: "should_delete", value: should_delete.to_s },
+			{ var: "new_backend", value: new_backend }
+		]
+		iq.from = from
+		iq
+	end
+
+	def setup_bandwidth_tn_repo_mock(
+		should_disconnect: false,
+		old_tel: nil,
+		new_tel: nil
+	)
+		mock_repo = Minitest::Mock.new
+		if should_disconnect
+			expected_order_name = "cust test swap to #{new_tel}"
+			mock_repo.expect(
+				:disconnect,
+				EMPromise.resolve(nil),
+				[old_tel, expected_order_name]
+			)
+		end
+		mock_repo
+	end
+
+	def assert_ibr_register_form(
+		iq,
+		tel,
+		nick: "test_bw_account",
+		username: "test_bw_user",
+		password: "test_bw_password",
+		from: "customer_test@component",
+		target_backend_sgx: "sgx"
+	)
+		assert iq.is_a?(Blather::Stanza::Iq::IBR)
+		assert_equal iq.type, :set
+		assert_equal iq.nick, nick
+		assert_equal iq.username, username
+		assert_equal iq.password, password
+		assert_equal iq.phone, tel
+		assert_equal iq.from, from
+		assert_equal iq.to, target_backend_sgx
+	end
+
+	def assert_ibr_deregister_form(
+		iq,
+		from: "customer_test@component",
+		target_backend_sgx: "sgx"
+	)
+		assert iq.is_a?(Blather::Stanza::Iq::IBR)
+		assert_equal iq.type, :set
+		assert_equal iq.to, target_backend_sgx
+		assert_equal iq.from, from
+		assert iq.remove?
+	end
+
 	def admin_command(tel="+15556667777", registered: OpenStruct.new(phone: tel))
 		sgx = Minitest::Mock.new(OpenStruct.new(
 			registered?: registered
@@ -352,24 +431,42 @@ class AdminCommandTest < Minitest::Test
 	end
 	em :test_action_cancel_account_keep_number
 
-	def test_change_num_default_sgx
-		# Same as default given by `sgx.registered?` returned by `admin_command`
-		old_tel = "+15556667777"
-		new_tel = "+12222222222"
+	def	test_change_num_for_unregistered_customer
+		execute_command { |exe|
+			sgx = Minitest::Mock.new(OpenStruct.new(
+				registered?: nil,
+				jid: Blather::JID.new(CONFIG[:sgx]) # Uses Bandwidth backend
+			))
+			target_customer = customer(sgx: sgx)
+			admin = AdminCommand.for(target_customer, exe.customer_repo)
 
-		admin_number_change_form = Blather::Stanza::Iq::Command.new
-		admin_number_change_form.form.fields = [
-			{ var: "new_tel", value: new_tel },
-			{ var: "should_delete", value: false }
-		]
-		admin_number_change_form.from = "test@example.com"
+			error = assert_raises(
+				"number change for unregistered customer should raise"
+			) {
+				admin.action_number_change.sync
+			}
+
+			assert_equal error.to_s, "Customer not registered"
+		}
+	end
+	em :test_change_num_for_unregistered_customer
+
+	def test_change_num_same_num_same_backend
+		# Same as default given by `sgx.registered?` returned by `admin_command`
+		tel = "+15556667777"
 
 		execute_command { |exe|
-			sgx, admin = admin_command
+			sgx = Minitest::Mock.new(OpenStruct.new(
+				registered?: OpenStruct.new(phone: tel),
+				jid: Blather::JID.new(CONFIG[:sgx]) # Uses Bandwidth backend
+			))
+
+			target_customer = customer(sgx: sgx)
+			admin = AdminCommand.for(target_customer, exe.customer_repo)
 
 			exe.customer_repo.expect(
 				:find_by_jid,
-				EMPromise.resolve(customer(sgx: sgx)),
+				EMPromise.resolve(target_customer),
 				[Matching.new do |jid|
 					assert jid.is_a? Blather::JID
 					assert_equal jid.stripped.to_s, "test@example.com"
@@ -379,80 +476,814 @@ class AdminCommandTest < Minitest::Test
 			AdminAction::NumberChange::REDIS.expect(
 				:exists,
 				EMPromise.resolve(1),
-				["catapult_jid-#{old_tel}"]
+				["catapult_jid-#{tel}"]
 			)
 
-			AdminAction::NumberChange::REDIS.expect(
-				:get,
-				EMPromise.resolve(nil), # Default SGX creds
-				["jmp_customer_backend_sgx-test"]
+			Command::COMMAND_MANAGER.expect(
+				:write,
+				EMPromise.resolve(change_number_form(
+					new_tel: tel,
+					new_backend: "sgx"
+				)),
+				[Matching.new do |iq|
+					assert_undoable_form(iq)
+					assert_change_number_form(iq)
+				end]
 			)
 
-			AdminAction::NumberChange::REDIS.expect(
-				:rename,
-				EMPromise.resolve(nil), # Default SGX creds
-				["catapult_fwd-#{old_tel}", "catapult_fwd-#{new_tel}"]
-			)
+			error = admin.action_number_change
+				.catch_only(AdminAction::NoOp) { |e| e }.sync
 
-			BackendSgx::REDIS.expect(
-				:set,
-				EMPromise.resolve(nil), # Default SGX creds
-				["catapult_jid-#{new_tel}", "customer_test@component"]
-			)
+			assert error.is_a?(AdminAction::NoOp)
+			assert_mock Command::COMMAND_MANAGER
+			assert_mock exe.customer_repo
+			assert_mock AdminAction::NumberChange::REDIS
+			assert_mock sgx
+		}
+	end
+	em :test_change_num_same_num_same_backend
 
-			expected_xadd_args = {
-				customer_id: "test",
-				old_tel: old_tel,
-				new_tel: new_tel,
-				should_delete: nil,
-				actor_id: "test",
-				class: "NumberChange",
-				direction: :forward
-			}
+	def test_change_num_blank_num_blank_backend
+		# Same as default given by `sgx.registered?` returned by `admin_command`
+		tel = "+15556667777"
 
-			AdminActionRepo::REDIS.expect(
-				:xadd,
-				EMPromise.resolve(nil)
-			) do |admin_actions, star, **xadd_args|
-				assert_equal admin_actions, "admin_actions"
-				assert_equal star, "*"
+		execute_command { |exe|
+			sgx = Minitest::Mock.new(OpenStruct.new(
+				registered?: OpenStruct.new(phone: tel),
+				jid: Blather::JID.new(CONFIG[:sgx]) # Uses Bandwidth backend
+			))
 
-				xadd_args.each do |k, v|
-					assert_equal v, expected_xadd_args[k]
-				end
-			end
+			target_customer = customer(sgx: sgx)
 
-			# Make sure the IBR record was sent off for
-			# the new number.
-			BackendSgx::IQ_MANAGER.expect(
-				:write,
-				EMPromise.resolve(nil),
-				[Matching.new do |iq|
-					assert_equal iq.nick, "test_bw_account"
-					assert_equal iq.username, "test_bw_user"
-					assert_equal iq.password, "test_bw_password"
-					assert_equal iq.phone, new_tel
+			admin = AdminCommand.for(target_customer, exe.customer_repo)
+
+			exe.customer_repo.expect(
+				:find_by_jid,
+				EMPromise.resolve(target_customer),
+				[Matching.new do |jid|
+					assert jid.is_a? Blather::JID
+					assert_equal jid.stripped.to_s, "test@example.com"
 				end]
 			)
 
+			AdminAction::NumberChange::REDIS.expect(
+				:exists,
+				EMPromise.resolve(1),
+				["catapult_jid-#{tel}"]
+			)
+
 			Command::COMMAND_MANAGER.expect(
 				:write,
-				EMPromise.resolve(admin_number_change_form),
+				EMPromise.resolve(change_number_form),
 				[Matching.new do |iq|
 					assert_undoable_form(iq)
-					assert iq.form.field("new_tel")
-					assert iq.form.field("should_delete")
+					assert_change_number_form(iq)
 				end]
 			)
 
-			admin.action_number_change.sync
+			error = admin.action_number_change
+				.catch_only(AdminAction::NoOp) { |e| e }.sync
 
+			assert error.is_a?(AdminAction::NoOp)
 			assert_mock Command::COMMAND_MANAGER
 			assert_mock exe.customer_repo
 			assert_mock AdminAction::NumberChange::REDIS
-			assert_mock AdminActionRepo::REDIS
-			assert_mock BackendSgx::IQ_MANAGER
+			assert_mock sgx
 		}
 	end
-	em :test_change_num_default_sgx
+	em :test_change_num_blank_num_blank_backend
+
+	def test_change_num_different_num_same_backend
+		old_tel = "+15556667777"
+		new_tel = "+12222222222"
+		bandwidth_tn_repo_mock = setup_bandwidth_tn_repo_mock(
+			should_disconnect: false
+		)
+
+		BandwidthTnRepo.stub :new, bandwidth_tn_repo_mock do
+			execute_command { |exe|
+				sgx = Minitest::Mock.new(OpenStruct.new(
+					registered?: OpenStruct.new(phone: old_tel),
+					jid: Blather::JID.new(CONFIG[:sgx])
+				))
+
+				target_customer = customer(sgx: sgx)
+				admin = AdminCommand.for(target_customer, exe.customer_repo)
+
+				exe.customer_repo.expect(
+					:find_by_jid,
+					EMPromise.resolve(target_customer),
+					[Matching.new do |jid|
+						assert jid.is_a? Blather::JID
+						assert_equal jid.stripped.to_s, "test@example.com"
+					end]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:rename,
+					EMPromise.resolve(nil),
+					["catapult_fwd-#{old_tel}", "catapult_fwd-#{new_tel}"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:get,
+					EMPromise.resolve(nil),
+					["jmp_customer_backend_sgx-test"]
+				)
+				AdminAction::NumberChange::REDIS.expect(
+					:get,
+					EMPromise.resolve(nil),
+					["jmp_customer_backend_sgx-test"]
+				)
+
+				BackendSgx::IQ_MANAGER.expect(
+					:write,
+					EMPromise.resolve(nil)
+				) do |iq|
+					assert_ibr_deregister_form(iq)
+				end
+
+				BackendSgx::IQ_MANAGER.expect(
+					:write,
+					EMPromise.resolve(nil)
+				) do |iq|
+					assert_ibr_register_form(
+						iq,
+						"+12222222222"
+					)
+				end
+
+				AdminAction::NumberChange::REDIS.expect(
+					:del,
+					EMPromise.resolve(nil),
+					["jmp_customer_backend_sgx-#{target_customer.customer_id}"]
+				)
+
+				BackendSgx::REDIS.expect(
+					:set,
+					EMPromise.resolve(nil),
+					["catapult_jid-#{new_tel}", "customer_test@component"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:exists,
+					EMPromise.resolve(1),
+					["catapult_jid-#{old_tel}"]
+				)
+
+				Command::COMMAND_MANAGER.expect(
+					:write,
+					EMPromise.resolve(change_number_form(
+						new_tel: "+12222222222"
+					))
+				) do |iq|
+					assert_undoable_form(iq)
+					assert_change_number_form(iq)
+				end
+
+				expected_xadd_args = {
+					customer_id: "test",
+					old_tel: old_tel,
+					new_tel: new_tel,
+					should_delete: nil,
+					actor_id: "test",
+					class: "NumberChange",
+					direction: :forward
+				}.freeze
+
+				AdminActionRepo::REDIS.expect(
+					:xadd,
+					EMPromise.resolve(nil)
+				) do |admin_actions, star, **xadd_args|
+					assert_equal admin_actions, "admin_actions"
+					assert_equal star, "*"
+
+					xadd_args.each do |k, v|
+						assert_equal v, expected_xadd_args[k]
+					end
+				end
+
+				result = admin.action_number_change.sync
+
+				slug, backend_report, number_report = result.lines
+
+				assert_equal slug.strip, "Action : Change Number"
+				assert_equal(
+					backend_report.strip,
+					"[move backend?]: sgx -> sgx"
+				)
+				assert_equal(
+					number_report.strip,
+					"[change number?]: +15556667777 -> +12222222222"
+				)
+
+				assert_mock Command::COMMAND_MANAGER
+				assert_mock exe.customer_repo
+				assert_mock AdminAction::NumberChange::REDIS
+				assert_mock AdminActionRepo::REDIS
+				assert_mock BackendSgx::REDIS
+				assert_mock BackendSgx::IQ_MANAGER
+				assert_mock bandwidth_tn_repo_mock
+				assert_mock sgx
+			}
+		end
+	end
+	em :test_change_num_different_num_same_backend
+
+	def test_change_num_same_num_different_backend
+		target_backend_sgx = "route_value"
+
+		bandwidth_tn_repo_mock = setup_bandwidth_tn_repo_mock(
+			should_disconnect: false
+		)
+
+		BandwidthTnRepo.stub :new, bandwidth_tn_repo_mock do
+			execute_command { |exe|
+				sgx = Minitest::Mock.new(OpenStruct.new(
+					registered?: OpenStruct.new(phone: "+15556667777"),
+					jid: Blather::JID.new(CONFIG[:sgx])
+				))
+
+				target_customer = customer(sgx: sgx)
+				admin = AdminCommand.for(target_customer, exe.customer_repo)
+
+				exe.customer_repo.expect(
+					:find_by_jid,
+					EMPromise.resolve(target_customer),
+					[Matching.new do |jid|
+						assert jid.is_a? Blather::JID
+						assert_equal jid.stripped.to_s, "test@example.com"
+					end]
+				)
+
+				Command::COMMAND_MANAGER.expect(
+					:write,
+					EMPromise.resolve(
+						change_number_form(
+							new_backend: target_backend_sgx
+						)
+					),
+					[Matching.new do |iq|
+						assert_undoable_form(iq)
+						assert_change_number_form(iq)
+					end]
+				)
+
+				BackendSgx::IQ_MANAGER.expect(
+					:write,
+					EMPromise.resolve(nil)
+				) do |iq|
+					assert_ibr_deregister_form(iq)
+				end
+
+				BackendSgx::IQ_MANAGER.expect(
+					:write,
+					EMPromise.resolve(nil)
+				) do |iq|
+					assert_ibr_register_form(
+						iq,
+						"+15556667777",
+						target_backend_sgx: target_backend_sgx,
+						nick: "test_sgx_account",
+						username: "test_sgx_user",
+						password: "test_sgx_password"
+					)
+				end
+
+				AdminAction::NumberChange::REDIS.expect(
+					:get,
+					EMPromise.resolve(nil),
+					["jmp_customer_backend_sgx-#{target_customer.customer_id}"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:set,
+					EMPromise.resolve(nil),
+					[
+						"jmp_customer_backend_sgx-#{target_customer.customer_id}",
+						"route_value"
+					]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:get,
+					EMPromise.resolve("route_value"),
+					["jmp_customer_backend_sgx-#{target_customer.customer_id}"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:exists,
+					EMPromise.resolve(1),
+					["catapult_jid-+15556667777"]
+				)
+
+				BackendSgx::REDIS.expect(
+					:set,
+					EMPromise.resolve(nil),
+					[
+						"catapult_jid-+15556667777",
+						"customer_test@component"
+					]
+				)
+
+				expected_xadd_args = {
+					old_tel: "+15556667777",
+					new_tel: "+15556667777",
+					old_backend: "sgx",
+					new_backend: "route_value",
+					should_delete: nil,
+					actor_id: "test",
+					class: "NumberChange",
+					direction: :forward
+				}.freeze
+
+				AdminActionRepo::REDIS.expect(
+					:xadd,
+					EMPromise.resolve(nil)
+				) do |admin_actions, star, **xadd_args|
+					assert_equal admin_actions, "admin_actions"
+					assert_equal star, "*"
+
+					xadd_args.each do |k, v|
+						assert_equal v, expected_xadd_args[k]
+					end
+				end
+
+				result = admin.action_number_change.sync
+
+				slug, backend_report, number_report = result.lines
+
+				assert_equal slug.strip, "Action : Change Number"
+				assert_equal(
+					backend_report.strip,
+					"[move backend?]: sgx -> route_value"
+				)
+				assert_equal(
+					number_report.strip,
+					"[change number?]: +15556667777 -> +15556667777"
+				)
+
+				assert_mock Command::COMMAND_MANAGER
+				assert_mock exe.customer_repo
+				assert_mock AdminAction::NumberChange::REDIS
+				assert_mock AdminActionRepo::REDIS
+				assert_mock BackendSgx::REDIS
+				assert_mock BackendSgx::IQ_MANAGER
+				assert_mock bandwidth_tn_repo_mock
+				assert_mock sgx
+			}
+		end
+	end
+	em :test_change_num_same_num_different_backend
+
+	def test_change_num_different_num_different_backend
+		old_tel = "+15556667777"
+		new_tel = "+12222222222"
+		target_backend_sgx = "route_value"
+
+		bandwidth_tn_repo_mock = setup_bandwidth_tn_repo_mock(
+			should_disconnect: false
+		)
+
+		BandwidthTnRepo.stub :new, bandwidth_tn_repo_mock do
+			execute_command { |exe|
+				sgx = Minitest::Mock.new(OpenStruct.new(
+					registered?: OpenStruct.new(phone: old_tel),
+					jid: Blather::JID.new(CONFIG[:sgx])
+				))
+
+				target_customer = customer(sgx: sgx)
+				admin = AdminCommand.for(target_customer, exe.customer_repo)
+
+				exe.customer_repo.expect(
+					:find_by_jid,
+					EMPromise.resolve(target_customer),
+					[Matching.new do |jid|
+						assert jid.is_a? Blather::JID
+						assert_equal jid.stripped.to_s, "test@example.com"
+					end]
+				)
+
+				Command::COMMAND_MANAGER.expect(
+					:write,
+					EMPromise.resolve(
+						change_number_form(
+							new_tel: new_tel,
+							new_backend: target_backend_sgx
+						)
+					),
+					[Matching.new do |iq|
+						assert_undoable_form(iq)
+						assert_change_number_form(iq)
+					end]
+				)
+
+				BackendSgx::IQ_MANAGER.expect(
+					:write,
+					EMPromise.resolve(nil)
+				) do |iq|
+					assert_ibr_deregister_form(iq)
+				end
+
+				BackendSgx::IQ_MANAGER.expect(
+					:write,
+					EMPromise.resolve(nil)
+				) do |iq|
+					assert_ibr_register_form(
+						iq,
+						"+12222222222",
+						target_backend_sgx: target_backend_sgx,
+						nick: "test_sgx_account",
+						username: "test_sgx_user",
+						password: "test_sgx_password"
+					)
+				end
+
+				AdminAction::NumberChange::REDIS.expect(
+					:exists,
+					EMPromise.resolve(1),
+					["catapult_jid-#{old_tel}"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:rename,
+					EMPromise.resolve(nil),
+					["catapult_fwd-#{old_tel}", "catapult_fwd-#{new_tel}"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:get,
+					EMPromise.resolve(nil),
+					["jmp_customer_backend_sgx-test"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:set,
+					EMPromise.resolve(nil),
+					[
+						"jmp_customer_backend_sgx-#{target_customer.customer_id}",
+						"route_value"
+					]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:get,
+					EMPromise.resolve("route_value"),
+					["jmp_customer_backend_sgx-test"]
+				)
+
+				BackendSgx::REDIS.expect(
+					:set,
+					EMPromise.resolve(nil),
+					[
+						"catapult_jid-#{new_tel}",
+						"customer_test@component"
+					]
+				)
+
+				expected_xadd_args = {
+					old_tel: "+15556667777",
+					new_tel: "+12222222222",
+					target_backend_sgx: "route_value",
+					should_delete: nil,
+					actor_id: "test",
+					class: "NumberChange",
+					direction: :forward
+				}.freeze
+
+				AdminActionRepo::REDIS.expect(
+					:xadd,
+					EMPromise.resolve(nil)
+				) do |admin_actions, star, **xadd_args|
+					assert_equal admin_actions, "admin_actions"
+					assert_equal star, "*"
+
+					xadd_args.each do |k, v|
+						assert_equal v, expected_xadd_args[k]
+					end
+				end
+
+				result = admin.action_number_change.sync
+
+				slug, backend_report, number_report = result.lines
+
+				assert_equal slug.strip, "Action : Change Number"
+				assert_equal(
+					backend_report.strip,
+					"[move backend?]: sgx -> route_value"
+				)
+				assert_equal(
+					number_report.strip,
+					"[change number?]: +15556667777 -> +12222222222"
+				)
+
+				assert_mock Command::COMMAND_MANAGER
+				assert_mock exe.customer_repo
+				assert_mock AdminAction::NumberChange::REDIS
+				assert_mock AdminActionRepo::REDIS
+				assert_mock BackendSgx::REDIS
+				assert_mock BackendSgx::IQ_MANAGER
+				assert_mock bandwidth_tn_repo_mock
+				assert_mock sgx
+			}
+		end
+	end
+	em :test_change_num_different_num_different_backend
+
+	def test_change_num_bandwidth_backend_with_delete_should_disconnect
+		old_tel = "+15556667777"
+		new_tel = "+12222222222"
+
+		bandwidth_tn_repo_mock = setup_bandwidth_tn_repo_mock(
+			should_disconnect: true,
+			old_tel: old_tel,
+			new_tel: new_tel
+		)
+
+		BandwidthTnRepo.stub :new, bandwidth_tn_repo_mock do
+			execute_command { |exe|
+				sgx = Minitest::Mock.new(OpenStruct.new(
+					registered?: OpenStruct.new(phone: old_tel),
+					jid: Blather::JID.new(CONFIG[:sgx])
+				))
+
+				target_customer = customer(sgx: sgx)
+				admin = AdminCommand.for(target_customer, exe.customer_repo)
+
+				exe.customer_repo.expect(
+					:find_by_jid,
+					EMPromise.resolve(customer(sgx: sgx)),
+					[Matching.new do |jid|
+						assert jid.is_a? Blather::JID
+						assert_equal jid.stripped.to_s, "test@example.com"
+					end]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:exists,
+					EMPromise.resolve(1),
+					["catapult_jid-#{old_tel}"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:rename,
+					EMPromise.resolve(nil),
+					["catapult_fwd-#{old_tel}", "catapult_fwd-#{new_tel}"]
+				)
+
+				expected_xadd_args = {
+					customer_id: "test",
+					old_tel: old_tel,
+					new_tel: new_tel,
+					should_delete: "true",
+					actor_id: "test",
+					class: "NumberChange",
+					direction: :forward
+				}.freeze
+
+				AdminActionRepo::REDIS.expect(
+					:xadd,
+					EMPromise.resolve(nil)
+				) do |admin_actions, star, **xadd_args|
+					assert_equal admin_actions, "admin_actions"
+					assert_equal star, "*"
+
+					xadd_args.each do |k, v|
+						assert_equal v, expected_xadd_args[k]
+					end
+				end
+
+				Command::COMMAND_MANAGER.expect(
+					:write,
+					EMPromise.resolve(
+						change_number_form(
+							new_tel: new_tel,
+							should_delete: true
+						)
+					),
+					[Matching.new do |iq|
+						assert_undoable_form(iq)
+						assert_change_number_form(iq)
+					end]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:get,
+					EMPromise.resolve(nil),
+					["jmp_customer_backend_sgx-test"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:del,
+					EMPromise.resolve(nil),
+					["jmp_customer_backend_sgx-test"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:get,
+					EMPromise.resolve(nil),
+					["jmp_customer_backend_sgx-test"]
+				)
+
+				BackendSgx::REDIS.expect(
+					:set,
+					EMPromise.resolve(nil),
+					[
+						"catapult_jid-#{new_tel}",
+						"customer_test@component"
+					]
+				)
+
+				BackendSgx::IQ_MANAGER.expect(
+					:write,
+					EMPromise.resolve(nil)
+				) do |iq|
+					assert_ibr_deregister_form(iq)
+				end
+
+				BackendSgx::IQ_MANAGER.expect(
+					:write,
+					EMPromise.resolve(nil)
+				) do |iq|
+					assert_ibr_register_form(
+						iq,
+						new_tel,
+						nick: "test_bw_account",
+						username: "test_bw_user",
+						password: "test_bw_password"
+					)
+				end
+
+				result = admin.action_number_change.sync
+
+				slug, backend_report, number_report = result.lines
+
+				assert_equal slug.strip, "Action : Change Number"
+				assert_equal(
+					backend_report.strip,
+					"[move backend?]: sgx -> sgx"
+				)
+				assert_equal(
+					number_report.strip,
+					"[change number?]: #{old_tel} -> #{new_tel}"
+				)
+
+				assert_mock Command::COMMAND_MANAGER
+				assert_mock exe.customer_repo
+				assert_mock AdminAction::NumberChange::REDIS
+				assert_mock AdminActionRepo::REDIS
+				assert_mock BackendSgx::REDIS
+				assert_mock BackendSgx::IQ_MANAGER
+				assert_mock bandwidth_tn_repo_mock
+				assert_mock sgx
+			}
+		end
+	end
+	em :test_change_num_bandwidth_backend_with_delete_should_disconnect
+
+	def test_change_num_non_bw_backend_should_not_disconnect
+		old_tel = "+15556667777"
+		new_tel = "+12222222222"
+		non_bw_backend = "route_value"
+
+		bandwidth_tn_repo_mock = setup_bandwidth_tn_repo_mock(
+			should_disconnect: false
+		)
+
+		BandwidthTnRepo.stub :new, bandwidth_tn_repo_mock do
+			execute_command { |exe|
+				Command::COMMAND_MANAGER.expect(
+					:write,
+					EMPromise.resolve(change_number_form(new_tel: new_tel)),
+					[Matching.new do |iq|
+						assert_undoable_form(iq)
+						assert_change_number_form(iq)
+					end]
+				)
+
+				non_bandwidth_sgx = Minitest::Mock.new(OpenStruct.new(
+					registered?: OpenStruct.new(phone: old_tel),
+					jid: Blather::JID.new(non_bw_backend)
+				))
+
+				target_customer = customer(sgx: non_bandwidth_sgx)
+				admin = AdminCommand.for(target_customer, exe.customer_repo)
+
+				exe.customer_repo.expect(
+					:find_by_jid,
+					EMPromise.resolve(target_customer),
+					[Blather::JID.new("test@example.com")]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:exists,
+					EMPromise.resolve(1),
+					["catapult_jid-+15556667777"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:rename,
+					EMPromise.resolve(nil),
+					["catapult_fwd-#{old_tel}", "catapult_fwd-#{new_tel}"]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:get,
+					EMPromise.resolve("route_value"),
+					["jmp_customer_backend_sgx-#{target_customer.customer_id}"]
+				)
+
+				BackendSgx::IQ_MANAGER.expect(
+					:write,
+					EMPromise.resolve(nil)
+				) do |iq|
+					assert_ibr_deregister_form(
+						iq,
+						target_backend_sgx: "route_value"
+					)
+				end
+
+				AdminAction::NumberChange::REDIS.expect(
+					:set,
+					EMPromise.resolve(nil),
+					[
+						"jmp_customer_backend_sgx-#{target_customer.customer_id}",
+						"route_value"
+					]
+				)
+
+				AdminAction::NumberChange::REDIS.expect(
+					:get,
+					EMPromise.resolve("route_value"),
+					["jmp_customer_backend_sgx-#{target_customer.customer_id}"]
+				)
+
+				BackendSgx::IQ_MANAGER.expect(
+					:write,
+					EMPromise.resolve(nil)
+				) do |iq|
+					assert_ibr_register_form(
+						iq,
+						"+12222222222",
+						nick: "test_sgx_account",
+						username: "test_sgx_user",
+						password: "test_sgx_password",
+						target_backend_sgx: "route_value"
+					)
+				end
+
+				BackendSgx::REDIS.expect(
+					:set,
+					EMPromise.resolve(nil),
+					["catapult_jid-+12222222222", "customer_test@component"]
+				)
+
+				expected_xadd_args = {
+					customer_id: "test",
+					old_tel: old_tel,
+					new_tel: new_tel,
+					should_delete: "true",
+					actor_id: "test",
+					class: "NumberChange",
+					direction: :forward
+				}.freeze
+
+				AdminActionRepo::REDIS.expect(
+					:xadd,
+					EMPromise.resolve(nil)
+				) do |admin_actions, star, **xadd_args|
+					assert_equal admin_actions, "admin_actions"
+					assert_equal star, "*"
+
+					xadd_args.each do |k, v|
+						assert_equal v, expected_xadd_args[k]
+					end
+				end
+
+				result = admin.action_number_change.sync
+
+				slug, backend_report, number_report = result.lines
+
+				assert_equal slug.strip, "Action : Change Number"
+				assert_equal(
+					backend_report.strip,
+					"[move backend?]: #{non_bw_backend} -> #{non_bw_backend}"
+				)
+				assert_equal(
+					number_report.strip,
+					"[change number?]: +15556667777 -> +12222222222"
+				)
+
+				assert_mock Command::COMMAND_MANAGER
+				assert_mock exe.customer_repo
+				assert_mock AdminAction::NumberChange::REDIS
+				assert_mock AdminActionRepo::REDIS
+				assert_mock BackendSgx::REDIS
+				assert_mock BackendSgx::IQ_MANAGER
+				assert_mock bandwidth_tn_repo_mock
+				assert_mock non_bandwidth_sgx
+			}
+		end
+	end
+	em :test_change_num_non_bw_backend_should_not_disconnect
 end