M .rubocop.yml => .rubocop.yml +3 -0
@@ 45,6 45,9 @@ Layout/SpaceAroundEqualsInParameterDefault:
Layout/AccessModifierIndentation:
EnforcedStyle: outdent
+Layout/FirstParameterIndentation:
+ EnforcedStyle: consistent
+
Style/BlockDelimiters:
EnforcedStyle: braces_for_chaining
M Gemfile => Gemfile +2 -0
@@ 8,9 8,11 @@ gem "dhall"
gem "em-hiredis"
gem "em-http-request"
gem "em-pg-client", git: "https://github.com/royaltm/ruby-em-pg-client"
+gem "em-synchrony"
gem "em_promise.rb"
gem "eventmachine"
gem "money-open-exchange-rates"
+gem "ruby-bandwidth-iris"
group(:development) do
gem "pry-reload"
M config.dhall.sample => config.dhall.sample +10 -4
@@ 8,11 8,12 @@
port = 5347
},
sgx = "component2.localhost",
- creds = toMap {
- nick = "userid",
- username = "token",
- password = "secret"
+ creds = {
+ account = "00000",
+ username = "dashboard user",
+ password = "dashboard password"
},
+ bandwidth_site = "",
braintree = {
environment = "sandbox",
merchant_id = "",
@@ 24,4 25,9 @@
}
},
plans = ./plans.dhall
+ electrum = ./electrum.dhall,
+ oxr_app_id = "",
+ activation_amount = 15,
+ credit_card_url = \(jid: Text) -> \(customer_id: Text) ->
+ "https://pay.jmp.chat/${jid}/credit_cards?customer_id=${customer_id}"
}
A lib/backend_sgx.rb => lib/backend_sgx.rb +35 -0
@@ 0,0 1,35 @@
+# frozen_string_literal: true
+
+class BackendSgx
+ def initialize(jid=CONFIG[:sgx], creds=CONFIG[:creds])
+ @jid = jid
+ @creds = creds
+ end
+
+ def register!(customer_id, tel)
+ ibr = mkibr(:set, customer_id)
+ ibr.nick = @creds[:account]
+ ibr.username = @creds[:username]
+ ibr.password = @creds[:password]
+ ibr.phone = tel
+ IQ_MANAGER.write(ibr)
+ end
+
+ def registered?(customer_id)
+ IQ_MANAGER.write(mkibr(:get, customer_id)).catch { nil }.then do |result|
+ if result&.respond_to?(:registered?) && result&.registered?
+ result
+ else
+ false
+ end
+ end
+ end
+
+protected
+
+ def mkibr(type, customer_id)
+ ibr = IBR.new(type, @jid)
+ ibr.from = "customer_#{customer_id}@#{CONFIG[:component][:jid]}"
+ ibr
+ end
+end
A lib/bandwidth_tn_order.rb => lib/bandwidth_tn_order.rb +87 -0
@@ 0,0 1,87 @@
+# frozen_string_literal: true
+
+require "forwardable"
+require "ruby-bandwidth-iris"
+Faraday.default_adapter = :em_synchrony
+
+class BandwidthTNOrder
+ def self.get(id)
+ EM.promise_fiber do
+ self.for(BandwidthIris::Order.get_order_response(
+ # https://github.com/Bandwidth/ruby-bandwidth-iris/issues/44
+ BandwidthIris::Client.new,
+ id
+ ))
+ end
+ end
+
+ def self.create(tel, name: "sgx-jmp order #{tel}")
+ bw_tel = tel.sub(/^\+?1?/, "")
+ EM.promise_fiber do
+ Received.new(BandwidthIris::Order.create(
+ name: name,
+ site_id: CONFIG[:bandwidth_site],
+ existing_telephone_number_order_type: {
+ telephone_number_list: { telephone_number: [bw_tel] }
+ }
+ ))
+ end
+ end
+
+ def self.for(bandwidth_order)
+ const_get(bandwidth_order.order_status.capitalize).new(bandwidth_order)
+ rescue NameError
+ new(bandwidth_order)
+ end
+
+ extend Forwardable
+ def_delegators :@order, :id
+
+ def initialize(bandwidth_order)
+ @order = bandwidth_order
+ end
+
+ def status
+ @order[:order_status]&.downcase&.to_sym
+ end
+
+ def error_description
+ @order[:error_list]&.dig(:error, :description)
+ end
+
+ def poll
+ raise "Unknown order status: #{status}"
+ end
+
+ class Received < BandwidthTNOrder
+ def status
+ :received
+ end
+
+ def poll
+ EM.promise_timer(1).then do
+ BandwidthTNOrder.get(id).then(&:poll)
+ end
+ end
+ end
+
+ class Complete < BandwidthTNOrder
+ def status
+ :complete
+ end
+
+ def poll
+ EMPromise.resolve(self)
+ end
+ end
+
+ class Failed < BandwidthTNOrder
+ def status
+ :failed
+ end
+
+ def poll
+ raise "Order failed: #{id} #{error_description}"
+ end
+ end
+end
M lib/btc_sell_prices.rb => lib/btc_sell_prices.rb +3 -1
@@ 1,6 1,8 @@
# frozen_string_literal: true
require "em-http"
+require "em_promise"
+require "em-synchrony/em-http" # For aget vs get
require "money/bank/open_exchange_rates_bank"
require "nokogiri"
@@ 38,7 40,7 @@ protected
EM::HttpRequest.new(
"https://www.canadianbitcoins.com",
tls: { verify_peer: true }
- ).get
+ ).aget
end
def cad_to_usd
M lib/customer.rb => lib/customer.rb +58 -15
@@ 1,5 1,8 @@
# frozen_string_literal: true
+require "forwardable"
+
+require_relative "./ibr"
require_relative "./payment_methods"
require_relative "./plan"
@@ 22,7 25,11 @@ class Customer
end
end
+ extend Forwardable
+
attr_reader :customer_id, :balance
+ def_delegator :@plan, :name, :plan_name
+ def_delegators :@plan, :currency, :merchant_account
def initialize(
customer_id,
@@ 45,16 52,13 @@ class Customer
)
end
- def plan_name
- @plan.name
- end
-
- def currency
- @plan.currency
- end
-
- def merchant_account
- @plan.merchant_account
+ def bill_plan
+ EM.promise_fiber do
+ DB.transaction do
+ charge_for_plan
+ add_one_month_to_current_plan unless activate_plan_starting_now
+ end
+ end
end
def payment_methods
@@ 69,11 73,50 @@ class Customer
@plan && @expires_at > Time.now
end
+ def register!(tel)
+ BACKEND_SGX.register!(customer_id, tel)
+ end
+
def registered?
- ibr = IBR.new(:get, CONFIG[:sgx])
- ibr.from = "customer_#{@customer_id}@#{CONFIG[:component][:jid]}"
- IQ_MANAGER.write(ibr).catch { nil }.then do |result|
- result&.respond_to?(:registered?) && result&.registered?
- end
+ BACKEND_SGX.registered?(customer_id)
+ end
+
+protected
+
+ def charge_for_plan
+ params = [
+ @customer_id,
+ "#{@customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
+ -@plan.monthly_price
+ ]
+ DB.exec(<<~SQL, params)
+ INSERT INTO transactions
+ (customer_id, transaction_id, created_at, amount)
+ VALUES ($1, $2, LOCALTIMESTAMP, $3)
+ SQL
+ end
+
+ def activate_plan_starting_now
+ DB.exec(<<~SQL, [@customer_id, plan_name]).cmd_tuples.positive?
+ INSERT INTO plan_log
+ (customer_id, plan_name, date_range)
+ VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'))
+ ON CONFLICT DO NOTHING
+ SQL
+ end
+
+ def add_one_month_to_current_plan
+ DB.exec(<<~SQL, [@customer_id])
+ UPDATE plan_log SET date_range=range_merge(
+ date_range,
+ tsrange(
+ LOCALTIMESTAMP,
+ GREATEST(upper(date_range), LOCALTIMESTAMP) + '1 month'
+ )
+ )
+ WHERE
+ customer_id=$1 AND
+ date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')
+ SQL
end
end
M lib/electrum.rb => lib/electrum.rb +3 -2
@@ 1,9 1,10 @@
# frozen_string_literal: true
require "bigdecimal"
+require "em-http"
require "em_promise"
+require "em-synchrony/em-http" # For apost vs post
require "json"
-require "net/http"
require "securerandom"
class Electrum
@@ 69,7 70,7 @@ protected
EM::HttpRequest.new(
@rpc_uri,
tls: { verify_peer: true }
- ).post(
+ ).apost(
head: {
"Authorization" => [@rpc_username, @rpc_password],
"Content-Type" => "application/json"
M lib/em.rb => lib/em.rb +20 -0
@@ 12,4 12,24 @@ module EM
)
promise
end
+
+ def self.promise_fiber
+ promise = EMPromise.new
+ Fiber.new {
+ begin
+ promise.fulfill(yield)
+ rescue StandardError => e
+ promise.reject(e)
+ end
+ }.resume
+ promise
+ end
+
+ def self.promise_timer(timeout)
+ promise = EMPromise.new
+ EM.add_timer(timeout) do
+ promise.fulfill(nil)
+ end
+ promise
+ end
end
A lib/oob.rb => lib/oob.rb +54 -0
@@ 0,0 1,54 @@
+# frozen_string_literal: true
+
+require "blather"
+
+class OOB < Blather::XMPPNode
+ register :oob, "jabber:x:oob"
+
+ def self.new(url=nil, desc: nil)
+ new_node = super :x
+
+ case url
+ when Nokogiri::XML::Node
+ new_node.inherit url
+ else
+ new_node.url = url if url
+ new_node.desc = desc if desc
+ end
+
+ new_node
+ end
+
+ def self.find_or_create(parent)
+ if (found_x = parent.find("ns:x", ns: registered_ns).first)
+ x = new(found_x)
+ found_x.remove
+ else
+ x = new
+ end
+ parent << x
+ x
+ end
+
+ def url
+ find("ns:url", ns: self.class.registered_ns).first&.content
+ end
+
+ def url=(u)
+ remove_children :url
+ i = Niceogiri::XML::Node.new(:url, document, namespace)
+ i.content = u
+ self << i
+ end
+
+ def desc
+ find("ns:desc", ns: self.class.registered_ns).first&.content
+ end
+
+ def desc=(d)
+ remove_children :desc
+ i = Niceogiri::XML::Node.new(:desc, document, namespace)
+ i.content = d
+ self << i
+ end
+end
M lib/payment_methods.rb => lib/payment_methods.rb +8 -2
@@ 19,7 19,11 @@ class PaymentMethods
end
def default_payment_method
- @methods.index(&:default?).to_s
+ @methods.find(&:default?)
+ end
+
+ def default_payment_method_index
+ @methods.index(&:default?)&.to_s
end
def to_options
@@ 37,12 41,14 @@ class PaymentMethods
type: "list-single",
label: "Credit card to pay with",
required: true,
- value: default_payment_method,
+ value: default_payment_method_index,
options: to_options
}.merge(kwargs)
end
class Empty
+ def default_payment_method; end
+
def to_list_single(*)
raise "No payment methods available"
end
M lib/plan.rb => lib/plan.rb +4 -0
@@ 20,6 20,10 @@ class Plan
@plan[:currency]
end
+ def monthly_price
+ BigDecimal.new(@plan[:monthly_price]) / 1000
+ end
+
def merchant_account
CONFIG[:braintree][:merchant_accounts].fetch(currency) do
raise "No merchant account for this currency"
M lib/registration.rb => lib/registration.rb +102 -13
@@ 1,12 1,14 @@
# frozen_string_literal: true
+require_relative "./oob"
+
class Registration
def self.for(iq, customer, web_register_manager)
raise "TODO" if customer&.active?
EMPromise.resolve(customer&.registered?).then do |registered|
if registered
- Registered.new(iq, result.phone)
+ Registered.new(iq, registered.phone)
else
web_register_manager.choose_tel(iq).then do |(riq, tel)|
Activation.for(riq, customer, tel)
@@ 63,7 65,7 @@ class Registration
},
{
value: "credit_card",
- label: "Credit Card"
+ label: "Credit Card ($#{CONFIG[:activation_amount]})"
},
{
value: "code",
@@ 103,27 105,27 @@ class Registration
end
module Payment
+ def self.kinds
+ @kinds ||= {}
+ end
+
def self.for(iq, customer, tel)
- case iq.form.field("activation_method")&.value&.to_s
- when "bitcoin"
- Bitcoin.new(iq, customer, tel)
- when "credit_card"
- raise "TODO"
- when "code"
- raise "TODO"
- else
+ plan_name = iq.form.field("plan_name").value.to_s
+ customer = customer.with_plan(plan_name)
+ kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
raise "Invalid activation method"
- end
+ }.call(iq, customer, tel)
end
class Bitcoin
+ Payment.kinds[:bitcoin] = method(:new)
+
def initialize(iq, customer, tel)
@reply = iq.reply
reply.note_type = :info
reply.status = :completed
- plan_name = iq.form.field("plan_name").value.to_s
- @customer = customer.with_plan(plan_name)
+ @customer = customer
@customer_id = customer.customer_id
@tel = tel
@addr = ELECTRUM.createnewaddress
@@ 165,5 167,92 @@ class Registration
end
end
end
+
+ class CreditCard
+ Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }
+
+ def self.for(iq, customer, tel)
+ customer.payment_methods.then do |payment_methods|
+ if (method = payment_methods.default_payment_method)
+ Activate.new(iq, customer, method, tel)
+ else
+ new(iq, customer, tel)
+ end
+ end
+ end
+
+ def initialize(iq, customer, tel)
+ @customer = customer
+ @tel = tel
+
+ @reply = iq.reply
+ @reply.allowed_actions = [:next]
+ @reply.note_type = :info
+ @reply.note_text = "#{oob.desc}: #{oob.url}"
+ end
+
+ attr_reader :reply
+
+ def oob
+ oob = OOB.find_or_create(@reply.command)
+ oob.url = CONFIG[:credit_card_url].call(
+ @reply.to.stripped.to_s,
+ @customer.customer_id
+ )
+ oob.desc = "Add credit card, then return here and choose next"
+ oob
+ end
+
+ def write
+ COMMAND_MANAGER.write(@reply).then do |riq|
+ CreditCard.for(riq, @customer, @tel)
+ end
+ end
+
+ class Activate
+ def initialize(iq, customer, payment_method, tel)
+ @iq = iq
+ @customer = customer
+ @payment_method = payment_method
+ @tel = tel
+ end
+
+ def write
+ Transaction.sale(
+ @customer.merchant_account,
+ @payment_method,
+ CONFIG[:activation_amount]
+ ).then(&:insert).then {
+ @customer.bill_plan
+ }.then do
+ Finish.new(@iq, @customer, @tel).write
+ end
+ end
+ end
+ end
+ end
+
+ class Finish
+ def initialize(iq, customer, tel)
+ @reply = iq.reply
+ @reply.status = :completed
+ @reply.note_type = :info
+ @reply.note_text = "Your JMP account has been activated as #{tel}"
+ @customer = customer
+ @tel = tel
+ end
+
+ def write
+ BandwidthTNOrder.create(@tel).then(&:poll).then(
+ ->(_) { @customer.register!(@tel).then { BLATHER << @reply } },
+ lambda do |_|
+ @reply.note_type = :error
+ @reply.note_text =
+ "The JMP number #{@tel} is no longer available, " \
+ "please visit https://jmp.chat and choose another."
+ BLATHER << @reply
+ end
+ )
+ end
end
end
M sgx_jmp.rb => sgx_jmp.rb +29 -10
@@ 2,30 2,45 @@
require "pg/em"
require "bigdecimal"
+require "blather/client/dsl" # Require this first to not auto-include
require "blather/client"
require "braintree"
require "dhall"
require "em-hiredis"
require "em_promise"
+require "ruby-bandwidth-iris"
+CONFIG =
+ Dhall::Coder
+ .new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
+ .load(ARGV[0], transform_keys: ->(k) { k&.to_sym })
+
+singleton_class.class_eval do
+ include Blather::DSL
+ Blather::DSL.append_features(self)
+end
+
+require_relative "lib/backend_sgx"
+require_relative "lib/bandwidth_tn_order"
require_relative "lib/btc_sell_prices"
require_relative "lib/buy_account_credit_form"
require_relative "lib/customer"
require_relative "lib/electrum"
require_relative "lib/em"
-require_relative "lib/existing_registration"
require_relative "lib/payment_methods"
require_relative "lib/registration"
require_relative "lib/transaction"
require_relative "lib/web_register_manager"
-CONFIG =
- Dhall::Coder
- .new(safe: Dhall::Coder::JSON_LIKE + [Symbol])
- .load(ARGV[0], transform_keys: ->(k) { k&.to_sym })
-
ELECTRUM = Electrum.new(**CONFIG[:electrum])
+Faraday.default_adapter = :em_synchrony
+BandwidthIris::Client.global_options = {
+ account_id: CONFIG[:creds][:account],
+ username: CONFIG[:creds][:username],
+ password: CONFIG[:creds][:password]
+}
+
# Braintree is not async, so wrap in EM.defer for now
class AsyncBraintree
def initialize(environment:, merchant_id:, public_key:, private_key:, **)
@@ 62,11 77,11 @@ class AsyncBraintree
end
BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])
-
-Blather::DSL.append_features(self.class)
+BACKEND_SGX = BackendSgx.new
def panic(e)
- warn "Error raised during event loop: #{e.message}"
+ m = e.respond_to?(:message) ? e.message : e
+ warn "Error raised during event loop: #{e.class}: #{m}"
warn e.backtrace if e.respond_to?(:backtrace)
exit 1
end
@@ 130,7 145,11 @@ class SessionManager
def fulfill(stanza)
id = "#{stanza.from.stripped}/#{stanza.public_send(@id_msg)}"
- @sessions.delete(id)&.fulfill(stanza)
+ if stanza.error?
+ @sessions.delete(id)&.reject(stanza)
+ else
+ @sessions.delete(id)&.fulfill(stanza)
+ end
end
end
A test/test_backend_sgx.rb => test/test_backend_sgx.rb +54 -0
@@ 0,0 1,54 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "backend_sgx"
+
+BackendSgx::IQ_MANAGER = Minitest::Mock.new
+
+class BackendSgxTest < Minitest::Test
+ def setup
+ @sgx = BackendSgx.new
+ end
+
+ def test_registered
+ BackendSgx::IQ_MANAGER.expect(
+ :write,
+ EMPromise.resolve(IBR.new.tap { |ibr| ibr.registered = true }),
+ [Matching.new do |ibr|
+ assert_equal :get, ibr.type
+ assert_equal "customer_test@component", ibr.from.to_s
+ end]
+ )
+ assert @sgx.registered?("test").sync
+ end
+ em :test_registered
+
+ def test_registered_not_registered
+ BackendSgx::IQ_MANAGER.expect(
+ :write,
+ EMPromise.resolve(IBR.new.tap { |ibr| ibr.registered = false }),
+ [Matching.new do |ibr|
+ assert_equal :get, ibr.type
+ assert_equal "customer_test@component", ibr.from.to_s
+ end]
+ )
+ refute @sgx.registered?("test").sync
+ end
+ em :test_registered_not_registered
+
+ def test_register!
+ BackendSgx::IQ_MANAGER.expect(
+ :write,
+ EMPromise.resolve(OpenStruct.new(error?: false)),
+ [Matching.new do |ibr|
+ assert_equal "customer_test@component", ibr.from.to_s
+ assert_equal "test_bw_account", ibr.nick
+ assert_equal "test_bw_user", ibr.username
+ assert_equal "test_bw_password", ibr.password
+ assert_equal "+15555550000", ibr.phone
+ end]
+ )
+ @sgx.register!("test", "+15555550000")
+ BackendSgx::IQ_MANAGER.verify
+ end
+end
A test/test_bandwidth_tn_order.rb => test/test_bandwidth_tn_order.rb +89 -0
@@ 0,0 1,89 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "bandwidth_tn_order"
+
+class BandwidthTNOrderTest < Minitest::Test
+ def test_for_received
+ order = BandwidthTNOrder.for(BandwidthIris::Order.new(
+ order_status: "RECEIVED"
+ ))
+ assert_kind_of BandwidthTNOrder::Received, order
+ end
+
+ def test_for_complete
+ order = BandwidthTNOrder.for(BandwidthIris::Order.new(
+ order_status: "COMPLETE"
+ ))
+ assert_kind_of BandwidthTNOrder::Complete, order
+ end
+
+ def test_for_failed
+ order = BandwidthTNOrder.for(BandwidthIris::Order.new(
+ order_status: "FAILED"
+ ))
+ assert_kind_of BandwidthTNOrder::Failed, order
+ end
+
+ def test_for_unknown
+ order = BandwidthTNOrder.for(BandwidthIris::Order.new(
+ order_status: "randOmgarBagE"
+ ))
+ assert_kind_of BandwidthTNOrder, order
+ assert_equal :randomgarbage, order.status
+ end
+
+ def test_poll
+ order = BandwidthTNOrder.new(BandwidthIris::Order.new)
+ assert_raises { order.poll.sync }
+ end
+ em :test_poll
+
+ class TestReceived < Minitest::Test
+ def setup
+ @order = BandwidthTNOrder::Received.new(
+ BandwidthIris::Order.new(id: "oid")
+ )
+ end
+
+ def test_poll
+ req = stub_request(
+ :get,
+ "https://dashboard.bandwidth.com/v1.0/accounts//orders/oid"
+ ).to_return(status: 200, body: <<~RESPONSE)
+ <OrderResponse>
+ <OrderStatus>COMPLETE</OrderStatus>
+ </OrderResponse>
+ RESPONSE
+ new_order = PromiseMock.new
+ new_order.expect(:poll, nil)
+ @order.poll.sync
+ assert_requested req
+ end
+ em :test_poll
+ end
+
+ class TestComplete < Minitest::Test
+ def setup
+ @order = BandwidthTNOrder::Complete.new(BandwidthIris::Order.new)
+ end
+
+ def test_poll
+ assert_equal @order, @order.poll.sync
+ end
+ em :test_poll
+ end
+
+ class TestFailed < Minitest::Test
+ def setup
+ @order = BandwidthTNOrder::Failed.new(
+ BandwidthIris::Order.new(id: "oid")
+ )
+ end
+
+ def test_poll
+ assert_raises { @order.poll.sync }
+ end
+ em :test_poll
+ end
+end
M test/test_buy_account_credit_form.rb => test/test_buy_account_credit_form.rb +0 -1
@@ 42,7 42,6 @@ class BuyAccountCreditFormTest < Minitest::Test
type: "list-single",
var: "payment_method",
label: "Credit card to pay with",
- value: "",
required: true,
options: [{ label: "Test 1234", value: "0" }]
),
M test/test_customer.rb => test/test_customer.rb +55 -0
@@ 47,4 47,59 @@ class CustomerTest < Minitest::Test
assert_equal BigDecimal.new(0), customer.balance
end
em :test_for_customer_id_not_found
+
+ def test_bill_plan_activate
+ Customer::DB.expect(:transaction, nil) do |&block|
+ block.call
+ true
+ end
+ Customer::DB.expect(
+ :exec,
+ nil,
+ [
+ String,
+ Matching.new do |params|
+ params[0] == "test" &&
+ params[1].is_a?(String) &&
+ BigDecimal.new(-1) == params[2]
+ end
+ ]
+ )
+ Customer::DB.expect(
+ :exec,
+ OpenStruct.new(cmd_tuples: 1),
+ [String, ["test", "test_usd"]]
+ )
+ Customer.new("test", plan_name: "test_usd").bill_plan.sync
+ Customer::DB.verify
+ end
+ em :test_bill_plan_activate
+
+ def test_bill_plan_update
+ Customer::DB.expect(:transaction, nil) do |&block|
+ block.call
+ true
+ end
+ Customer::DB.expect(
+ :exec,
+ nil,
+ [
+ String,
+ Matching.new do |params|
+ params[0] == "test" &&
+ params[1].is_a?(String) &&
+ BigDecimal.new(-1) == params[2]
+ end
+ ]
+ )
+ Customer::DB.expect(
+ :exec,
+ OpenStruct.new(cmd_tuples: 0),
+ [String, ["test", "test_usd"]]
+ )
+ Customer::DB.expect(:exec, nil, [String, ["test"]])
+ Customer.new("test", plan_name: "test_usd").bill_plan.sync
+ Customer::DB.verify
+ end
+ em :test_bill_plan_update
end
M test/test_helper.rb => test/test_helper.rb +30 -2
@@ 32,16 32,24 @@ rescue LoadError
nil
end
+require "backend_sgx"
+
CONFIG = {
sgx: "sgx",
component: {
jid: "component"
},
+ creds: {
+ account: "test_bw_account",
+ username: "test_bw_user",
+ password: "test_bw_password"
+ },
activation_amount: 1,
plans: [
{
name: "test_usd",
- currency: :USD
+ currency: :USD,
+ monthly_price: 1000
},
{
name: "test_bad_currency",
@@ 52,9 60,12 @@ CONFIG = {
merchant_accounts: {
USD: "merchant_usd"
}
- }
+ },
+ credit_card_url: ->(*) { "http://creditcard.example.com" }
}.freeze
+BACKEND_SGX = Minitest::Mock.new(BackendSgx.new)
+
BLATHER = Class.new {
def <<(*); end
}.new.freeze
@@ 69,6 80,23 @@ class Matching
end
end
+class PromiseMock < Minitest::Mock
+ def then
+ yield self
+ end
+end
+
+module EventMachine
+ class << self
+ # Patch EM.add_timer to be instant in tests
+ alias old_add_timer add_timer
+ def add_timer(*args, &block)
+ args[0] = 0
+ old_add_timer(*args, &block)
+ end
+ end
+end
+
module Minitest
class Test
def self.property(m, &block)
A test/test_oob.rb => test/test_oob.rb +49 -0
@@ 0,0 1,49 @@
+# frozen_string_literal: true
+
+require "oob"
+
+class OOBTest < Minitest::Test
+ def test_new
+ oob = OOB.new
+ assert_kind_of OOB, oob
+ assert_nil oob.url
+ assert_nil oob.desc
+ end
+
+ def test_new_with_node
+ assert_kind_of OOB, OOB.new(Blather::XMPPNode.new)
+ end
+
+ property(:new_with_attrs) { [string(:alnum), string] }
+ def new_with_attrs(u, d)
+ oob = OOB.new(u, desc: d)
+ assert_kind_of OOB, oob
+ assert_equal u, oob.url
+ assert_equal d, oob.desc
+ end
+
+ def test_find_or_create_not_found
+ assert_kind_of OOB, OOB.find_or_create(Blather::XMPPNode.new)
+ end
+
+ def test_find_or_create_found
+ parent = Blather::XMPPNode.new
+ parent << OOB.new("http://example.com")
+ assert_kind_of OOB, OOB.find_or_create(parent)
+ assert_equal "http://example.com", OOB.find_or_create(parent).url
+ end
+
+ property(:url) { string(:alnum) }
+ def url(u)
+ oob = OOB.new
+ oob.url = u
+ assert_equal u, oob.url
+ end
+
+ property(:desc) { string }
+ def desc(d)
+ oob = OOB.new
+ oob.desc = d
+ assert_equal d, oob.desc
+ end
+end
M test/test_payment_methods.rb => test/test_payment_methods.rb +10 -2
@@ 27,7 27,15 @@ class PaymentMethodsTest < Minitest::Test
OpenStruct.new(card_type: "Test", last_4: "1234"),
OpenStruct.new(card_type: "Test", last_4: "1234", default?: true)
])
- assert_equal "1", methods.default_payment_method
+ assert_equal methods.fetch(1), methods.default_payment_method
+ end
+
+ def test_default_payment_method_index
+ methods = PaymentMethods.new([
+ OpenStruct.new(card_type: "Test", last_4: "1234"),
+ OpenStruct.new(card_type: "Test", last_4: "1234", default?: true)
+ ])
+ assert_equal "1", methods.default_payment_method_index
end
def test_to_options
@@ 52,7 60,7 @@ class PaymentMethodsTest < Minitest::Test
type: "list-single",
label: "Credit card to pay with",
required: true,
- value: "",
+ value: nil,
options: [
{ value: "0", label: "Test 1234" }
]
M test/test_registration.rb => test/test_registration.rb +197 -11
@@ 4,8 4,6 @@ require "test_helper"
require "registration"
class RegistrationTest < Minitest::Test
- Customer::IQ_MANAGER = Minitest::Mock.new
-
def test_for_activated
skip "Registration#for activated not implemented yet"
iq = Blather::Stanza::Iq::Command.new
@@ 14,10 12,10 @@ class RegistrationTest < Minitest::Test
em :test_for_activated
def test_for_not_activated_with_customer_id
- Customer::IQ_MANAGER.expect(
- :write,
+ BACKEND_SGX.expect(
+ :registered?,
EMPromise.resolve(nil),
- [Blather::Stanza::Iq]
+ ["test"]
)
web_manager = WebRegisterManager.new
web_manager["test@example.com"] = "+15555550000"
@@ 61,6 59,7 @@ class RegistrationTest < Minitest::Test
end
class PaymentTest < Minitest::Test
+ Customer::BRAINTREE = Minitest::Mock.new
Registration::Payment::Bitcoin::ELECTRUM = Minitest::Mock.new
def test_for_bitcoin
@@ 79,15 78,30 @@ class RegistrationTest < Minitest::Test
end
def test_for_credit_card
- skip "CreditCard not implemented yet"
+ braintree_customer = Minitest::Mock.new
+ Customer::BRAINTREE.expect(
+ :customer,
+ braintree_customer
+ )
+ braintree_customer.expect(
+ :find,
+ EMPromise.resolve(OpenStruct.new(payment_methods: [])),
+ ["test"]
+ )
iq = Blather::Stanza::Iq::Command.new
+ iq.from = "test@example.com"
iq.form.fields = [
{ var: "activation_method", value: "credit_card" },
{ var: "plan_name", value: "test_usd" }
]
- result = Registration::Payment.for(iq, "test", "+15555550000")
+ result = Registration::Payment.for(
+ iq,
+ Customer.new("test"),
+ "+15555550000"
+ ).sync
assert_kind_of Registration::Payment::CreditCard, result
end
+ em :test_for_credit_card
def test_for_code
skip "Code not implemented yet"
@@ 112,12 126,9 @@ class RegistrationTest < Minitest::Test
EMPromise.resolve("testaddr")
)
iq = Blather::Stanza::Iq::Command.new
- iq.form.fields = [
- { var: "plan_name", value: "test_usd" }
- ]
@bitcoin = Registration::Payment::Bitcoin.new(
iq,
- Customer.new("test"),
+ Customer.new("test", plan_name: "test_usd"),
"+15555550000"
)
end
@@ 150,5 161,180 @@ class RegistrationTest < Minitest::Test
end
em :test_write
end
+
+ class CreditCardTest < Minitest::Test
+ def setup
+ @iq = Blather::Stanza::Iq::Command.new
+ @iq.from = "test@example.com"
+ @credit_card = Registration::Payment::CreditCard.new(
+ @iq,
+ Customer.new("test"),
+ "+15555550000"
+ )
+ end
+
+ def test_for
+ customer = Minitest::Mock.new(Customer.new("test"))
+ customer.expect(
+ :payment_methods,
+ EMPromise.resolve(OpenStruct.new(default_payment_method: :test))
+ )
+ assert_kind_of(
+ Registration::Payment::CreditCard::Activate,
+ Registration::Payment::CreditCard.for(
+ @iq,
+ customer,
+ "+15555550000"
+ ).sync
+ )
+ end
+ em :test_for
+
+ def test_reply
+ assert_equal [:execute, :next], @credit_card.reply.allowed_actions
+ assert_equal(
+ "Add credit card, then return here and choose next: " \
+ "http://creditcard.example.com",
+ @credit_card.reply.note.content
+ )
+ end
+ end
+
+ class ActivateTest < Minitest::Test
+ Registration::Payment::CreditCard::Activate::Finish =
+ Minitest::Mock.new
+ Registration::Payment::CreditCard::Activate::Transaction =
+ Minitest::Mock.new
+
+ def test_write
+ transaction = PromiseMock.new
+ transaction.expect(
+ :insert,
+ EMPromise.resolve(nil)
+ )
+ Registration::Payment::CreditCard::Activate::Transaction.expect(
+ :sale,
+ transaction,
+ [
+ "merchant_usd",
+ :test_default_method,
+ CONFIG[:activation_amount]
+ ]
+ )
+ iq = Blather::Stanza::Iq::Command.new
+ customer = Minitest::Mock.new(
+ Customer.new("test", plan_name: "test_usd")
+ )
+ customer.expect(:bill_plan, nil)
+ Registration::Payment::CreditCard::Activate::Finish.expect(
+ :new,
+ OpenStruct.new(write: nil),
+ [Blather::Stanza::Iq, customer, "+15555550000"]
+ )
+ Registration::Payment::CreditCard::Activate.new(
+ iq,
+ customer,
+ :test_default_method,
+ "+15555550000"
+ ).write.sync
+ Registration::Payment::CreditCard::Activate::Transaction.verify
+ transaction.verify
+ customer.verify
+ end
+ em :test_write
+ end
+ end
+
+ class FinishTest < Minitest::Test
+ Registration::Finish::BLATHER = Minitest::Mock.new
+
+ def setup
+ @finish = Registration::Finish.new(
+ Blather::Stanza::Iq::Command.new,
+ Customer.new("test"),
+ "+15555550000"
+ )
+ end
+
+ def test_write
+ 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>
+ </OrderResponse>
+ RESPONSE
+ BACKEND_SGX.expect(
+ :register!,
+ EMPromise.resolve(OpenStruct.new(error?: false)),
+ ["test", "+15555550000"]
+ )
+ Registration::Finish::BLATHER.expect(
+ :<<,
+ nil,
+ [Matching.new do |reply|
+ assert_equal :completed, reply.status
+ assert_equal :info, reply.note_type
+ assert_equal(
+ "Your JMP account has been activated as +15555550000",
+ reply.note.content
+ )
+ end]
+ )
+ @finish.write.sync
+ assert_requested create_order
+ BACKEND_SGX.verify
+ Registration::Finish::BLATHER.verify
+ end
+ em :test_write
+
+ def test_write_tn_fail
+ 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>FAILED</OrderStatus>
+ </OrderResponse>
+ RESPONSE
+ Registration::Finish::BLATHER.expect(
+ :<<,
+ nil,
+ [Matching.new do |reply|
+ assert_equal :completed, reply.status
+ assert_equal :error, reply.note_type
+ assert_equal(
+ "The JMP number +15555550000 is no longer available, " \
+ "please visit https://jmp.chat and choose another.",
+ reply.note.content
+ )
+ end]
+ )
+ @finish.write.sync
+ assert_requested create_order
+ Registration::Finish::BLATHER.verify
+ end
+ em :test_write_tn_fail
end
end