diff --git a/Gemfile b/Gemfile index c9623ae3dc918289b6f1b8c1e9a17d8e8b830fbf..1434dcd91915aa18af9fb265380c573ce9fe7ea1 100644 --- a/Gemfile +++ b/Gemfile @@ -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" diff --git a/bin/porting b/bin/porting new file mode 100755 index 0000000000000000000000000000000000000000..8313e6b881caeb8cd8ce5ab011e01e51dd54d7e9 --- /dev/null +++ b/bin/porting @@ -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 diff --git a/forms/admin_info.rb b/forms/admin_info.rb index 9b31c1ed3c9d684481d5f02b0e5d9c1f7657d774..81b44dfb9dfaaa6490d97b48babd3d8f8d09df01 100644 --- a/forms/admin_info.rb +++ b/forms/admin_info.rb @@ -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", diff --git a/lib/blather_client.rb b/lib/blather_client.rb index 6de5c2ddf899d9b9999f5ade86db6c335011eecc..29aec145b9e0a82b4644f6f6167a4d2e18efa1a8 100644 --- a/lib/blather_client.rb +++ b/lib/blather_client.rb @@ -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) diff --git a/lib/blather_notify.rb b/lib/blather_notify.rb new file mode 100644 index 0000000000000000000000000000000000000000..70d2300c44e30e58c20ae704178c3300907ce433 --- /dev/null +++ b/lib/blather_notify.rb @@ -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 diff --git a/lib/form_to_h.rb b/lib/form_to_h.rb index 3ab2fed5d4e46b3070f2b382058bb0883b1f985d..69705b942e0dfc7a95a6842fa9e0fe49196e1e5a 100644 --- a/lib/form_to_h.rb +++ b/lib/form_to_h.rb @@ -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 diff --git a/lib/porting_step.rb b/lib/porting_step.rb new file mode 100644 index 0000000000000000000000000000000000000000..3d3e93779f1f68ff83c3e5f31d9bcbdf1e7419f7 --- /dev/null +++ b/lib/porting_step.rb @@ -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 diff --git a/test/test_porting_step.rb b/test/test_porting_step.rb new file mode 100644 index 0000000000000000000000000000000000000000..940110d94b6a41d1be2d370703bc1c27d1c14afe --- /dev/null +++ b/test/test_porting_step.rb @@ -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