A .builds/debian-stable.yml => .builds/debian-stable.yml +22 -0
@@ 0,0 1,22 @@
+image: debian/stable
+sources:
+- https://git.sr.ht/~singpolyma/sgx-jmp
+packages:
+- ruby
+- ruby-dev
+- bundler
+- libxml2-dev
+- libpq-dev
+- rubocop
+environment:
+ LANG: C.UTF-8
+tasks:
+- dependencies: |
+ cd sgx-jmp
+ bundle install --without=development --path=.gems
+- lint: |
+ cd sgx-jmp
+ rubocop
+- test: |
+ cd sgx-jmp
+ RANTLY_COUNT=100 bundle exec rake test
M .rubocop.yml => .rubocop.yml +4 -0
@@ 10,6 10,10 @@ Metrics/MethodLength:
Exclude:
- test/*
+Metrics/AbcSize:
+ Exclude:
+ - test/*
+
Style/Tab:
Enabled: false
M Gemfile => Gemfile +2 -0
@@ 6,9 6,11 @@ gem "blather", git: "https://github.com/singpolyma/blather.git", branch: "ergono
gem "braintree"
gem "dhall"
gem "em-hiredis"
+gem "em-http-request"
gem "em-pg-client", git: "https://github.com/royaltm/ruby-em-pg-client"
gem "em_promise.rb"
gem "eventmachine"
+gem "money-open-exchange-rates"
group(:development) do
gem "pry-reload"
A Rakefile => Rakefile +22 -0
@@ 0,0 1,22 @@
+# frozen_string_literal: true
+
+require "rake/testtask"
+require "rubocop/rake_task"
+
+Rake::TestTask.new(:test) do |t|
+ ENV["RANTLY_VERBOSE"] = "0" unless ENV["RANTLY_VERBOSE"]
+ ENV["RANTLY_COUNT"] = "10" unless ENV["RANTLY_COUNT"]
+
+ t.libs << "test"
+ t.libs << "lib"
+ t.test_files = FileList["test/**/test_*.rb"]
+ t.warning = false
+end
+
+RuboCop::RakeTask.new(:lint)
+
+task :entr do
+ sh "sh", "-c", "git ls-files | entr -s 'rubocop && rake test'"
+end
+
+task default: :test
A lib/btc_sell_prices.rb => lib/btc_sell_prices.rb +57 -0
@@ 0,0 1,57 @@
+# frozen_string_literal: true
+
+require "em-http"
+require "money/bank/open_exchange_rates_bank"
+require "nokogiri"
+
+require_relative "em"
+
+class BTCSellPrices
+ def initialize(redis, oxr_app_id)
+ @redis = redis
+ @oxr = Money::Bank::OpenExchangeRatesBank.new(
+ Money::RatesStore::Memory.new
+ )
+ @oxr.app_id = oxr_app_id
+ end
+
+ def cad
+ fetch_canadianbitcoins.then do |http|
+ canadianbitcoins = Nokogiri::HTML.parse(http.response)
+
+ bitcoin_row = canadianbitcoins.at("#ticker > table > tbody > tr")
+ raise "Bitcoin row has moved" unless bitcoin_row.at("td").text == "Bitcoin"
+
+ BigDecimal.new(
+ bitcoin_row.at("td:nth-of-type(3)").text.match(/^\$(\d+\.\d+)/)[1]
+ )
+ end
+ end
+
+ def usd
+ EMPromise.all([cad, cad_to_usd]).then { |(a, b)| a * b }
+ end
+
+protected
+
+ def fetch_canadianbitcoins
+ EM::HttpRequest.new(
+ "https://www.canadianbitcoins.com",
+ tls: { verify_peer: true }
+ ).get
+ end
+
+ def cad_to_usd
+ @redis.get("cad_to_usd").then do |rate|
+ next rate.to_f if rate
+
+ EM.promise_defer {
+ # OXR gem is not async, so defer to threadpool
+ oxr.update_rates
+ oxr.get_rate("CAD", "USD")
+ }.then do |orate|
+ @redis.set("cad_to_usd", orate, ex: 60 * 60).then { orate }
+ end
+ end
+ end
+end
M lib/customer.rb => lib/customer.rb +38 -3
@@ 13,7 13,7 @@ class Customer
def self.for_customer_id(customer_id)
result = DB.query_defer(<<~SQL, [customer_id])
- SELECT COALESCE(balance,0) AS balance, plan_name
+ SELECT COALESCE(balance,0) AS balance, plan_name, expires_at
FROM customer_plans LEFT JOIN balances USING (customer_id)
WHERE customer_id=$1 LIMIT 1
SQL
@@ 22,14 22,37 @@ class Customer
end
end
- attr_reader :balance
+ attr_reader :customer_id, :balance
- def initialize(customer_id, plan_name: nil, balance: BigDecimal.new(0))
+ def initialize(
+ customer_id,
+ plan_name: nil,
+ expires_at: Time.now,
+ balance: BigDecimal.new(0)
+ )
@plan = plan_name && Plan.for(plan_name)
+ @expires_at = expires_at
@customer_id = customer_id
@balance = balance
end
+ def with_plan(plan_name)
+ self.class.new(
+ @customer_id,
+ balance: @balance,
+ expires_at: @expires_at,
+ plan_name: plan_name
+ )
+ end
+
+ def plan_name
+ @plan.name
+ end
+
+ def currency
+ @plan.currency
+ end
+
def merchant_account
@plan.merchant_account
end
@@ 41,4 64,16 @@ class Customer
.find(@customer_id)
.then(PaymentMethods.method(:for_braintree_customer))
end
+
+ def active?
+ @plan && @expires_at > Time.now
+ 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
+ end
end
A lib/electrum.rb => lib/electrum.rb +80 -0
@@ 0,0 1,80 @@
+# frozen_string_literal: true
+
+require "bigdecimal"
+require "em_promise"
+require "json"
+require "net/http"
+require "securerandom"
+
+class Electrum
+ def initialize(rpc_uri:, rpc_username:, rpc_password:)
+ @rpc_uri = URI(rpc_uri)
+ @rpc_username = rpc_username
+ @rpc_password = rpc_password
+ end
+
+ def createnewaddress
+ rpc_call(:createnewaddress, {}).then { |r| r["result"] }
+ end
+
+ def getaddresshistory(address)
+ rpc_call(:getaddresshistory, address: address).then { |r| r["result"] }
+ end
+
+ def gettransaction(tx_hash)
+ rpc_call(:gettransaction, txid: tx_hash).then { |tx|
+ rpc_call(:deserialize, [tx["result"]])
+ }.then do |tx|
+ Transaction.new(self, tx_hash, tx["result"])
+ end
+ end
+
+ def get_tx_status(tx_hash)
+ rpc_call(:get_tx_status, txid: tx_hash).then { |r| r["result"] }
+ end
+
+ class Transaction
+ def initialize(electrum, tx_hash, tx)
+ @electrum = electrum
+ @tx_hash = tx_hash
+ @tx = tx
+ end
+
+ def confirmations
+ @electrum.get_tx_status(@tx_hash).then { |r| r["confirmations"] }
+ end
+
+ def amount_for(*addresses)
+ BigDecimal.new(
+ @tx["outputs"]
+ .select { |o| addresses.include?(o["address"]) }
+ .map { |o| o["value_sats"] }
+ .sum
+ ) * 0.00000001
+ end
+ end
+
+protected
+
+ def rpc_call(method, params)
+ post_json(
+ jsonrpc: "2.0",
+ id: SecureRandom.hex,
+ method: method.to_s,
+ params: params
+ ).then { |res| JSON.parse(res.response) }
+ end
+
+ def post_json(data)
+ EM::HttpRequest.new(
+ @rpc_uri,
+ tls: { verify_peer: true }
+ ).post(
+ head: {
+ "Authorization" => [@rpc_username, @rpc_password],
+ "Content-Type" => "application/json"
+ },
+ body: data.to_json
+ )
+ end
+end
M lib/plan.rb => lib/plan.rb +4 -0
@@ 12,6 12,10 @@ class Plan
@plan = plan
end
+ def name
+ @plan[:name]
+ end
+
def currency
@plan[:currency]
end
A lib/registration.rb => lib/registration.rb +169 -0
@@ 0,0 1,169 @@
+# frozen_string_literal: true
+
+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)
+ else
+ web_register_manager.choose_tel(iq).then do |(riq, tel)|
+ Activation.for(riq, customer, tel)
+ end
+ end
+ end
+ end
+
+ class Registered
+ def initialize(iq, tel)
+ @reply = iq.reply
+ @reply.status = :completed
+ @tel = tel
+ end
+
+ def write
+ @reply.note_type = :error
+ @reply.note_text = <<~NOTE
+ You are already registered with JMP number #{@tel}
+ NOTE
+ BLATHER << @reply
+ nil
+ end
+ end
+
+ class Activation
+ def self.for(iq, customer, tel)
+ return EMPromise.resolve(new(iq, customer, tel)) if customer
+
+ # Create customer_id
+ raise "TODO"
+ end
+
+ def initialize(iq, customer, tel)
+ @reply = iq.reply
+ reply.allowed_actions = [:next]
+
+ @customer = customer
+ @tel = tel
+ end
+
+ attr_reader :reply, :customer, :tel
+
+ FORM_FIELDS = [
+ {
+ var: "activation_method",
+ type: "list-single",
+ label: "Activate using",
+ required: true,
+ options: [
+ {
+ value: "bitcoin",
+ label: "Bitcoin"
+ },
+ {
+ value: "credit_card",
+ label: "Credit Card"
+ },
+ {
+ value: "code",
+ label: "Referral or Activation Code"
+ }
+ ]
+ },
+ {
+ var: "plan_name",
+ type: "list-single",
+ label: "What currency should your account balance be in?",
+ required: true,
+ options: [
+ {
+ value: "cad_beta_unlimited-v20210223",
+ label: "Canadian Dollars"
+ },
+ {
+ value: "usd_beta_unlimited-v20210223",
+ label: "United States Dollars"
+ }
+ ]
+ }
+ ].freeze
+
+ def write
+ form = reply.form
+ form.type = :form
+ form.title = "Activate JMP"
+ form.instructions = "Going to activate #{tel} (TODO RATE CTR)"
+ form.fields = FORM_FIELDS
+
+ COMMAND_MANAGER.write(reply).then { |iq|
+ Payment.for(iq, customer, tel)
+ }.then(&:write)
+ end
+ end
+
+ module Payment
+ 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
+ raise "Invalid activation method"
+ end
+ end
+
+ class Bitcoin
+ 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_id = customer.customer_id
+ @tel = tel
+ @addr = ELECTRUM.createnewaddress
+ end
+
+ attr_reader :reply, :customer_id, :tel
+
+ def save
+ EMPromise.all([
+ REDIS.mset(
+ "pending_tel_for-#{customer_id}", tel,
+ "pending_plan_for-#{customer_id}", @customer.plan_name
+ ),
+ @addr.then do |addr|
+ REDIS.sadd("jmp_customer_btc_addresses-#{customer_id}", addr)
+ end
+ ])
+ end
+
+ def note_text(amount, addr)
+ <<~NOTE
+ Activate your account by sending at least #{'%.6f' % amount} BTC to
+ #{addr}
+
+ You will receive a notification when your payment is complete.
+ NOTE
+ end
+
+ def write
+ EMPromise.all([
+ @addr,
+ save,
+ BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
+ ]).then do |(addr, _, rate)|
+ min = CONFIG[:activation_amount] / rate
+ reply.note_text = note_text(min, addr)
+ BLATHER << reply
+ nil
+ end
+ end
+ end
+ end
+end
A lib/web_register_manager.rb => lib/web_register_manager.rb +35 -0
@@ 0,0 1,35 @@
+# frozen_string_literal: true
+
+class WebRegisterManager
+ def initialize
+ @tel_map = Hash.new { ChooseTel.new }
+ end
+
+ def []=(jid, tel)
+ @tel_map[jid.to_s] = HaveTel.new(tel)
+ end
+
+ def [](jid)
+ @tel_map[jid.to_s]
+ end
+
+ def choose_tel(iq)
+ self[iq&.from&.stripped].choose_tel(iq)
+ end
+
+ class HaveTel
+ def initialize(tel)
+ @tel = tel
+ end
+
+ def choose_tel(iq)
+ EMPromise.resolve([iq, @tel])
+ end
+ end
+
+ class ChooseTel
+ def choose_tel(_iq)
+ raise "TODO"
+ end
+ end
+end
M sgx_jmp.rb => sgx_jmp.rb +28 -0
@@ 8,17 8,24 @@ require "dhall"
require "em-hiredis"
require "em_promise"
+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])
+
# Braintree is not async, so wrap in EM.defer for now
class AsyncBraintree
def initialize(environment:, merchant_id:, public_key:, private_key:, **)
@@ 60,13 67,16 @@ Blather::DSL.append_features(self.class)
def panic(e)
warn "Error raised during event loop: #{e.message}"
+ warn e.backtrace if e.respond_to?(:backtrace)
exit 1
end
EM.error_handler(&method(:panic))
when_ready do
+ BLATHER = self
REDIS = EM::Hiredis.connect
+ BTC_SELL_PRICES = BTCSellPrices.new(REDIS, CONFIG[:oxr_app_id])
DB = PG::EM::Client.new(dbname: "jmp")
DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)
@@ 126,6 136,7 @@ end
IQ_MANAGER = SessionManager.new(self, :id)
COMMAND_MANAGER = SessionManager.new(self, :sessionid, timeout: 60 * 60)
+web_register_manager = WebRegisterManager.new
disco_items node: "http://jabber.org/protocol/commands" do |iq|
reply = iq.reply
@@ 136,11 147,28 @@ disco_items node: "http://jabber.org/protocol/commands" do |iq|
iq.to,
"buy-credit",
"Buy account credit"
+ ),
+ Blather::Stanza::DiscoItems::Item.new(
+ iq.to,
+ "jabber:iq:register",
+ "Register"
)
]
self << reply
end
+command :execute?, node: "jabber:iq:register", sessionid: nil do |iq|
+ Customer.for_jid(iq.from.stripped).catch {
+ nil
+ }.then { |customer|
+ Registration.for(
+ iq,
+ customer,
+ web_register_manager
+ ).then(&:write)
+ }.catch(&method(:panic))
+end
+
def reply_with_note(iq, text, type: :info)
reply = iq.reply
reply.status = :completed
A test/test_btc_sell_prices.rb => test/test_btc_sell_prices.rb +31 -0
@@ 0,0 1,31 @@
+# frozen_string_literal: true
+
+require "em-hiredis"
+require "test_helper"
+require "btc_sell_prices"
+
+class BTCSellPricesTest < Minitest::Test
+ def setup
+ @redis = Minitest::Mock.new
+ @subject = BTCSellPrices.new(@redis, "")
+ end
+
+ def test_cad
+ stub_request(:get, "https://www.canadianbitcoins.com").to_return(
+ body: "<div id='ticker'><table><tbody><tr>" \
+ "<td>Bitcoin</td><td></td><td>$123.00</td>"
+ )
+ assert_equal BigDecimal.new(123), @subject.cad.sync
+ end
+ em :test_cad
+
+ def test_usd
+ stub_request(:get, "https://www.canadianbitcoins.com").to_return(
+ body: "<div id='ticker'><table><tbody><tr>" \
+ "<td>Bitcoin<td></td><td>$123.00</td>"
+ )
+ @redis.expect(:get, EMPromise.resolve("0.5"), ["cad_to_usd"])
+ assert_equal BigDecimal.new(123) / 2, @subject.usd.sync
+ end
+ em :test_usd
+end
A test/test_electrum.rb => test/test_electrum.rb +106 -0
@@ 0,0 1,106 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "electrum"
+
+class ElectrumTest < Minitest::Test
+ RPC_URI = "http://example.com"
+
+ def setup
+ @electrum = Electrum.new(
+ rpc_uri: RPC_URI,
+ rpc_username: "username",
+ rpc_password: "password"
+ )
+ end
+
+ def stub_rpc(method, params)
+ stub_request(:post, RPC_URI).with(
+ headers: { "Content-Type" => "application/json" },
+ basic_auth: ["username", "password"],
+ body: hash_including(
+ method: method,
+ params: params
+ )
+ )
+ end
+
+ property(:getaddresshistory) { string(:alnum) }
+ em :test_getaddresshistory
+ def getaddresshistory(address)
+ req =
+ stub_rpc("getaddresshistory", address: address)
+ .to_return(body: { result: "result" }.to_json)
+ assert_equal "result", @electrum.getaddresshistory(address).sync
+ assert_requested(req)
+ end
+
+ property(:get_tx_status) { string(:alnum) }
+ em :test_get_tx_status
+ def get_tx_status(tx_hash)
+ req =
+ stub_rpc("get_tx_status", txid: tx_hash)
+ .to_return(body: { result: "result" }.to_json)
+ assert_equal "result", @electrum.get_tx_status(tx_hash).sync
+ assert_requested(req)
+ end
+
+ property(:gettransaction) { [string(:alnum), string(:xdigit)] }
+ em :test_gettransaction
+ def gettransaction(tx_hash, dummy_tx)
+ req1 =
+ stub_rpc("gettransaction", txid: tx_hash)
+ .to_return(body: { result: dummy_tx }.to_json)
+ req2 =
+ stub_rpc("deserialize", [dummy_tx])
+ .to_return(body: { result: { outputs: [] } }.to_json)
+ assert_kind_of Electrum::Transaction, @electrum.gettransaction(tx_hash).sync
+ assert_requested(req1)
+ assert_requested(req2)
+ end
+
+ class TransactionTest < Minitest::Test
+ def transaction(outputs=[])
+ electrum_mock = Minitest::Mock.new("Electrum")
+ [
+ electrum_mock,
+ Electrum::Transaction.new(
+ electrum_mock,
+ "txhash",
+ "outputs" => outputs
+ )
+ ]
+ end
+
+ def test_confirmations
+ electrum_mock, tx = transaction
+ electrum_mock.expect(
+ :get_tx_status,
+ EMPromise.resolve("confirmations" => 1234),
+ ["txhash"]
+ )
+ assert_equal 1234, tx.confirmations.sync
+ end
+ em :test_confirmations
+
+ def test_amount_for_empty
+ _, tx = transaction
+ assert_equal 0, tx.amount_for
+ end
+
+ def test_amount_for_address_not_present
+ _, tx = transaction([{ "address" => "address", "value_sats" => 1 }])
+ assert_equal 0, tx.amount_for("other_address")
+ end
+
+ def test_amount_for_address_present
+ _, tx = transaction([{ "address" => "address", "value_sats" => 1 }])
+ assert_equal 0.00000001, tx.amount_for("address")
+ end
+
+ def test_amount_for_one_of_address_present
+ _, tx = transaction([{ "address" => "address", "value_sats" => 1 }])
+ assert_equal 0.00000001, tx.amount_for("boop", "address", "lol")
+ end
+ end
+end
M test/test_helper.rb => test/test_helper.rb +24 -0
@@ 14,6 14,19 @@ require "webmock/minitest"
begin
require "pry-rescue/minitest"
require "pry-reload"
+
+ module Minitest
+ class Test
+ alias old_capture_exceptions capture_exceptions
+ def capture_exceptions
+ old_capture_exceptions do
+ yield
+ rescue Minitest::Skip => e
+ failures << e
+ end
+ end
+ end
+ end
rescue LoadError
# Just helpers for dev, no big deal if missing
nil
@@ 24,6 37,7 @@ CONFIG = {
component: {
jid: "component"
},
+ activation_amount: 1,
plans: [
{
name: "test_usd",
@@ 45,6 59,16 @@ BLATHER = Class.new {
def <<(*); end
}.new.freeze
+class Matching
+ def initialize(&block)
+ @block = block
+ end
+
+ def ===(other)
+ @block.call(other)
+ end
+end
+
module Minitest
class Test
def self.property(m, &block)
A test/test_registration.rb => test/test_registration.rb +154 -0
@@ 0,0 1,154 @@
+# frozen_string_literal: true
+
+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
+ Registration.for(iq, Customer.new("test"), Minitest::Mock.new).sync
+ end
+ em :test_for_activated
+
+ def test_for_not_activated_with_customer_id
+ Customer::IQ_MANAGER.expect(
+ :write,
+ EMPromise.resolve(nil),
+ [Blather::Stanza::Iq]
+ )
+ web_manager = WebRegisterManager.new
+ web_manager["test@example.com"] = "+15555550000"
+ iq = Blather::Stanza::Iq::Command.new
+ iq.from = "test@example.com"
+ result = Registration.for(
+ iq,
+ Customer.new("test"),
+ web_manager
+ ).sync
+ assert_kind_of Registration::Activation, result
+ end
+ em :test_for_not_activated_with_customer_id
+
+ def test_for_not_activated_without_customer_id
+ skip "customer_id creation not implemented yet"
+ iq = Blather::Stanza::Iq::Command.new
+ Registration.for(iq, nil, Minitest::Mock.new).sync
+ end
+ em :test_for_not_activated_without_customer_id
+
+ class ActivationTest < Minitest::Test
+ Registration::Activation::COMMAND_MANAGER = Minitest::Mock.new
+ def setup
+ iq = Blather::Stanza::Iq::Command.new
+ @activation = Registration::Activation.new(iq, "test", "+15555550000")
+ end
+
+ def test_write
+ result = Minitest::Mock.new
+ result.expect(:then, result)
+ result.expect(:then, EMPromise.resolve(:test_result))
+ Registration::Activation::COMMAND_MANAGER.expect(
+ :write,
+ result,
+ [Blather::Stanza::Iq::Command]
+ )
+ assert_equal :test_result, @activation.write.sync
+ end
+ em :test_write
+ end
+
+ class PaymentTest < Minitest::Test
+ Registration::Payment::Bitcoin::ELECTRUM = Minitest::Mock.new
+
+ def test_for_bitcoin
+ Registration::Payment::Bitcoin::ELECTRUM.expect(:createnewaddress, "addr")
+ iq = Blather::Stanza::Iq::Command.new
+ iq.form.fields = [
+ { var: "activation_method", value: "bitcoin" },
+ { var: "plan_name", value: "test_usd" }
+ ]
+ result = Registration::Payment.for(
+ iq,
+ Customer.new("test"),
+ "+15555550000"
+ )
+ assert_kind_of Registration::Payment::Bitcoin, result
+ end
+
+ def test_for_credit_card
+ skip "CreditCard not implemented yet"
+ iq = Blather::Stanza::Iq::Command.new
+ iq.form.fields = [
+ { var: "activation_method", value: "credit_card" },
+ { var: "plan_name", value: "test_usd" }
+ ]
+ result = Registration::Payment.for(iq, "test", "+15555550000")
+ assert_kind_of Registration::Payment::CreditCard, result
+ end
+
+ def test_for_code
+ skip "Code not implemented yet"
+ iq = Blather::Stanza::Iq::Command.new
+ iq.form.fields = [
+ { var: "activation_method", value: "code" },
+ { var: "plan_name", value: "test_usd" }
+ ]
+ result = Registration::Payment.for(iq, "test", "+15555550000")
+ assert_kind_of Registration::Payment::Code, result
+ end
+
+ class BitcoinTest < Minitest::Test
+ Registration::Payment::Bitcoin::ELECTRUM = Minitest::Mock.new
+ Registration::Payment::Bitcoin::REDIS = Minitest::Mock.new
+ Registration::Payment::Bitcoin::BTC_SELL_PRICES = Minitest::Mock.new
+ Registration::Payment::Bitcoin::BLATHER = Minitest::Mock.new
+
+ def setup
+ Registration::Payment::Bitcoin::ELECTRUM.expect(
+ :createnewaddress,
+ 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"),
+ "+15555550000"
+ )
+ end
+
+ def test_write
+ reply_text = <<~NOTE
+ Activate your account by sending at least 1.000000 BTC to
+ testaddr
+
+ You will receive a notification when your payment is complete.
+ NOTE
+ Registration::Payment::Bitcoin::BLATHER.expect(
+ :<<,
+ nil,
+ [Matching.new do |reply|
+ assert_equal :completed, reply.status
+ assert_equal :info, reply.note_type
+ assert_equal reply_text, reply.note.content
+ true
+ end]
+ )
+ Registration::Payment::Bitcoin::BTC_SELL_PRICES.expect(
+ :usd,
+ EMPromise.resolve(BigDecimal.new(1))
+ )
+ @bitcoin.stub(:save, EMPromise.resolve(nil)) do
+ @bitcoin.write.sync
+ end
+ Registration::Payment::Bitcoin::BLATHER.verify
+ end
+ em :test_write
+ end
+ end
+end
A test/test_web_register_manager.rb => test/test_web_register_manager.rb +32 -0
@@ 0,0 1,32 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "web_register_manager"
+
+class WebRegisterManagerTest < Minitest::Test
+ def setup
+ @manager = WebRegisterManager.new
+ end
+
+ def test_set_get
+ assert_kind_of WebRegisterManager::ChooseTel, @manager["jid@example.com"]
+ @manager["jid@example.com"] = "+15555550000"
+ assert_kind_of WebRegisterManager::HaveTel, @manager["jid@example.com"]
+ end
+
+ def test_choose_tel_have_tel
+ @manager["jid@example.com"] = "+15555550000"
+ iq = Blather::Stanza::Iq.new
+ iq.from = "jid@example.com"
+ assert_equal [iq, "+15555550000"], @manager.choose_tel(iq).sync
+ end
+ em :test_choose_tel_have_tel
+
+ def test_choose_tel_not_have_tel
+ skip "ChooseTel not implemented yet"
+ iq = Blather::Stanza::Iq.new
+ iq.from = "jid@example.com"
+ @manager.choose_tel(iq).sync
+ end
+ em :test_choose_tel_not_have_tel
+end