straight-server 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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