straight-server 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -0
  3. data/Gemfile +3 -1
  4. data/Gemfile.lock +57 -47
  5. data/Gemfile.travis +26 -0
  6. data/README.md +175 -22
  7. data/Rakefile +7 -0
  8. data/VERSION +1 -1
  9. data/benchmark/addons.yml +15 -0
  10. data/benchmark/config.yml +78 -0
  11. data/benchmark/default_last_keychain_id +1 -0
  12. data/benchmark/server_secret +1 -0
  13. data/bin/goliath.log +6 -0
  14. data/bin/goliath.log_stdout.log +51 -0
  15. data/bin/straight-server-benchmark +68 -0
  16. data/db/migrations/003_add_payment_id_to_orders.rb +13 -0
  17. data/db/migrations/004_add_description_to_orders.rb +11 -0
  18. data/db/migrations/005_add_orders_expiration_period_to_gateways.rb +11 -0
  19. data/db/migrations/006_add_check_order_status_in_db_first_to_gateways.rb +11 -0
  20. data/db/migrations/007_add_active_switcher_to_gateways.rb +11 -0
  21. data/db/migrations/008_add_order_counters_to_gateways.rb +11 -0
  22. data/db/migrations/009_add_hashed_id_to_gateways.rb +18 -0
  23. data/examples/client/client.dart +5 -0
  24. data/examples/client/client.html +7 -15
  25. data/examples/client/client.js +15 -0
  26. data/lib/straight-server/config.rb +1 -1
  27. data/lib/straight-server/gateway.rb +241 -59
  28. data/lib/straight-server/initializer.rb +170 -44
  29. data/lib/straight-server/logger.rb +1 -1
  30. data/lib/straight-server/order.rb +74 -9
  31. data/lib/straight-server/orders_controller.rb +23 -6
  32. data/lib/straight-server/random_string.rb +18 -0
  33. data/lib/straight-server/server.rb +44 -17
  34. data/lib/straight-server/utils/hash_string_to_sym_keys.rb +24 -0
  35. data/lib/straight-server.rb +6 -3
  36. data/spec/.straight/config.yml +16 -0
  37. data/spec/.straight/server_secret +1 -0
  38. data/spec/fixtures/addons.yml +19 -0
  39. data/spec/fixtures/test_addon.rb +8 -0
  40. data/spec/lib/gateway_spec.rb +93 -13
  41. data/spec/lib/initializer_spec.rb +104 -0
  42. data/spec/lib/order_spec.rb +59 -0
  43. data/spec/lib/orders_controller_spec.rb +34 -1
  44. data/spec/lib/utils/hash_string_to_sym_keys.rb +18 -0
  45. data/spec/spec_helper.rb +10 -2
  46. data/straight-server.gemspec +36 -8
  47. data/templates/addons.yml +15 -0
  48. data/templates/config.yml +41 -0
  49. metadata +47 -5
@@ -1,6 +1,6 @@
1
1
  module StraightServer
2
2
 
3
- class Order < Sequel::Model
3
+ class Order < Sequel::Model
4
4
 
5
5
  include Straight::OrderModule
6
6
  plugin :validation_helpers
@@ -8,6 +8,12 @@ module StraightServer
8
8
 
9
9
  plugin :serialization
10
10
  serialize_attributes :marshal, :callback_response
11
+ serialize_attributes :marshal, :data
12
+
13
+ plugin :after_initialize
14
+ def after_initialize
15
+ @status = self[:status] || 0
16
+ end
11
17
 
12
18
  def gateway
13
19
  @gateway ||= Gateway.find_by_id(gateway_id)
@@ -18,17 +24,42 @@ module StraightServer
18
24
  @gateway = g
19
25
  end
20
26
 
27
+ # This method is called from the Straight::OrderModule::Prependable
28
+ # using super(). The reason it is reloaded here is because sometimes
29
+ # we want to query the DB first and see if status has changed there.
30
+ #
31
+ # If it indeed changed in the DB and is > 1, then the original
32
+ # Straight::OrderModule::Prependable#status method will not try to
33
+ # query the blockchain (using adapters) because the status has already
34
+ # been changed to be > 1.
35
+ #
36
+ # This is mainly useful for debugging. For example,
37
+ # when testing payments, you don't actually want to pay, you can just
38
+ # run the server console, change order status in the DB and see how your
39
+ # client picks it up, showing you that your order has been paid for.
40
+ #
41
+ # If you want the feature described above on,
42
+ # set StraightServer::Config.check_order_status_in_db_first to true
43
+ def status(as_sym: false, reload: false)
44
+ if reload && StraightServer::Config.check_order_status_in_db_first
45
+ @old_status = self.status
46
+ self.refresh
47
+ unless self[:status] == @old_status
48
+ @status = self[:status]
49
+ @status_changed = true
50
+ self.gateway.order_status_changed(self)
51
+ end
52
+ end
53
+ self[:status] = @status
54
+ end
55
+
21
56
  def save
22
57
  super # calling Sequel::Model save
23
58
  @status_changed = false
24
59
  end
25
60
 
26
- def status_changed?
27
- @status_changed
28
- end
29
-
30
61
  def to_h
31
- super.merge({ id: id })
62
+ super.merge({ id: id, payment_id: payment_id, amount_in_btc: amount_in_btc(as: :string) })
32
63
  end
33
64
 
34
65
  def to_json
@@ -39,6 +70,8 @@ module StraightServer
39
70
  super # calling Sequel::Model validator
40
71
  errors.add(:amount, "is invalid") if !amount.kind_of?(Numeric) || amount <= 0
41
72
  errors.add(:gateway_id, "is invalid") if !gateway_id.kind_of?(Numeric) || gateway_id <= 0
73
+ errors.add(:description, "should be shorter than 255 charachters") if description.kind_of?(String) && description.length > 255
74
+ errors.add(:gateway, "is inactive, cannot create order for inactive gateway") unless gateway.active
42
75
  validates_unique :id, :address, [:keychain_id, :gateway_id]
43
76
  validates_presence [:address, :keychain_id, :gateway_id, :amount]
44
77
  end
@@ -47,16 +80,48 @@ module StraightServer
47
80
  "order_id=#{id}&amount=#{amount}&status=#{status}&address=#{address}&tid=#{tid}"
48
81
  end
49
82
 
50
- def start_periodic_status_check
51
- StraightServer.logger.info "Starting periodic status checks of the order #{self.id}"
83
+ def before_create
84
+ self.payment_id = gateway.sign_with_secret("#{keychain_id}#{amount}#{created_at}")
85
+
86
+ # Save info about current exchange rate at the time of purchase
87
+ unless gateway.default_currency == 'BTC'
88
+ self.data = {} unless self.data
89
+ self.data[:exchange_rate] = { price: gateway.current_exchange_rate, currency: gateway.default_currency }
90
+ end
91
+
52
92
  super
53
93
  end
54
94
 
55
- def check_status_on_schedule(period: 10, iteration_index: 0)
95
+ # Update Gateway's order_counters, incrementing the :new counter.
96
+ # All other increments/decrements happen in the the Gateway#order_status_changed callback,
97
+ # but the initial :new increment needs this code because the Gateway#order_status_changed
98
+ # isn't called in this case.
99
+ def after_create
100
+ self.gateway.increment_order_counter!(:new) if StraightServer::Config.count_orders
101
+ end
102
+
103
+ # Reloads the method in Straight engine. We need to take
104
+ # Order#created_at into account now, so that we don't start checking on
105
+ # an order that is already expired. Or, if it's not expired yet,
106
+ # we make sure to stop all checks as soon as it expires, but not later.
107
+ def start_periodic_status_check(duration: gateway.orders_expiration_period)
108
+ StraightServer.logger.info "Starting periodic status checks of order #{self.id}"
109
+ if (t = time_left_before_expiration) > 0
110
+ check_status_on_schedule(duration: t)
111
+ end
112
+ self.save if self.status_changed?
113
+ end
114
+
115
+ def check_status_on_schedule(period: 10, iteration_index: 0, duration: 600, time_passed: 0)
56
116
  StraightServer.logger.info "Checking status of order #{self.id}"
57
117
  super
58
118
  end
59
119
 
120
+ def time_left_before_expiration(duration: gateway.orders_expiration_period)
121
+ time_passed_after_creation = (Time.now - created_at).to_i
122
+ gateway.orders_expiration_period-time_passed_after_creation
123
+ end
124
+
60
125
  end
61
126
 
62
127
  end
@@ -13,13 +13,20 @@ module StraightServer
13
13
  end
14
14
 
15
15
  def create
16
+
17
+ unless @gateway
18
+ StraightServer.logger.warn "Gateway not found"
19
+ return [404, {}, "Gateway not found" ]
20
+ end
21
+
16
22
  begin
17
23
  order = @gateway.create_order(
18
- amount: @params['amount'],
24
+ amount: @params['amount'], # this is satoshi
19
25
  currency: @params['currency'],
20
26
  btc_denomination: @params['btc_denomination'],
21
27
  id: @params['order_id'],
22
- signature: @params['signature']
28
+ signature: @params['signature'],
29
+ data: @params['data']
23
30
  )
24
31
  StraightServer::Thread.new do
25
32
  order.start_periodic_status_check
@@ -33,11 +40,14 @@ module StraightServer
33
40
  rescue StraightServer::GatewayModule::InvalidOrderId
34
41
  StraightServer.logger.warn message = "An invalid id for order supplied: #{@params['order_id']}"
35
42
  [409, {}, message ]
43
+ rescue StraightServer::GatewayModule::GatewayInactive
44
+ StraightServer.logger.warn message = "The gateway is inactive, you cannot create order with it"
45
+ [503, {}, message ]
36
46
  end
37
47
  end
38
48
 
39
49
  def show
40
- order = Order[@params['id']]
50
+ order = Order[@params['id']] || (@params['id'] =~ /[^\d]+/ && Order[:payment_id => @params['id']])
41
51
  if order
42
52
  order.status(reload: true)
43
53
  order.save if order.status_changed?
@@ -46,7 +56,13 @@ module StraightServer
46
56
  end
47
57
 
48
58
  def websocket
49
- order = Order[@params['id']]
59
+
60
+ order = if @params['id'] =~ /[^\d]+/
61
+ Order[:payment_id => @params['id']]
62
+ else
63
+ Order[@params['id']]
64
+ end
65
+
50
66
  if order
51
67
  begin
52
68
  @gateway.add_websocket_for_order ws = Faye::WebSocket.new(@env), order
@@ -66,10 +82,11 @@ module StraightServer
66
82
  StraightServer.logger.blank_lines
67
83
  StraightServer.logger.info "#{@method} #{@env['REQUEST_PATH']}\n#{@params}"
68
84
 
69
- @gateway = StraightServer::Gateway.find_by_id(@request_path[1])
85
+ @gateway = StraightServer::Gateway.find_by_hashed_id(@request_path[1])
70
86
 
71
87
  @response = if @request_path[3] # if an order id is supplied
72
- @params['id'] = @request_path[3].to_i
88
+ @params['id'] = @request_path[3]
89
+ @params['id'] = @params['id'].to_i if @params['id'] =~ /\A\d+\Z/
73
90
  if @request_path[4] == 'websocket'
74
91
  websocket
75
92
  elsif @request_path[4].nil? && @method == 'GET'
@@ -0,0 +1,18 @@
1
+ class String
2
+
3
+ def self.random(len)
4
+ s = ""
5
+ while s.length != len do
6
+ s = rand(36**len).to_s(36)
7
+ end
8
+ s
9
+ end
10
+
11
+ def repeat(times)
12
+ result = ""
13
+ times.times { result << self }
14
+ result
15
+ end
16
+
17
+ end
18
+
@@ -6,12 +6,24 @@ module StraightServer
6
6
  Faye::WebSocket.load_adapter('goliath')
7
7
 
8
8
  def initialize
9
+ super
9
10
  prepare
10
- StraightServer.logger.info "Starting Straight server v #{StraightServer::VERSION}"
11
+ StraightServer.logger.info "starting Straight Server v #{StraightServer::VERSION}"
11
12
  require_relative 'order'
12
13
  require_relative 'gateway'
13
14
  require_relative 'orders_controller'
14
- super
15
+ load_addons
16
+ resume_tracking_active_orders!
17
+ end
18
+
19
+ def options_parser(opts, options)
20
+ # Even though we define that option here, it is purely for the purposes of compliance with
21
+ # Goliath server. If don't do that, there will be an exception saying "unrecognized argument".
22
+ # In reality, we make use of --config-dir value in the in StraightServer::Initializer and stored
23
+ # it in StraightServer::Initializer.config_dir property.
24
+ opts.on('-c', '--config-dir STRING', "Directory where config files and addons are placed") do |val|
25
+ options[:config_dir] = File.expand_path(val || ENV['HOME'] + '/.straight' )
26
+ end
15
27
  end
16
28
 
17
29
  def response(env)
@@ -24,21 +36,15 @@ module StraightServer
24
36
 
25
37
  StraightServer.logger.watch_exceptions do
26
38
 
27
- # This is a client implementation example, an html page + a dart script
28
- # supposed to only be loaded in development.
29
- if Goliath.env == :development
30
- if env['REQUEST_PATH'] == '/'
31
- return [200, {}, IO.read(Initializer::GEM_ROOT + '/examples/client/client.html')]
32
- elsif Goliath.env == :development && env['REQUEST_PATH'] == '/client.dart'
33
- return [200, {}, IO.read(Initializer::GEM_ROOT + '/examples/client/client.dart')]
34
- end
35
- end
36
-
37
- if env['REQUEST_PATH'] =~ /\A\/gateways\/.+?\/orders(\/.+)?\Z/
38
- controller = OrdersController.new(env)
39
- return controller.response
40
- else
41
- return [404, {}, "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} Not found"]
39
+ # If the process is daemonized, we get Sequel::DatabaseDisconnectError with Postgres.
40
+ # The explanation is here: https://github.com/thuehlinger/daemons/issues/31
41
+ # Until I figure out where to call connect_to_db so that it connects to the DB
42
+ # AFTER the process is daemonized, this shall remain as it is now.
43
+ begin
44
+ return process_request(env)
45
+ rescue Sequel::DatabaseDisconnectError
46
+ connect_to_db
47
+ return process_request(env)
42
48
  end
43
49
 
44
50
  end
@@ -47,6 +53,27 @@ module StraightServer
47
53
  [500, {}, "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} Server Error"]
48
54
 
49
55
  end
56
+
57
+ # This is a separate method now because of the need to rescue Sequel::DatabaseDisconnectError
58
+ # As soon as we figure out where should #connect_to_db be placed so that it is executed AFTER the process
59
+ # is daemonized, I'll refactor the code.
60
+ def process_request(env)
61
+ # This is a client implementation example, an html page + a dart script
62
+ # supposed to only be loaded in development.
63
+ if Goliath.env == :development
64
+ if env['REQUEST_PATH'] == '/'
65
+ return [200, {}, IO.read(Initializer::GEM_ROOT + '/examples/client/client.html')]
66
+ elsif Goliath.env == :development && env['REQUEST_PATH'] == '/client.js'
67
+ return [200, {}, IO.read(Initializer::GEM_ROOT + '/examples/client/client.js')]
68
+ end
69
+ end
70
+
71
+ @routes.each do |path, action| # path is a regexp
72
+ return action.call(env) if env['REQUEST_PATH'] =~ path
73
+ end
74
+ # no block was called, means no route matched. Let's render 404
75
+ return [404, {}, "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} Not found"]
76
+ end
50
77
 
51
78
  end
52
79
  end
@@ -0,0 +1,24 @@
1
+ class Hash
2
+
3
+ # Replace String keys in the current hash with symbol keys
4
+ def keys_to_sym!
5
+ new_hash = keys_to_sym
6
+ self.clear
7
+ new_hash.each do |k,v|
8
+ self[k] = v
9
+ end
10
+ end
11
+
12
+ def keys_to_sym
13
+ symbolized_hash = {}
14
+ self.each do |k,v|
15
+ if k =~ /\A[a-zA-Z0-9!?_]+\Z/
16
+ symbolized_hash[k.to_sym] = v
17
+ else
18
+ symbolized_hash[k] = v
19
+ end
20
+ end
21
+ symbolized_hash
22
+ end
23
+
24
+ end
@@ -3,12 +3,15 @@ require 'json'
3
3
  require 'sequel'
4
4
  require 'straight'
5
5
  require 'logmaster'
6
- require 'hmac'
7
- require 'hmac-sha1'
6
+ require 'openssl'
7
+ require 'base64'
8
8
  require 'net/http'
9
9
  require 'faye/websocket'
10
10
  Sequel.extension :migration
11
11
 
12
+
13
+ require_relative 'straight-server/utils/hash_string_to_sym_keys'
14
+ require_relative 'straight-server/random_string'
12
15
  require_relative 'straight-server/config'
13
16
  require_relative 'straight-server/initializer'
14
17
  require_relative 'straight-server/thread'
@@ -16,7 +19,7 @@ require_relative 'straight-server/orders_controller'
16
19
 
17
20
  module StraightServer
18
21
 
19
- VERSION = '0.1.0'
22
+ VERSION = File.read(File.expand_path('../', File.dirname(__FILE__)) + '/VERSION')
20
23
 
21
24
  class << self
22
25
  attr_accessor :db_connection, :logger
@@ -1,6 +1,8 @@
1
1
  # If set to db, then use DB table to store gateways,
2
2
  # useful when your run many gateways on the same server.
3
3
  gateways_source: config
4
+ environment: test
5
+ count_orders: true
4
6
 
5
7
  gateways:
6
8
 
@@ -12,10 +14,12 @@ gateways:
12
14
  check_signature: true
13
15
  callback_url: 'http://localhost:3000/payment-callback'
14
16
  default_currency: 'BTC'
17
+ orders_expiration_period: 900 # seconds
15
18
  exchange_rate_adapters:
16
19
  - Bitpay
17
20
  - Coinbase
18
21
  - Bitstamp
22
+ active: true
19
23
  second_gateway:
20
24
  pubkey: 'xpub-001'
21
25
  confirmations_required: 0
@@ -24,11 +28,23 @@ gateways:
24
28
  check_signature: false
25
29
  callback_url: 'http://localhost:3001/payment-callback'
26
30
  default_currency: 'BTC'
31
+ orders_expiration_period: 300 # seconds
27
32
  exchange_rate_adapters:
28
33
  - Bitpay
29
34
  - Coinbase
30
35
  - Bitstamp
36
+ active: true
37
+
38
+ blockchain_adapters:
39
+ - BlockchainInfo
40
+ - Mycelium
31
41
 
32
42
  db:
33
43
  adapter: sqlite
34
44
  name: straight.db
45
+
46
+ redis:
47
+ host: localhost
48
+ port: 6379
49
+ db: null
50
+ password: null
@@ -0,0 +1 @@
1
+ global server secret
@@ -0,0 +1,19 @@
1
+ # This is where you put info about your addons.
2
+ #
3
+ # Addons are just modules that extend the StraightServer::Server module.
4
+ #
5
+ # Addon modules can be both rubygems or files under ~/.straight/addons/.
6
+ # If ~/.straight/addons.yml contains a 'path' key for a particular addon, then it means
7
+ # the addon is placed under the ~/.straight/addons/. If not, it is assumed it is
8
+ # already in the LOAD_PATH somehow, with rubygems for example.
9
+ #
10
+ #
11
+ # Example:
12
+ #
13
+ # payment_ui: # <- name doesn't affect anything, just shows up in the log file
14
+ # path: addons/payment_ui # <- This is unnecessary if addon is already in the LOAD_PATH
15
+ # module: PaymentUI # <- actual module should be a submodule of StraightServer::Addon
16
+
17
+ test_addon:
18
+ path: 'addons/test_addon'
19
+ module: 'TestAddon'
@@ -0,0 +1,8 @@
1
+ module StraightServer
2
+ module Addon
3
+ module TestAddon
4
+ def test_addon_method
5
+ end
6
+ end
7
+ end
8
+ end
@@ -5,6 +5,7 @@ RSpec.describe StraightServer::Gateway do
5
5
  before(:each) do
6
6
  @gateway = StraightServer::GatewayOnConfig.find_by_id(1)
7
7
  @order_mock = double("order mock")
8
+ allow(@order_mock).to receive(:old_status)
8
9
  [:id, :gateway=, :save, :to_h, :id=].each { |m| allow(@order_mock).to receive(m) }
9
10
  @order_for_keychain_id_args = { amount: 1, keychain_id: 1, currency: nil, btc_denomination: nil }
10
11
  end
@@ -13,7 +14,7 @@ RSpec.describe StraightServer::Gateway do
13
14
  @gateway.last_keychain_id = 0
14
15
  expect( -> { @gateway.create_order(amount: 1, signature: 'invalid', id: 1) }).to raise_exception(StraightServer::GatewayModule::InvalidSignature)
15
16
  expect(@gateway).to receive(:order_for_keychain_id).with(@order_for_keychain_id_args).once.and_return(@order_mock)
16
- @gateway.create_order(amount: 1, signature: hmac_sha1(1, 'secret'), id: 1)
17
+ @gateway.create_order(amount: 1, signature: hmac_sha256(1, 'secret'), id: 1)
17
18
  end
18
19
 
19
20
  it "checks md5 signature only if that setting is set ON for a particular gateway" do
@@ -25,8 +26,8 @@ RSpec.describe StraightServer::Gateway do
25
26
  end
26
27
 
27
28
  it "doesn't allow nil or empty order id if signature checks are enabled" do
28
- expect( -> { @gateway.create_order(amount: 1, signature: 'invalid', id: nil) }).to raise_exception(StraightServer::GatewayModule::InvalidOrderId)
29
- expect( -> { @gateway.create_order(amount: 1, signature: 'invalid', id: '') }).to raise_exception(StraightServer::GatewayModule::InvalidOrderId)
29
+ expect( -> { @gateway.create_order(amount: 1, signature: hmac_sha256(nil, 'secret'), id: nil) }).to raise_exception(StraightServer::GatewayModule::InvalidOrderId)
30
+ expect( -> { @gateway.create_order(amount: 1, signature: hmac_sha256('', 'secret'), id: '') }).to raise_exception(StraightServer::GatewayModule::InvalidOrderId)
30
31
  end
31
32
 
32
33
  it "sets order amount in satoshis calculated from another currency" do
@@ -36,6 +37,16 @@ RSpec.describe StraightServer::Gateway do
36
37
  expect(@gateway.create_order(amount: 2252.706, currency: 'USD').amount).to eq(500000000)
37
38
  end
38
39
 
40
+ it "doesn't allow to create a new order if the gateway is inactive" do
41
+ @gateway.active = false
42
+ expect( -> { @gateway.create_order }).to raise_exception(StraightServer::GatewayModule::GatewayInactive)
43
+ @gateway.active = true
44
+ end
45
+
46
+ it "loads blockchain adapters according to the config file" do
47
+ expect(@gateway.blockchain_adapters.map(&:class)).to eq([Straight::Blockchain::BlockchainInfoAdapter, Straight::Blockchain::MyceliumAdapter])
48
+ end
49
+
39
50
  context "callback url" do
40
51
 
41
52
  before(:each) do
@@ -64,7 +75,7 @@ RSpec.describe StraightServer::Gateway do
64
75
  it "signs the callback if gateway has a secret" do
65
76
  @gateway = StraightServer::GatewayOnConfig.find_by_id(1) # Gateway 1 requires signatures
66
77
  expect(@response_mock).to receive(:code).twice.and_return("200")
67
- expect(URI).to receive(:parse).with('http://localhost:3000/payment-callback?' + @order.to_http_params + "&signature=#{hmac_sha1(hmac_sha1(@order.id, 'secret'), 'secret')}")
78
+ expect(URI).to receive(:parse).with('http://localhost:3000/payment-callback?' + @order.to_http_params + "&signature=#{hmac_sha256(hmac_sha256(@order.id, 'secret'), 'secret')}")
68
79
  expect(Net::HTTP).to receive(:get_response).and_return(@response_mock)
69
80
  @gateway.order_status_changed(@order)
70
81
  end
@@ -88,6 +99,51 @@ RSpec.describe StraightServer::Gateway do
88
99
 
89
100
  end
90
101
 
102
+ describe "order counters" do
103
+
104
+ it "uses 0 for non-existent order counters and increments them" do
105
+ expect(@gateway.order_counters(reload: true)).to include({ new: 0, unconfirmed: 0, paid: 0, underpaid: 0, overpaid: 0, expired: 0 })
106
+ @gateway.increment_order_counter!(:new)
107
+ expect(@gateway.order_counters(reload: true)[:new]).to eq(1)
108
+ end
109
+
110
+ it "raises exception when trying to access counters but the feature is disabled" do
111
+ allow(StraightServer::Config).to receive(:count_orders).and_return(false)
112
+ expect( -> { @gateway.order_counters(reload: true) }).to raise_exception(StraightServer::Gateway::OrderCountersDisabled)
113
+ expect( -> { @gateway.increment_order_counter!(:new) }).to raise_exception(StraightServer::Gateway::OrderCountersDisabled)
114
+ end
115
+
116
+ it "updates gateway's order counters when an associated order status changes" do
117
+ allow_any_instance_of(StraightServer::Order).to receive(:transaction).and_return({ tid: 'xxx' })
118
+ allow(@gateway).to receive(:send_callback_http_request)
119
+ allow(@gateway).to receive(:send_order_to_websocket_client)
120
+
121
+ expect(@gateway.order_counters(reload: true)).to eq({ new: 0, unconfirmed: 0, paid: 0, underpaid: 0, overpaid: 0, expired: 0 })
122
+ order = create(:order, gateway_id: @gateway.id)
123
+ expect(@gateway.order_counters(reload: true)).to eq({ new: 1, unconfirmed: 0, paid: 0, underpaid: 0, overpaid: 0, expired: 0 })
124
+ order.status = 2
125
+ expect(@gateway.order_counters(reload: true)).to eq({ new: 0, unconfirmed: 0, paid: 1, underpaid: 0, overpaid: 0, expired: 0 })
126
+
127
+ expect(@gateway.order_counters(reload: true)).to eq({ new: 0, unconfirmed: 0, paid: 1, underpaid: 0, overpaid: 0, expired: 0 })
128
+ order = create(:order, gateway_id: @gateway.id)
129
+ expect(@gateway.order_counters(reload: true)).to eq({ new: 1, unconfirmed: 0, paid: 1, underpaid: 0, overpaid: 0, expired: 0 })
130
+ order.status = 1
131
+ expect(@gateway.order_counters(reload: true)).to eq({ new: 0, unconfirmed: 1, paid: 1, underpaid: 0, overpaid: 0, expired: 0 })
132
+ order.status = 5
133
+ expect(@gateway.order_counters(reload: true)).to eq({ new: 0, unconfirmed: 0, paid: 1, underpaid: 0, overpaid: 0, expired: 1 })
134
+ end
135
+
136
+ it "doesn't increment orders on status update unless the option is turned on (but no exception raised)" do
137
+ allow(StraightServer::Config).to receive(:count_orders).and_return(false)
138
+ allow_any_instance_of(StraightServer::Order).to receive(:transaction).and_return({ tid: 'xxx' })
139
+ allow(@gateway).to receive(:send_callback_http_request)
140
+ allow(@gateway).to receive(:send_order_to_websocket_client)
141
+ order = create(:order, gateway_id: @gateway.id)
142
+ expect(StraightServer::Config.redis[:connection].get("#{StraightServer::Config.redis[:prefix]}:gateway_#{@gateway.id}:new_orders_counter")).to be_nil
143
+ end
144
+
145
+ end
146
+
91
147
  describe "config based gateway" do
92
148
 
93
149
  it "loads all the gateways from the config file and assigns correct attributes" do
@@ -113,16 +169,23 @@ RSpec.describe StraightServer::Gateway do
113
169
  expect(File.read("#{ENV['HOME']}/.straight/default_last_keychain_id").to_i).to eq(1)
114
170
 
115
171
  expect(@gateway).to receive(:order_for_keychain_id).with(@order_for_keychain_id_args.merge({ keychain_id: 2})).once.and_return(@order_mock)
116
- @gateway.create_order(amount: 1, signature: hmac_sha1(1, 'secret'), id: 1)
172
+ @gateway.create_order(amount: 1, signature: hmac_sha256(1, 'secret'), id: 1)
117
173
  expect(File.read("#{ENV['HOME']}/.straight/default_last_keychain_id").to_i).to eq(2)
118
174
  end
175
+
176
+ it "searches for Gateway using regular ids when find_by_hashed_id method is called" do
177
+ expect(StraightServer::GatewayOnConfig.find_by_hashed_id(1)).not_to be_nil
178
+ end
119
179
 
120
180
  end
121
181
 
122
182
  describe "db based gateway" do
123
183
 
124
184
  before(:each) do
125
- @gateway = StraightServer::GatewayOnDB.create(
185
+ # clean the database
186
+ DB.run("DELETE FROM gateways")
187
+
188
+ @gateway = StraightServer::GatewayOnDB.new(
126
189
  confirmations_required: 0,
127
190
  pubkey: 'xpub-000',
128
191
  order_class: 'StraightServer::Order',
@@ -134,21 +197,40 @@ RSpec.describe StraightServer::Gateway do
134
197
  end
135
198
 
136
199
  it "saves and retrieves last_keychain_id from the db" do
200
+ @gateway.save
137
201
  expect(DB[:gateways][:name => 'default'][:last_keychain_id]).to eq(0)
138
202
  @gateway.increment_last_keychain_id!
139
203
  expect(DB[:gateways][:name => 'default'][:last_keychain_id]).to eq(1)
140
204
 
141
205
  expect(@gateway).to receive(:order_for_keychain_id).with(@order_for_keychain_id_args.merge({ keychain_id: 2})).once.and_return(@order_mock)
142
- @gateway.create_order(amount: 1, signature: hmac_sha1(1, 'secret'), id: 1)
206
+ @gateway.create_order(amount: 1, signature: hmac_sha256(1, 'secret'), id: 1)
143
207
  expect(DB[:gateways][:name => 'default'][:last_keychain_id]).to eq(2)
144
208
  end
145
209
 
210
+ it "encryptes and decrypts the gateway secret" do
211
+ expect(@gateway.send(:encrypt_secret)).to eq("96c1c24edff5c1c2:6THJEZqg+2qlDhtWE2Tytg==")
212
+ expect(@gateway.send(:decrypt_secret)).to eq("secret")
213
+ expect(@gateway.secret).to eq("secret")
214
+ end
215
+
216
+ it "finds orders using #find_by_id method which is essentially an alias for Gateway[]" do
217
+ @gateway.save
218
+ expect(StraightServer::GatewayOnDB.find_by_id(@gateway.id)).to eq(@gateway)
219
+ end
220
+
221
+ it "assigns hashed_id to gateway and then finds gateway using that value" do
222
+ @gateway.save
223
+ hashed_id = hmac_sha256(@gateway.id, 'global server secret')
224
+ expect(@gateway.hashed_id).to eq(hashed_id)
225
+ expect(StraightServer::GatewayOnDB.find_by_hashed_id(hashed_id)).to eq(@gateway)
226
+ end
227
+
146
228
  end
147
229
 
148
230
  describe "handling websockets" do
149
231
 
150
232
  before(:each) do
151
- @gateway.instance_variable_set(:@websockets, {})
233
+ StraightServer::GatewayModule.class_variable_set(:@@websockets, { @gateway.id => {} })
152
234
  @ws = double("websocket mock")
153
235
  allow(@ws).to receive(:on).with(:close)
154
236
  allow(@order_mock).to receive(:id).and_return(1)
@@ -157,7 +239,7 @@ RSpec.describe StraightServer::Gateway do
157
239
 
158
240
  it "adds a new websocket for the order" do
159
241
  @gateway.add_websocket_for_order(@ws, @order_mock)
160
- expect(@gateway.instance_variable_get(:@websockets)).to eq({ 1 => @ws})
242
+ expect(@gateway.websockets).to eq({1 => @ws})
161
243
  end
162
244
 
163
245
  it "sends a message to the websocket when status of the order is changed and closes the connection" do
@@ -182,10 +264,8 @@ RSpec.describe StraightServer::Gateway do
182
264
 
183
265
  end
184
266
 
185
- def hmac_sha1(key, secret)
186
- h = HMAC::SHA1.new('secret')
187
- h << key.to_s
188
- h.hexdigest
267
+ def hmac_sha256(key, secret)
268
+ OpenSSL::HMAC.digest('sha256', secret, key.to_s).unpack("H*").first
189
269
  end
190
270
 
191
271
  end