Detailed changes
@@ -4,7 +4,7 @@ source "https://rubygems.org"
gem "amazing_print"
gem "bandwidth-sdk", "<= 6.1.0"
-gem "blather", git: "https://github.com/adhearsion/blather", branch: "develop"
+gem "blather", git: "https://github.com/psycotica0/blather", branch: "cv_new_id"
gem "braintree"
gem "dhall", ">= 0.5.3.fixed"
gem "em-hiredis"
@@ -20,7 +20,7 @@ gem "multihashes"
gem "ougai"
gem "relative_time"
gem "roda"
-gem "ruby-bandwidth-iris", git: "https://github.com/singpolyma/ruby-bandwidth-iris", branch: "tn_move"
+gem "ruby-bandwidth-iris"
gem "sentry-ruby", "<= 4.3.1"
gem "slim"
gem "statsd-instrument", git: "https://github.com/singpolyma/statsd-instrument.git", branch: "graphite"
@@ -0,0 +1,270 @@
+#!/usr/bin/ruby
+# frozen_string_literal: true
+
+require "date"
+require "dhall"
+require "em-hiredis"
+require "em-http"
+require "em_promise"
+require "json"
+require "optparse"
+require "ruby-bandwidth-iris"
+require "securerandom"
+require "sentry-ruby"
+require "time"
+
+@verbosity = 0
+@real_data = true
+@dry_run = false
+
+OptionParser.new do |opts|
+ opts.banner = "Usage: porting [-vvf] DHALL_CONFIG"
+
+ opts.on(
+ "-v", "--verbose",
+ "Print to terminal, run twice to not even send to customer"
+ ) do
+ @verbosity += 1
+ end
+
+ opts.on("-f", "--fake", "Run with fake ports rather than fetching") do
+ @real_data = false
+ end
+
+ opts.on(
+ "-n", "--dry-run",
+ "Figure out what state they're in, but don't take action"
+ ) do
+ @dry_run = true
+ end
+
+ opts.on("-h", "--help", "Print this help") do
+ puts opts
+ exit
+ end
+end.parse!
+
+SCHEMA = "{
+ bandwidth : { account: Text, username: Text, password: Text },
+ xmpp: { jid: Text, password: Text },
+ notification: { endpoint: Text, source_number: Text },
+ pubsub: { server: Text, node: Text },
+ testing_tel: Text,
+ admin_server: Text
+}"
+
+raise "Need a Dhall config" unless ARGV[0]
+
+CONFIG = Dhall::Coder
+ .new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
+ .load("#{ARGV.first} : #{SCHEMA}", transform_keys: :to_sym)
+
+require_relative "../lib/blather_notify"
+require_relative "../lib/expiring_lock"
+require_relative "../lib/form_to_h"
+require_relative "../lib/porting_step"
+
+Faraday.default_adapter = :em_synchrony
+BandwidthIris::Client.global_options = {
+ account_id: CONFIG[:bandwidth][:account],
+ username: CONFIG[:bandwidth][:username],
+ password: CONFIG[:bandwidth][:password]
+}
+
+class FullManual < PortingStepRepo::Outputs
+ def info(id, key, msg)
+ puts "[#{id}] INFO(#{key}): #{msg}"
+ end
+
+ def warn(id, key, msg)
+ puts "[#{id}] WARN(#{key}): #{msg}"
+ end
+
+ def error(id, key, e_or_msg)
+ puts "[#{id}] ERRR(#{key}): #{e_or_msg}"
+ return unless e_or_msg.respond_to?(:backtrace)
+
+ e_or_msg.backtrace.each do |b|
+ puts "[#{id}] ERRR(#{key}): #{b}"
+ end
+ end
+
+ def to_customer(id, key, tel, msg)
+ puts "[#{id}] CUST(#{key}, #{tel}): #{msg}"
+ end
+end
+
+class ObservedAuto < FullManual
+ def initialize(endpoint, source_number)
+ @endpoint = endpoint
+ @src = source_number
+ end
+
+ def to_customer(id, key, tel, msg)
+ ExpiringLock.new(lock_key(id, key)).with do
+ EM::HttpRequest
+ .new(@endpoint)
+ .apost(
+ head: { "Content-Type" => "application/json" },
+ body: format_msg(tel, msg)
+ )
+ end
+ end
+
+protected
+
+ def lock_key(id, key)
+ "jmp_port_customer_msg_#{key}-#{id}"
+ end
+
+ def format_msg(tel, msg)
+ [{
+ time: DateTime.now.iso8601,
+ type: "message-received",
+ to: tel,
+ description: "Incoming message received",
+ message: actual_message(tel, msg)
+ }].to_json
+ end
+
+ def actual_message(tel, msg)
+ {
+ id: SecureRandom.uuid,
+ owner: tel,
+ applicationId: SecureRandom.uuid,
+ time: DateTime.now.iso8601,
+ segmentCount: 1,
+ direction: "in",
+ to: [tel], from: @src,
+ text: msg
+ }
+ end
+end
+
+class FullAutomatic < ObservedAuto
+ using FormToH
+
+ def initialize(pubsub_addr, endpoint, source_number)
+ @pubsub = BlatherNotify.pubsub(pubsub_addr)
+
+ Sentry.init do |config|
+ config.background_worker_threads = 0
+ end
+
+ super(endpoint, source_number)
+ end
+
+ # No one's watch; swallow informational messages
+ def info(*); end
+
+ def warn(id, key, msg)
+ ExpiringLock.new(warn_lock_key(id, key), expiry: 60 * 15).with do
+ entrykey = "#{id}:#{key}"
+ @pubsub.publish("#{entrykey}": error_entry("Port Warning", msg, entrykey))
+ end
+ end
+
+ def error(id, key, e_or_msg)
+ Sentry.with_scope do |scope|
+ scope.set_context("port", { id: id, action: key })
+
+ if e_or_msg.is_a?(::Exception)
+ Sentry.capture_exception(e_or_msg)
+ else
+ Sentry.capture_message(e_or_msg.to_s)
+ end
+ end
+ end
+
+protected
+
+ def error_entry(title, text, id)
+ Nokogiri::XML::Builder.new { |xml|
+ xml.entry(xmlns: "http://www.w3.org/2005/Atom") do
+ xml.updated DateTime.now.iso8601
+ xml.id id
+ xml.title title
+ xml.content text.to_s, type: "text"
+ xml.author { xml.name "porting" }
+ xml.generator "porting", version: "1.0"
+ end
+ }.doc.root
+ end
+
+ def warn_lock_key(id, key)
+ "jmp_port_warn_msg_#{key}-#{id}"
+ end
+end
+
+@output =
+ case @verbosity
+ when 0
+ FullAutomatic.new(
+ BlatherNotify::PubSub::Address.new(**CONFIG[:pubsub]),
+ CONFIG[:notification][:endpoint],
+ CONFIG[:notification][:source_number]
+ )
+ when 1
+ ObservedAuto.new(
+ CONFIG[:notification][:endpoint],
+ CONFIG[:notification][:source_number]
+ )
+ else
+ FullManual.new
+ end
+
+ports = if @real_data
+ BandwidthIris::PortIn.list(
+ page: 1,
+ size: 50,
+ start_date: Date.today - 1,
+ end_date: Date.today
+ ) || []
+else
+ MP = Struct.new(
+ :order_id,
+ :processing_status,
+ :actual_foc_date,
+ :last_modified_date,
+ :customer_order_id,
+ :billing_telephone_number
+ )
+
+ minutes = 1.0 / (24 * 60)
+
+ [
+ # This should be ignored
+ MP.new("T01", "SUBMITTED", nil, DateTime.now - 1, "ignored", "9998887777"),
+ MP.new(
+ "T02", "COMPLETE", DateTime.now - 60 * minutes,
+ DateTime.now - 55 * minutes, "0001", "2223334444"
+ )
+ ]
+end
+
+EM.run do
+ REDIS = EM::Hiredis.connect
+
+ BlatherNotify.start(
+ CONFIG[:xmpp][:jid],
+ CONFIG[:xmpp][:password]
+ ).then {
+ ports.reduce(EMPromise.resolve(nil)) { |promise, port|
+ promise.then do
+ @output.info(port.order_id, :start, "Here we go")
+ PortingStepRepo.new(output: @output).find(port).then { |s|
+ @output.info(port.order_id, :class, s.class)
+ s
+ }.then { |s|
+ if @dry_run
+ @output.info("DRY", :dry, "Not taking action")
+ else
+ s.perform_next_step
+ end
+ }
+ end
+ }
+ }.catch { |e|
+ @output.error("ROOT", :catch, e)
+ }.then { BlatherNotify.shutdown }
+end
@@ -31,7 +31,7 @@ if @admin_info.info.tel
)
end
-if @admin_info.fwd.uri
+if @admin_info.fwd&.uri
field(
var: "fwd",
label: "Fwd",
@@ -59,7 +59,8 @@ class BlatherClient < Blather::Client
found = stanza.find(*guards)
handler.call(stanza, found) unless found.empty?
else
- handler.call(stanza) unless guarded?(guards, stanza)
+ throw :pass if guarded?(guards, stanza)
+ handler.call(stanza)
end
return result unless result.is_a?(Promise)
@@ -0,0 +1,208 @@
+# frozen_string_literal: true
+
+require "blather/client/dsl"
+require "em_promise"
+require "timeout"
+
+require "ostruct"
+require "securerandom"
+require_relative "form_to_h"
+
+module BlatherNotify
+ extend Blather::DSL
+
+ class PubSub
+ class Address
+ attr_reader :node, :server
+
+ def initialize(node:, server:)
+ @node = node
+ @server = server
+ end
+
+ def to_uri
+ "xmpp:#{@server}?;node=#{@node}"
+ end
+ end
+
+ def initialize(blather, addr)
+ @blather = blather
+ @addr = addr
+ end
+
+ def publish(xml)
+ @blather.write_with_promise(
+ Blather::Stanza::PubSub::Publish.new(
+ @addr.server,
+ @addr.node,
+ :set,
+ xml
+ )
+ )
+ end
+ end
+
+ class CommandExecution
+ using FormToH
+
+ class FormErrorResponse < RuntimeError; end
+
+ def initialize(blather, server, node)
+ @blather = blather
+ @server = server
+ @node = node
+ @sessionid = nil
+ end
+
+ def fetch_and_submit(**form)
+ get_form.then { |response|
+ @sessionid ||= response.sessionid
+
+ validate_form(form, response.form)
+
+ @blather.write_with_promise(@blather.command(
+ @node, @sessionid, form: form.to_form(:submit), server: @server
+ ))
+ }.then(&method(:check_for_error)).then { |response|
+ @last_response = response
+ OpenStruct.new(response.form.to_h)
+ }
+ end
+
+ protected
+
+ def check_for_error(response)
+ if response.note&.[]("type") == "error"
+ raise FormErrorResponse, response.note.text
+ end
+
+ response
+ end
+
+ def validate_form(to_submit, received)
+ to_submit.each_key do |key|
+ raise "No field #{key}" unless received.field(key.to_s)
+ end
+ end
+
+ def get_form
+ # If we already got a form on the last submit then
+ # assume we should fill that out here.
+ # If not, then move next to find the form
+ if @last_response&.form&.form?
+ EMPromise.resolve(@last_response)
+ else
+ @blather.write_with_promise(@blather.command(
+ @node,
+ @sessionid,
+ server: @server
+ ))
+ end
+ end
+ end
+
+ @ready = Queue.new
+
+ when_ready { @ready << :ready }
+
+ def self.start(jid, password, default_pubsub_addr: nil)
+ # workqueue_count MUST be 0 or else Blather uses threads!
+ setup(jid, password, nil, nil, nil, nil, workqueue_count: 0)
+ set_default_pubsub(default_pubsub_addr)
+
+ EM.error_handler(&method(:panic))
+
+ EM.next_tick { client.run }
+
+ block_until_ready
+ end
+
+ def self.block_until_ready
+ if EM.reactor_running?
+ promise = EMPromise.new
+ disconnected { true.tap { EM.next_tick { EM.stop } } }
+ Thread.new { promise.fulfill(@ready.pop) }
+ timeout_promise(promise, timeout: 30)
+ else
+ @thread = Thread.new { EM.run }
+ Timeout.timeout(30) { @ready.pop }
+ at_exit { wait_then_exit }
+ end
+ end
+
+ def self.panic(e)
+ warn e.message
+ warn e.backtrace
+ exit! 2
+ end
+
+ def self.wait_then_exit
+ disconnected { EM.stop }
+ EM.add_timer(30) { EM.stop }
+ shutdown
+ @thread&.join
+ end
+
+ def self.timeout_promise(promise, timeout: 15)
+ timer = EventMachine::Timer.new(timeout) {
+ promise.reject(:timeout)
+ }
+
+ promise.then do
+ timer.cancel
+ end
+ end
+
+ def self.write_with_promise(stanza)
+ promise = EMPromise.new
+ timeout_promise(promise)
+
+ client.write_with_handler(stanza) do |s|
+ if s.error? || s.type == :error
+ promise.reject(s)
+ else
+ promise.fulfill(s)
+ end
+ end
+ promise
+ end
+
+ def self.command(
+ node, sessionid=nil,
+ server: CONFIG[:sgx_jmp], action: :execute, form: nil
+ )
+ Blather::Stanza::Iq::Command.new.tap do |cmd|
+ cmd.to = server
+ cmd.node = node
+ cmd.command[:sessionid] = sessionid if sessionid
+ cmd.action = action
+ cmd.command << form if form
+ end
+ end
+
+ def self.execute(command_node, form=nil)
+ write_with_promise(command(command_node)).then do |iq|
+ next iq unless form
+
+ write_with_promise(command(command_node, iq.sessionid, form: form))
+ end
+ end
+
+ def self.pubsub(addr)
+ PubSub.new(self, addr)
+ end
+
+ def self.set_default_pubsub(addr)
+ @default_pubsub = addr && pubsub(addr)
+ end
+
+ def self.publish(xml)
+ raise "No default pubsub set!" unless @default_pubsub
+
+ @default_pubsub.publish(xml)
+ end
+
+ def self.command_execution(server, node)
+ CommandExecution.new(self, server, node)
+ end
+end
@@ -18,4 +18,16 @@ module FormToH
params.to_h
end
end
+
+ refine ::Hash do
+ def to_fields
+ map { |k, v| { var: k.to_s, value: v.to_s } }
+ end
+
+ def to_form(type)
+ Blather::Stanza::Iq::X.new(type).tap do |form|
+ form.fields = to_fields
+ end
+ end
+ end
end
@@ -0,0 +1,311 @@
+# frozen_string_literal: true
+
+require "date"
+require "em_promise"
+require "lazy_object"
+require "value_semantics/monkey_patched"
+
+require_relative "blather_notify"
+
+class Tel
+ def initialize(str)
+ @tel = if str.is_a? Tel
+ str.to_s
+ else
+ "+1#{str.sub(/^\+?1?/, '')}"
+ end
+ end
+
+ def to_s
+ @tel
+ end
+
+ def ==(other)
+ to_s == other.to_s
+ end
+end
+
+class PortingStepRepo
+ # Any thing that debounces messages must happen inside this.
+ # The porting logic will pass everything in every time.
+ class Outputs
+ def info(port_id, kind, msg); end
+
+ def warn(port_id, kind, msg); end
+
+ def error(port_id, kind, e_or_msg); end
+
+ def to_customer(port_id, kind, tel, msg); end
+ end
+
+ value_semantics do
+ redis Anything(), default: LazyObject.new { REDIS }
+ blather_notify Anything(), default: BlatherNotify
+ admin_server Anything(), default: CONFIG[:admin_server]
+ testing_tel Anything(), default: CONFIG[:testing_tel]
+ output Outputs, default: Outputs.new
+ end
+
+ def find(port)
+ if_processing(port) do
+ case port.processing_status
+ when "FOC"
+ FOC.new(**to_h).find(port)
+ when "COMPLETE"
+ Complete.new(**to_h).find(port)
+ else
+ Wait.new(port, output: output)
+ end
+ end
+ end
+
+ def if_processing(port)
+ redis.exists("jmp_port_freeze-#{port.order_id}").then do |v|
+ next Frozen.new(port, output: output) if v == 1
+
+ yield
+ end
+ end
+
+ class FOC < self
+ # This is how long we'll wait for a port to move from FOC to COMPLETED
+ # It's in fractional days because DateTime
+ GRACE_PERIOD = 15.0 / (24 * 60)
+
+ def find(port)
+ Alert.for(
+ port,
+ grace_period: GRACE_PERIOD,
+ output: output, key: :late_foc,
+ msg: "⚠ Port is still in FOC state a while past FOC",
+ real_step: Wait.new(port, output: output)
+ )
+ end
+ end
+
+ class Complete < self
+ # If it's been 35 minutes and the number isn't reachable, a human
+ # should get involved
+ GRACE_PERIOD = 35.0 / (24 * 60)
+
+ def find(port)
+ if_not_complete(port) do
+ exe = blather_notify.command_execution(admin_server, "customer info")
+ AdminCommand.new(exe: exe, **to_h).find(port).then do |step|
+ Alert.for(
+ port,
+ grace_period: GRACE_PERIOD, output: output,
+ key: :late_finish, msg: msg(port), real_step: step
+ )
+ end
+ end
+ end
+
+ def if_not_complete(port)
+ redis.exists("jmp_port_complete-#{port.order_id}").then do |v|
+ next Done.new(port, output: output) if v == 1
+
+ yield
+ end
+ end
+
+ def msg(port)
+ "⚠ Port still hasn't finished. We'll keep trying unless you set redis "\
+ "key `jmp_port_freeze-#{port.order_id}`"
+ end
+
+ class AdminCommand < self
+ def initialize(exe:, **kwargs)
+ @exe = exe
+ super(**kwargs)
+ end
+
+ def to_h
+ super.merge(exe: @exe)
+ end
+
+ def find(port)
+ @exe.fetch_and_submit(q: port.customer_order_id).then do |form|
+ tel = Tel.new(port.billing_telephone_number)
+ if tel == Tel.new(form.tel)
+ GoodNumber.new(**to_h).find(port)
+ else
+ WrongNumber.new(right_number: tel, execution: @exe)
+ end
+ end
+ end
+
+ class GoodNumber < self
+ def find(port)
+ Reachability.new(type: "voice", **to_h).test(port) do
+ Reachability.new(type: "sms", **to_h).test(port) do
+ FinishUp.new(port, redis: redis, output: output)
+ end
+ end
+ end
+
+ class Reachability < self
+ def initialize(type:, **args)
+ @type = type
+ super(**args)
+ end
+
+ def test(port)
+ execute_command(port).then do |response|
+ if response.count == "0"
+ RunTest.new(
+ type: @type, tel: port.billing_telephone_number,
+ **to_h.slice(:blather_notify, :testing_tel, :admin_server)
+ )
+ else
+ yield
+ end
+ end
+ end
+
+ class RunTest
+ # This is here for tests
+ attr_reader :type
+
+ def initialize(
+ type:, tel:, blather_notify:, testing_tel:, admin_server:
+ )
+ @type = type
+ @tel = tel
+ @blather_notify = blather_notify
+ @testing_tel = testing_tel
+ @admin_server = admin_server
+ end
+
+ def perform_next_step
+ @blather_notify.command_execution(@admin_server, "reachability")
+ .fetch_and_submit(
+ tel: @tel, type: @type, reachability_tel: @testing_tel
+ )
+ end
+ end
+
+ protected
+
+ def execute_command(port)
+ blather_notify.command_execution(admin_server, "reachability")
+ .fetch_and_submit(tel: port.billing_telephone_number, type: @type)
+ end
+ end
+
+ class FinishUp
+ MESSAGE = "Hi! This is JMP support - your number has "\
+ "successfully transferred in to JMP! All calls/messages "\
+ "will now use your transferred-in number - your old JMP "\
+ "number has been disabled. Let us know if you have any "\
+ "questions and thanks for using JMP!"
+
+ def initialize(port, redis:, output:)
+ @port = port
+ @redis = redis
+ @output = output
+ end
+
+ def set_key
+ @redis.set(
+ "jmp_port_complete-#{@port.order_id}",
+ DateTime.now.iso8601,
+ "EX",
+ 60 * 60 * 24 * 2 ### 2 Days should be enough to not see it listed
+ )
+ end
+
+ def perform_next_step
+ set_key.then do
+ EMPromise.all([
+ @output.info(@port.order_id, :complete, "Port Complete!"),
+ @output.to_customer(
+ @port.order_id, :complete,
+ Tel.new(@port.billing_telephone_number), MESSAGE
+ )
+ ])
+ end
+ end
+ end
+ end
+
+ class WrongNumber
+ def initialize(right_number:, execution:)
+ @right_number = right_number
+ @exe = execution
+ end
+
+ def perform_next_step
+ @exe.fetch_and_submit(action: "number_change").then do |_form|
+ @exe.fetch_and_submit(new_tel: @right_number, should_delete: "true")
+ end
+ end
+ end
+ end
+ end
+
+ # This doesn't do anything and just waits for something to happen later
+ class Wait
+ def initialize(port, output:)
+ @port = port
+ @output = output
+ end
+
+ def perform_next_step
+ @output.info(@port.order_id, :wait, "Waiting...")
+ end
+ end
+
+ # This also doesn't do anything but is more content about it
+ class Done
+ def initialize(port, output:)
+ @port = port
+ @output = output
+ end
+
+ def perform_next_step
+ @output.info(@port.order_id, :done, "Done.")
+ end
+ end
+
+ # This also also doesn't do anything but is intentional
+ class Frozen
+ def initialize(port, output:)
+ @port = port
+ @output = output
+ end
+
+ def perform_next_step
+ @output.info(@port.order_id, :frozen, "Frozen.")
+ end
+ end
+
+ # This class sends and error to the human to check things out
+ class Alert
+ def self.for(port, grace_period:, real_step:, **args)
+ if (DateTime.now - port.actual_foc_date) > grace_period
+ new(port, real_step: real_step, **args)
+ else
+ real_step
+ end
+ end
+
+ # For tests
+ attr_reader :key
+ attr_reader :real_step
+
+ def initialize(port, real_step:, output:, msg:, key:)
+ @port = port
+ @real_step = real_step
+ @output = output
+ @msg = msg
+ @key = key
+ end
+
+ def perform_next_step
+ @output.warn(@port.order_id, @key, @msg).then {
+ @real_step.perform_next_step
+ }
+ end
+ end
+end
@@ -0,0 +1,412 @@
+# frozen_string_literal: true
+
+require "date"
+require "ostruct"
+require "test_helper"
+
+require "customer_info"
+require "form_template"
+require "form_to_h"
+require "porting_step"
+
+MINS = 1.0 / (24 * 60)
+
+class BlatherNotifyMock < Minitest::Mock
+ def initialize
+ super
+ @exes = []
+ end
+
+ def expect_execution(server, node, *args)
+ exe = Execution.new(args)
+ expect(
+ :command_execution,
+ exe,
+ [server, node]
+ )
+ @exes << exe
+ end
+
+ def verify
+ super
+ @exes.each(&:verify)
+ end
+
+ class Execution < Minitest::Mock
+ def initialize(args)
+ super()
+ args.each_slice(2) do |(submission, result)|
+ expect(
+ :fetch_and_submit,
+ EMPromise.resolve(to_response(result)),
+ **submission
+ )
+ end
+ end
+
+ using FormToH
+
+ def to_response(form)
+ OpenStruct.new(form.to_h)
+ end
+ end
+end
+
+def info(tel)
+ CustomerInfo.new(
+ plan_info: PlanInfo::NoPlan.new,
+ tel: tel,
+ balance: 0.to_d,
+ cnam: nil
+ )
+end
+
+def admin_info(customer_id, tel)
+ AdminInfo.new(
+ jid: Blather::JID.new("#{customer_id}@example.com"),
+ customer_id: customer_id,
+ fwd: nil,
+ info: info(tel),
+ api: API::V2.new,
+ call_info: "",
+ trust_level: "",
+ backend_jid: "customer_#{customer_id}@example.com"
+ )
+end
+
+def menu
+ FormTemplate.render("admin_menu")
+end
+
+class PortingStepTest < Minitest::Test
+ Port = Struct.new(
+ :order_id,
+ :processing_status,
+ :actual_foc_date,
+ :last_modified_date,
+ :customer_order_id,
+ :billing_telephone_number
+ )
+
+ def test_ignore_submitted_ports
+ redis = Minitest::Mock.new
+ redis.expect(:exists, EMPromise.resolve(0), ["jmp_port_freeze-01"])
+
+ step = PortingStepRepo.new(redis: redis).find(Port.new(
+ "01",
+ "SUBMITTED",
+ nil,
+ DateTime.now - 1 * MINS,
+ "ignored",
+ "9998887777"
+ )).sync
+
+ assert_kind_of PortingStepRepo::Wait, step
+ end
+ em :test_ignore_submitted_ports
+
+ def test_ignore_recent_foc
+ redis = Minitest::Mock.new
+ redis.expect(:exists, EMPromise.resolve(0), ["jmp_port_freeze-01"])
+
+ step = PortingStepRepo.new(redis: redis).find(Port.new(
+ "01",
+ "FOC",
+ DateTime.now - 5 * MINS,
+ DateTime.now - 1 * MINS,
+ "ignored",
+ "9998887777"
+ )).sync
+
+ assert_kind_of PortingStepRepo::Wait, step
+ end
+ em :test_ignore_recent_foc
+
+ def test_warn_for_late_foc
+ redis = Minitest::Mock.new
+ redis.expect(:exists, EMPromise.resolve(0), ["jmp_port_freeze-01"])
+
+ step = PortingStepRepo.new(redis: redis).find(Port.new(
+ "01",
+ "FOC",
+ DateTime.now - 25 * MINS,
+ DateTime.now - 1 * MINS,
+ "ignored",
+ "9998887777"
+ )).sync
+
+ assert_kind_of PortingStepRepo::Alert, step
+ assert_equal :late_foc, step.key
+ assert_kind_of PortingStepRepo::Wait, step.real_step
+ end
+ em :test_warn_for_late_foc
+
+ def test_already_complete
+ redis = Minitest::Mock.new
+ redis.expect(:exists, EMPromise.resolve(0), ["jmp_port_freeze-01"])
+ redis.expect(:exists, 1, ["jmp_port_complete-01"])
+
+ step = PortingStepRepo.new(redis: redis).find(Port.new(
+ "01",
+ "COMPLETE",
+ DateTime.now - 25 * MINS,
+ DateTime.now - 1 * MINS,
+ "completed",
+ "9998887777"
+ )).sync
+
+ assert_kind_of PortingStepRepo::Done, step
+ assert_mock redis
+ end
+ em :test_already_complete
+
+ def test_change_number
+ redis = Minitest::Mock.new
+ redis.expect(:exists, EMPromise.resolve(0), ["jmp_port_freeze-01"])
+ redis.expect(:exists, "0", ["jmp_port_complete-01"])
+
+ notify = BlatherNotifyMock.new
+ notify.expect_execution(
+ "sgx", "customer info",
+ { q: "starting" }, admin_info("starting", "+19998881111").form
+ )
+
+ step = PortingStepRepo.new(
+ redis: redis,
+ blather_notify: notify,
+ admin_server: "sgx"
+ ).find(Port.new(
+ "01",
+ "COMPLETE",
+ DateTime.now - 25 * MINS,
+ DateTime.now - 1 * MINS,
+ "starting",
+ "9998887777"
+ )).sync
+
+ assert_kind_of PortingStepRepo::Complete::AdminCommand::WrongNumber, step
+ assert_mock redis
+ assert_mock notify
+ end
+ em :test_change_number
+
+ def test_first_reachability
+ redis = Minitest::Mock.new
+ redis.expect(:exists, EMPromise.resolve(0), ["jmp_port_freeze-01"])
+ redis.expect(:exists, "0", ["jmp_port_complete-01"])
+
+ notify = BlatherNotifyMock.new
+ notify.expect_execution(
+ "sgx", "customer info",
+ { q: "starting" }, admin_info("starting", "+19998887777").form
+ )
+
+ notify.expect_execution(
+ "sgx", "reachability",
+ { tel: "9998887777", type: "voice" },
+ FormTemplate.render("reachability_result", count: 0)
+ )
+
+ step = PortingStepRepo.new(
+ redis: redis,
+ blather_notify: notify,
+ admin_server: "sgx"
+ ).find(Port.new(
+ "01",
+ "COMPLETE",
+ DateTime.now - 25 * MINS,
+ DateTime.now - 1 * MINS,
+ "starting",
+ "9998887777"
+ )).sync
+
+ assert_kind_of(
+ PortingStepRepo::Complete::AdminCommand::GoodNumber::
+ Reachability::RunTest,
+ step
+ )
+ assert_equal "voice", step.type
+ assert_mock redis
+ assert_mock notify
+ end
+ em :test_first_reachability
+
+ def test_reach_sms_reachability
+ redis = Minitest::Mock.new
+ redis.expect(:exists, EMPromise.resolve(0), ["jmp_port_freeze-01"])
+ redis.expect(:exists, "0", ["jmp_port_complete-01"])
+
+ notify = BlatherNotifyMock.new
+ notify.expect_execution(
+ "sgx", "customer info",
+ { q: "starting" }, admin_info("starting", "+19998887777").form
+ )
+
+ notify.expect_execution(
+ "sgx", "reachability",
+ { tel: "9998887777", type: "voice" },
+ FormTemplate.render("reachability_result", count: 1)
+ )
+
+ notify.expect_execution(
+ "sgx", "reachability",
+ { tel: "9998887777", type: "sms" },
+ FormTemplate.render("reachability_result", count: 0)
+ )
+
+ step = PortingStepRepo.new(
+ redis: redis,
+ blather_notify: notify,
+ admin_server: "sgx"
+ ).find(Port.new(
+ "01",
+ "COMPLETE",
+ DateTime.now - 25 * MINS,
+ DateTime.now - 1 * MINS,
+ "starting",
+ "9998887777"
+ )).sync
+
+ assert_kind_of(
+ PortingStepRepo::Complete::AdminCommand::GoodNumber::
+ Reachability::RunTest,
+ step
+ )
+ assert_equal "sms", step.type
+ assert_mock redis
+ assert_mock notify
+ end
+ em :test_reach_sms_reachability
+
+ def test_all_reachable
+ redis = Minitest::Mock.new
+ redis.expect(:exists, EMPromise.resolve(0), ["jmp_port_freeze-01"])
+ redis.expect(:exists, "0", ["jmp_port_complete-01"])
+
+ notify = BlatherNotifyMock.new
+ notify.expect_execution(
+ "sgx", "customer info",
+ { q: "starting" }, admin_info("starting", "+19998887777").form
+ )
+
+ notify.expect_execution(
+ "sgx", "reachability",
+ { tel: "9998887777", type: "voice" },
+ FormTemplate.render("reachability_result", count: 1)
+ )
+
+ notify.expect_execution(
+ "sgx", "reachability",
+ { tel: "9998887777", type: "sms" },
+ FormTemplate.render("reachability_result", count: 1)
+ )
+
+ step = PortingStepRepo.new(
+ redis: redis,
+ blather_notify: notify,
+ admin_server: "sgx"
+ ).find(Port.new(
+ "01",
+ "COMPLETE",
+ DateTime.now - 25 * MINS,
+ DateTime.now - 1 * MINS,
+ "starting",
+ "9998887777"
+ )).sync
+
+ assert_kind_of(
+ PortingStepRepo::Complete::AdminCommand::GoodNumber::FinishUp,
+ step
+ )
+ assert_mock redis
+ assert_mock notify
+ end
+ em :test_all_reachable
+
+ def test_not_done_in_time
+ redis = Minitest::Mock.new
+ redis.expect(:exists, EMPromise.resolve(0), ["jmp_port_freeze-01"])
+ redis.expect(:exists, "0", ["jmp_port_complete-01"])
+
+ notify = BlatherNotifyMock.new
+ notify.expect_execution(
+ "sgx", "customer info",
+ { q: "starting" }, admin_info("starting", "+19998887777").form
+ )
+
+ notify.expect_execution(
+ "sgx", "reachability",
+ { tel: "9998887777", type: "voice" },
+ FormTemplate.render("reachability_result", count: 0)
+ )
+
+ step = PortingStepRepo.new(
+ redis: redis,
+ blather_notify: notify,
+ admin_server: "sgx"
+ ).find(Port.new(
+ "01",
+ "COMPLETE",
+ DateTime.now - 55 * MINS,
+ DateTime.now - 50 * MINS,
+ "starting",
+ "9998887777"
+ )).sync
+
+ assert_kind_of PortingStepRepo::Alert, step
+ assert_equal :late_finish, step.key
+ assert_kind_of(
+ PortingStepRepo::Complete::AdminCommand::GoodNumber::
+ Reachability::RunTest,
+ step.real_step
+ )
+ assert_mock redis
+ assert_mock notify
+ end
+ em :test_not_done_in_time
+
+ def test_ignore_frozen_ports
+ # This tests that we ignore ports in various states
+ [
+ Port.new(
+ "01",
+ "SUBMITTED",
+ nil,
+ DateTime.now - 1 * MINS,
+ "ignored",
+ "9998887777"
+ ),
+ Port.new(
+ "01",
+ "FOC",
+ DateTime.now - 300 * MINS,
+ DateTime.now - 300 * MINS,
+ "ignored",
+ "9998887777"
+ ),
+ Port.new(
+ "01",
+ "COMPLETED",
+ DateTime.now - 10 * MINS,
+ DateTime.now - 10 * MINS,
+ "ignored",
+ "9998887777"
+ ),
+ Port.new(
+ "01",
+ "COMPLETED",
+ DateTime.now - 300 * MINS,
+ DateTime.now - 300 * MINS,
+ "ignored",
+ "9998887777"
+ )
+ ].each do |port|
+ redis = Minitest::Mock.new
+ redis.expect(:exists, EMPromise.resolve(1), ["jmp_port_freeze-01"])
+
+ step = PortingStepRepo.new(redis: redis).find(port).sync
+ assert_kind_of PortingStepRepo::Frozen, step
+ end
+ end
+ em :test_ignore_frozen_ports
+end