@@ -0,0 +1,33 @@
+form!
+title "Setup Snikket Instance"
+
+instructions(
+ "Phone number #{@tel} has been activated. " \
+ "Now you will need a Jabber ID to associate with your new JMP account. " \
+ "JMP has partnered with Snikket CIC to provide you with your very own " \
+ "Snikket instance that will provide Jabber service to you and anyone you " \
+ "invite.\n\n" \
+ "A Jabber ID is kind of like an email address, and looks similar too. " \
+ "People who contact you using your Jabber ID instead of your phone number " \
+ "will get additional features, such as video calling and end-to-end " \
+ "encryption.\n\n" \
+ "You will be prompted to choose a username after your instance is ready."
+)
+
+field(
+ label: "Choose a unique name for your Snikket instance",
+ var: "subdomain",
+ suffix: ".snikket.chat",
+ description: @error ||
+ "Your Jabber ID will be of the form username@instance-name.snikket.chat"
+)
+
+field(
+ label: "Action",
+ type: "list-single",
+ var: "http://jabber.org/protocol/commands#actions",
+ options: [
+ { label: "Next", value: "next" },
+ { label: "Use Your Own Domain Name", value: "custom_domain" }
+ ]
+)
@@ -514,6 +514,135 @@ class Registration
put_default_fwd
])
}.then do
+ FinishOnboarding.for(@customer, @tel).write
+ end
+ end
+ end
+
+ module FinishOnboarding
+ def self.for(customer, tel)
+ jid = ProxiedJID.new(customer.jid).unproxied
+ if jid.domain == CONFIG[:onboarding_domain]
+ Snikket.new(tel)
+ else
+ NotOnboarding.new(customer, tel)
+ end
+ end
+
+ class Snikket
+ def initialize(tel, error: nil)
+ @tel = tel
+ @error = error
+ end
+
+ ACTION_VAR = "http://jabber.org/protocol/commands#actions"
+
+ def write
+ Command.reply { |reply|
+ reply.allowed_actions = [:next]
+ reply.command << form
+ }.then do |iq|
+ if iq.form.field(ACTION_VAR)&.value == "custom_domain"
+ CustomDomain.new(@tel).write
+ else
+ launch("#{iq.form.field('subdomain')&.value}.snikket.chat")
+ end
+ end
+ end
+
+ def form
+ FormTemplate.render(
+ "registration/snikket",
+ tel: @tel,
+ error: @error
+ )
+ end
+
+ def launch(domain)
+ IQ_MANAGER.write(::Snikket::Launch.new(
+ nil, CONFIG[:snikket_hosting_api],
+ domain: domain
+ )).then { |launched|
+ GetInvite.for(domain, launched).then(&:write)
+ }.catch { |e|
+ next EMPromise.reject(e) unless e.respond_to?(:text)
+
+ Snikket.new(@tel, error: e.text).write
+ }
+ end
+
+ class GetInvite
+ def self.for(domain, launched)
+ launched.fetch_invite(domain).then do |xmpp_uri|
+ if xmpp_uri
+ GoToInvite.new(xmpp_uri)
+ else
+ new(domain, launched)
+ end
+ end
+ end
+
+ def initialize(domain, launched)
+ @domain = domain
+ @launched = launched
+ end
+
+ def write
+ Command.reply { |reply|
+ reply.allowed_actions = [:next]
+ reply.note_type = :info
+ reply.note_text =
+ "Your instance #{@domain} is starting up. " \
+ "This may take several minutes. " \
+ "Press next to check if it is ready."
+ }.then { GetInvite.for(@domain, @launched).then(&:write) }
+ end
+ end
+
+ class GoToInvite
+ def initialize(xmpp_uri)
+ @xmpp_uri = xmpp_uri
+ end
+
+ def write
+ Command.finish do |reply|
+ oob = OOB.find_or_create(reply.command)
+ oob.url = @xmpp_uri
+ end
+ end
+ end
+ end
+
+ class CustomDomain
+ def initialize(tel)
+ @tel = tel
+ end
+
+ CONTACT_SUPPORT =
+ "Please contact JMP support to set up " \
+ "an instance on an existing domain."
+
+ def write
+ Command.reply { |reply|
+ reply.allowed_actions = [:prev]
+ reply.status = :canceled
+ reply.note_type = :info
+ reply.note_text = CONTACT_SUPPORT
+ }.then do |iq|
+ raise "Action not allowed" unless iq.prev?
+
+ Snikket.new(@tel).write
+ end
+ end
+ end
+
+ class NotOnboarding
+ def initialize(customer, tel)
+ @customer = customer
+ @tel = tel
+ end
+
+ def write
WelcomeMessage.new(@customer, @tel).welcome
Command.finish("Your JMP account has been activated as #{@tel}")
end
@@ -810,6 +810,89 @@ class RegistrationTest < Minitest::Test
end
em :test_write
+ def test_write_onboarding
+ create_order = stub_request(
+ :post,
+ "https://dashboard.bandwidth.com/v1.0/accounts//orders"
+ ).to_return(status: 201, body: <<~RESPONSE)
+ <OrderResponse>
+ <Order>
+ <id>test_order</id>
+ </Order>
+ </OrderResponse>
+ RESPONSE
+ stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/accounts//orders/test_order"
+ ).to_return(status: 201, body: <<~RESPONSE)
+ <OrderResponse>
+ <OrderStatus>COMPLETE</OrderStatus>
+ <CompletedNumbers>
+ <TelephoneNumber>
+ <FullNumber>5555550000</FullNumber>
+ </TelephoneNumber>
+ </CompletedNumbers>
+ </OrderResponse>
+ RESPONSE
+ stub_request(
+ :post,
+ "https://dashboard.bandwidth.com/v1.0/accounts//sites//sippeers//movetns"
+ )
+ Registration::Finish::REDIS.expect(
+ :del,
+ nil,
+ ["pending_tel_for-test\\40onboarding.example.com@proxy"]
+ )
+ Bwmsgsv2Repo::REDIS.expect(
+ :set,
+ nil,
+ [
+ "catapult_fwd-+15555550000",
+ "xmpp:test\\40onboarding.example.com@proxy"
+ ]
+ )
+ Bwmsgsv2Repo::REDIS.expect(
+ :set,
+ nil,
+ ["catapult_fwd_timeout-customer_test@component", 25]
+ )
+ result = execute_command do
+ @sgx.expect(
+ :register!,
+ EMPromise.resolve(@sgx.with(
+ registered?: Blather::Stanza::Iq::IBR.new.tap do |ibr|
+ ibr.phone = "+15555550000"
+ end
+ )),
+ ["+15555550000"]
+ )
+
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert iq.form.field("subdomain")
+ end]
+ )
+
+ Registration::Finish.new(
+ customer(
+ sgx: @sgx,
+ jid: Blather::JID.new("test\\40onboarding.example.com@proxy")
+ ),
+ "+15555550000"
+ ).write.catch { |e| e }
+ end
+ assert_equal :test_result, result
+ assert_requested create_order
+ assert_mock @sgx
+ assert_mock Registration::Finish::REDIS
+ assert_mock Bwmsgsv2Repo::REDIS
+ assert_mock Command::COMMAND_MANAGER
+ end
+ em :test_write_onboarding
+
def test_write_tn_fail
create_order = stub_request(
:post,
@@ -856,4 +939,227 @@ class RegistrationTest < Minitest::Test
end
em :test_write_tn_fail
end
+
+ class SnikketTest < Minitest::Test
+ Command::COMMAND_MANAGER = Minitest::Mock.new
+ Registration::FinishOnboarding::Snikket::IQ_MANAGER = Minitest::Mock.new
+
+ def setup
+ @sgx = Minitest::Mock.new(TrivialBackendSgxRepo.new.get("test"))
+ @snikket = Registration::FinishOnboarding::Snikket.new("+15555550000")
+ end
+
+ def test_write
+ xmpp_uri = "xmpp:test.snikket.chat?register;preauth=NEWTOKEN"
+
+ subdomain_form = Blather::Stanza::Iq::Command.new
+ subdomain_form.form.fields = [
+ { var: "subdomain", value: "test" }
+ ]
+
+ launched = Snikket::Launched.new
+ launched << Niceogiri::XML::Node.new(
+ :launched, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |inner|
+ inner << Niceogiri::XML::Node.new(
+ :bootstrap, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |bootstrap|
+ bootstrap << Niceogiri::XML::Node.new(
+ :token, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |token|
+ token.content = "TOKEN"
+ }
+ }
+ }
+
+ blather = Minitest::Mock.new
+ blather.expect(
+ :<<,
+ nil,
+ [Matching.new do |reply|
+ assert_equal :completed, reply.status
+ assert_equal(
+ xmpp_uri,
+ OOB.find_or_create(reply.command).url
+ )
+ end]
+ )
+
+ execute_command(blather: blather) do
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.resolve(subdomain_form),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert iq.form.field("subdomain")
+ end]
+ )
+
+ Registration::FinishOnboarding::Snikket::IQ_MANAGER.expect(
+ :write,
+ EMPromise.resolve(launched),
+ [Matching.new do |iq|
+ assert_equal :set, iq.type
+ assert_equal CONFIG[:snikket_hosting_api], iq.to.to_s
+ assert_equal(
+ "test.snikket.chat",
+ iq.xpath(
+ "./ns:launch/ns:domain",
+ ns: "xmpp:snikket.org/hosting/v1"
+ ).text
+ )
+ end]
+ )
+
+ # Webmock doesn't support redirects properly, but they work live
+ stub_request(
+ :head, "https://test.snikket.chat/invites_bootstrap?token=TOKEN"
+ ).to_return(
+ status: 200,
+ headers: {
+ "link" => "<#{xmpp_uri}>; rel=\"alternate\""
+ }
+ )
+
+ @snikket.write
+ end
+
+ assert_mock Command::COMMAND_MANAGER
+ assert_mock Registration::FinishOnboarding::Snikket::IQ_MANAGER
+ assert_mock blather
+ end
+ em :test_write
+
+ def test_write_not_yet
+ subdomain_form = Blather::Stanza::Iq::Command.new
+ subdomain_form.form.fields = [
+ { var: "subdomain", value: "test" }
+ ]
+
+ launched = Snikket::Launched.new
+ launched << Niceogiri::XML::Node.new(
+ :launched, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |inner|
+ inner << Niceogiri::XML::Node.new(
+ :bootstrap, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |bootstrap|
+ bootstrap << Niceogiri::XML::Node.new(
+ :token, launched.document, "xmpp:snikket.org/hosting/v1"
+ ).tap { |token|
+ token.content = "TOKEN"
+ }
+ }
+ }
+
+ result = execute_command do
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.resolve(subdomain_form),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert iq.form.field("subdomain")
+ end]
+ )
+
+ Registration::FinishOnboarding::Snikket::IQ_MANAGER.expect(
+ :write,
+ EMPromise.resolve(launched),
+ [Matching.new do |iq|
+ assert_equal :set, iq.type
+ assert_equal CONFIG[:snikket_hosting_api], iq.to.to_s
+ assert_equal(
+ "test.snikket.chat",
+ iq.xpath(
+ "./ns:launch/ns:domain",
+ ns: "xmpp:snikket.org/hosting/v1"
+ ).text
+ )
+ end]
+ )
+
+ stub_request(
+ :head, "https://test.snikket.chat/invites_bootstrap?token=TOKEN"
+ ).to_timeout
+
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ [Matching.new do |iq|
+ assert_equal :info, iq.note_type
+ assert iq.note.content =~ / test\.snikket\.chat /
+ end]
+ )
+
+ @snikket.write.catch { |e| e }
+ end
+
+ assert_equal :test_result, result
+ assert_mock Command::COMMAND_MANAGER
+ assert_mock Registration::FinishOnboarding::Snikket::IQ_MANAGER
+ end
+ em :test_write_not_yet
+
+ def test_write_already_taken
+ subdomain_form = Blather::Stanza::Iq::Command.new
+ subdomain_form.form.fields = [
+ { var: "subdomain", value: "test" }
+ ]
+
+ launched = Snikket::Launched.new.as_error(
+ "internal-server-error",
+ :wait,
+ "This is an error"
+ )
+
+ result = execute_command do
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.resolve(subdomain_form),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert iq.form.field("subdomain")
+ end]
+ )
+
+ Registration::FinishOnboarding::Snikket::IQ_MANAGER.expect(
+ :write,
+ EMPromise.reject(launched),
+ [Matching.new do |iq|
+ assert_equal :set, iq.type
+ assert_equal CONFIG[:snikket_hosting_api], iq.to.to_s
+ assert_equal(
+ "test.snikket.chat",
+ iq.xpath(
+ "./ns:launch/ns:domain",
+ ns: "xmpp:snikket.org/hosting/v1"
+ ).text
+ )
+ end]
+ )
+
+ stub_request(
+ :head, "https://test.snikket.chat/invites_bootstrap?token=TOKEN"
+ ).to_timeout
+
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ [Matching.new do |iq|
+ assert iq.executing?
+ assert_equal(
+ "This is an error",
+ iq.form.field("subdomain").desc
+ )
+ end]
+ )
+
+ @snikket.write.catch { |e| e }
+ end
+
+ assert_equal :test_result, result
+ assert_mock Command::COMMAND_MANAGER
+ assert_mock Registration::FinishOnboarding::Snikket::IQ_MANAGER
+ end
+ em :test_write_already_taken
+ end
end