straight-server 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +5 -2
  4. data/Gemfile.travis +2 -0
  5. data/README.md +46 -19
  6. data/Rakefile +5 -2
  7. data/VERSION +1 -1
  8. data/db/migrations/003_add_payment_id_to_orders.rb +1 -1
  9. data/db/migrations/004_add_description_to_orders.rb +1 -1
  10. data/db/migrations/005_add_orders_expiration_period_to_gateways.rb +1 -1
  11. data/db/migrations/006_add_check_order_status_in_db_first_to_gateways.rb +1 -1
  12. data/db/migrations/007_add_active_switcher_to_gateways.rb +1 -1
  13. data/db/migrations/008_add_order_counters_to_gateways.rb +1 -1
  14. data/db/migrations/009_add_hashed_id_to_gateways.rb +2 -2
  15. data/db/migrations/010_add_address_reusability_orders.rb +19 -0
  16. data/db/migrations/011_add_callback_data_to_orders.rb +11 -0
  17. data/db/schema.rb +55 -0
  18. data/examples/client/client.html +1 -1
  19. data/examples/client/client.js +1 -1
  20. data/lib/straight-server/config.rb +3 -1
  21. data/lib/straight-server/gateway.rb +133 -34
  22. data/lib/straight-server/initializer.rb +8 -7
  23. data/lib/straight-server/order.rb +12 -5
  24. data/lib/straight-server/orders_controller.rb +45 -13
  25. data/lib/straight-server/throttler.rb +63 -0
  26. data/lib/tasks/db.rake +42 -0
  27. data/spec/.straight/config.yml +3 -2
  28. data/spec/lib/gateway_spec.rb +88 -9
  29. data/spec/lib/order_spec.rb +8 -13
  30. data/spec/lib/orders_controller_spec.rb +36 -4
  31. data/spec/lib/throttle_spec.rb +52 -0
  32. data/straight-server.gemspec +13 -6
  33. data/templates/config.yml +19 -2
  34. metadata +22 -6
  35. data/bin/goliath.log +0 -6
  36. data/bin/goliath.log_stdout.log +0 -51
@@ -3,6 +3,7 @@ module StraightServer
3
3
  module Initializer
4
4
 
5
5
  GEM_ROOT = File.expand_path('../..', File.dirname(__FILE__))
6
+ MIGRATIONS_ROOT = GEM_ROOT + '/db/migrations/'
6
7
 
7
8
  module ConfigDir
8
9
 
@@ -52,13 +53,13 @@ module StraightServer
52
53
  FileUtils.mkdir_p(ConfigDir.path) unless File.exist?(ConfigDir.path)
53
54
 
54
55
  unless File.exist?(ConfigDir.path + '/addons.yml')
55
- puts "\e[1;33mNOTICE!\e[0m \e[33mNo file ~/.straight/addons.yml was found. Created an empty sample for you.\e[0m"
56
+ puts "\e[1;33mNOTICE!\e[0m \e[33mNo file #{ConfigDir.path}/addons.yml was found. Created an empty sample for you.\e[0m"
56
57
  puts "No need to restart until you actually list your addons there. Now will continue loading StraightServer."
57
- FileUtils.cp(GEM_ROOT + '/templates/addons.yml', ENV['HOME'] + '/.straight/')
58
+ FileUtils.cp(GEM_ROOT + '/templates/addons.yml', ConfigDir.path)
58
59
  end
59
60
 
60
61
  unless File.exist?(ConfigDir.path + '/server_secret')
61
- puts "\e[1;33mNOTICE!\e[0m \e[33mNo file ~/.straight/server_secret was found. Created one for you.\e[0m"
62
+ puts "\e[1;33mNOTICE!\e[0m \e[33mNo file #{ConfigDir.path}/server_secret was found. Created one for you.\e[0m"
62
63
  puts "No need to restart so far. Now will continue loading StraightServer."
63
64
  File.open(ConfigDir.path + '/server_secret', "w") do |f|
64
65
  f.puts String.random(16)
@@ -66,10 +67,10 @@ module StraightServer
66
67
  end
67
68
 
68
69
  unless File.exist?(ConfigDir.path + '/config.yml')
69
- puts "\e[1;33mWARNING!\e[0m \e[33mNo file ~/.straight/config was found. Created a sample one for you.\e[0m"
70
+ puts "\e[1;33mWARNING!\e[0m \e[33mNo file #{ConfigDir.path}/config.yml was found. Created a sample one for you.\e[0m"
70
71
  puts "You should edit it and try starting the server again.\n"
71
72
 
72
- FileUtils.cp(GEM_ROOT + '/templates/config.yml', ENV['HOME'] + '/.straight/')
73
+ FileUtils.cp(GEM_ROOT + '/templates/config.yml', ConfigDir.path)
73
74
  puts "Shutting down now.\n\n"
74
75
  exit
75
76
  end
@@ -106,12 +107,12 @@ module StraightServer
106
107
 
107
108
  def run_migrations
108
109
  print "\nPending migrations for the database detected. Migrating..."
109
- Sequel::Migrator.run(StraightServer.db_connection, GEM_ROOT + '/db/migrations/')
110
+ Sequel::Migrator.run(StraightServer.db_connection, MIGRATIONS_ROOT)
110
111
  print "done\n\n"
111
112
  end
112
113
 
113
114
  def migrations_pending?
114
- !Sequel::Migrator.is_current?(StraightServer.db_connection, GEM_ROOT + '/db/migrations/')
115
+ !Sequel::Migrator.is_current?(StraightServer.db_connection, MIGRATIONS_ROOT)
115
116
  end
116
117
 
117
118
  def create_logger
@@ -7,8 +7,15 @@ module StraightServer
7
7
  plugin :timestamps, create: :created_at, update: :updated_at
8
8
 
9
9
  plugin :serialization
10
- serialize_attributes :marshal, :callback_response
10
+
11
+ # Additional data that can be passed and stored with each order. Not returned with the callback.
11
12
  serialize_attributes :marshal, :data
13
+
14
+ # data that was provided by the merchan upon order creation and is sent back with the callback
15
+ serialize_attributes :marshal, :callback_data
16
+
17
+ # stores the response of the server to which the callback is issued
18
+ serialize_attributes :marshal, :callback_response
12
19
 
13
20
  plugin :after_initialize
14
21
  def after_initialize
@@ -59,7 +66,7 @@ module StraightServer
59
66
  end
60
67
 
61
68
  def to_h
62
- super.merge({ id: id, payment_id: payment_id, amount_in_btc: amount_in_btc(as: :string) })
69
+ super.merge({ id: id, payment_id: payment_id, amount_in_btc: amount_in_btc(as: :string), keychain_id: keychain_id, last_keychain_id: self.gateway.last_keychain_id })
63
70
  end
64
71
 
65
72
  def to_json
@@ -73,16 +80,16 @@ module StraightServer
73
80
  errors.add(:gateway_id, "is invalid") if !gateway_id.kind_of?(Numeric) || gateway_id <= 0
74
81
  errors.add(:description, "should be shorter than 255 charachters") if description.kind_of?(String) && description.length > 255
75
82
  errors.add(:gateway, "is inactive, cannot create order for inactive gateway") unless gateway.active
76
- validates_unique :id, :address, [:keychain_id, :gateway_id]
83
+ validates_unique :id
77
84
  validates_presence [:address, :keychain_id, :gateway_id, :amount]
78
85
  end
79
86
 
80
87
  def to_http_params
81
- "order_id=#{id}&amount=#{amount}&status=#{status}&address=#{address}&tid=#{tid}"
88
+ "order_id=#{id}&amount=#{amount}&amount_in_btc=#{amount_in_btc(as: :string)}&status=#{status}&address=#{address}&tid=#{tid}&keychain_id=#{keychain_id}&last_keychain_id=#{@gateway.last_keychain_id}"
82
89
  end
83
90
 
84
91
  def before_create
85
- self.payment_id = gateway.sign_with_secret("#{keychain_id}#{amount}#{created_at}")
92
+ self.payment_id = gateway.sign_with_secret("#{keychain_id}#{amount}#{created_at}#{(Order.max(:id) || 0)+1}")
86
93
 
87
94
  # Save info about current exchange rate at the time of purchase
88
95
  unless gateway.default_currency == 'BTC'
@@ -1,3 +1,5 @@
1
+ require_relative './throttler'
2
+
1
3
  module StraightServer
2
4
 
3
5
  class OrdersController
@@ -13,34 +15,52 @@ module StraightServer
13
15
  end
14
16
 
15
17
  def create
16
-
18
+
17
19
  unless @gateway
18
20
  StraightServer.logger.warn "Gateway not found"
19
21
  return [404, {}, "Gateway not found" ]
20
22
  end
21
23
 
24
+ unless @gateway.check_signature
25
+ ip = @env['HTTP_X_FORWARDED_FOR'].to_s
26
+ ip = @env['REMOTE_ADDR'] if ip.empty?
27
+ if StraightServer::Throttler.new(@gateway.id).deny?(ip)
28
+ StraightServer.logger.warn message = "Too many requests, please try again later"
29
+ return [429, {}, message]
30
+ end
31
+ end
32
+
22
33
  begin
34
+
35
+ # This is to inform users of previous version of a deprecated param
36
+ # It will have to be removed at some point.
37
+ if @params['order_id']
38
+ return [409, {}, "Error: order_id is no longer a valid param. Use keychain_id instead and consult the documentation." ]
39
+ end
40
+
23
41
  order_data = {
24
42
  amount: @params['amount'], # this is satoshi
25
43
  currency: @params['currency'],
26
44
  btc_denomination: @params['btc_denomination'],
27
- keychain_id: @params['order_id'],
45
+ keychain_id: @params['keychain_id'],
28
46
  signature: @params['signature'],
47
+ callback_data: @params['callback_data'],
29
48
  data: @params['data']
30
49
  }
31
50
  order = @gateway.create_order(order_data)
32
51
  StraightServer::Thread.new do
33
52
  # Because this is a new thread, we have to wrap the code inside in #watch_exceptions
34
- # once again. Otherwise, not watching is done. Oh, threads!
53
+ # once again. Otherwise, no watching is done. Oh, threads!
35
54
  StraightServer.logger.watch_exceptions do
36
55
  order.start_periodic_status_check
37
56
  end
38
57
  end
58
+ order = add_callback_data_warning(order)
39
59
  [200, {}, order.to_json ]
40
60
  rescue Sequel::ValidationFailed => e
41
61
  StraightServer.logger.warn(
42
62
  "VALIDATION ERRORS in order, cannot create it:\n" +
43
- "#{e.message.split(",").each_with_index.map { |e,i| "#{i+1}. #{e.lstrip}"}.join("\n") }\n" +
63
+ "#{e.message.split(",").each_with_index.map { |e,i| "#{i+1}. #{e.lstrip}"}.join("\n") }\n" +
44
64
  "Order data: #{order_data.inspect}\n"
45
65
  )
46
66
  [409, {}, "Invalid order: #{e.message}" ]
@@ -62,7 +82,8 @@ module StraightServer
62
82
  return [404, {}, "Gateway not found" ]
63
83
  end
64
84
 
65
- order = Order[@params['id']] || (@params['id'] =~ /[^\d]+/ && Order[:payment_id => @params['id']])
85
+ order = find_order
86
+
66
87
  if order
67
88
  order.status(reload: true)
68
89
  order.save if order.status_changed?
@@ -71,13 +92,8 @@ module StraightServer
71
92
  end
72
93
 
73
94
  def websocket
74
-
75
- order = if @params['id'] =~ /[^\d]+/
76
- Order[:payment_id => @params['id']]
77
- else
78
- Order[@params['id']]
79
- end
80
95
 
96
+ order = find_order
81
97
  if order
82
98
  begin
83
99
  @gateway.add_websocket_for_order ws = Faye::WebSocket.new(@env), order
@@ -93,7 +109,7 @@ module StraightServer
93
109
  private
94
110
 
95
111
  def dispatch
96
-
112
+
97
113
  StraightServer.logger.blank_lines
98
114
  StraightServer.logger.info "#{@method} #{@env['REQUEST_PATH']}\n#{@params}"
99
115
 
@@ -110,7 +126,23 @@ module StraightServer
110
126
  elsif @request_path[3].nil?# && @method == 'POST'
111
127
  create
112
128
  end
113
- @response = [404, {}, "#{@method} /#{@request_path.join('/')} Not found"] if @response.nil?
129
+ @response = [404, {}, "#{@method} /#{@request_path.join('/')} Not found"] if @response.nil?
130
+ end
131
+
132
+ def find_order
133
+ if @params['id'] =~ /[^\d]+/
134
+ Order[:payment_id => @params['id']]
135
+ else
136
+ Order[@params['id']]
137
+ end
138
+ end
139
+
140
+ def add_callback_data_warning(order)
141
+ o = order.to_h
142
+ if @params['data'].kind_of?(String) && @params['callback_data'].nil?
143
+ o[:WARNING] = "Maybe you meant to use callback_data? The API has changed now. Consult the documentation."
144
+ end
145
+ o
114
146
  end
115
147
 
116
148
  end
@@ -0,0 +1,63 @@
1
+ module StraightServer
2
+ class Throttler
3
+
4
+ def initialize(gateway_id)
5
+ @id = "gateway_#{gateway_id}"
6
+ @redis = Config.redis && Config.redis[:connection]
7
+ @limit = @period = @ip_ban_duration = 0
8
+ if Config.throttle
9
+ @limit = Config.throttle[:requests_limit].to_i
10
+ @period = Config.throttle[:period].to_i # in seconds
11
+ @ip_ban_duration = Config.throttle[:ip_ban_duration].to_i # in seconds
12
+ end
13
+ end
14
+
15
+ # @param [String] ip address
16
+ # @return [Boolean|Nil] true if request should be rejected,
17
+ # false if request should be served,
18
+ # nil if redis is not available
19
+ def deny?(ip)
20
+ banned?(ip) || throttled?(ip)
21
+ end
22
+
23
+ private
24
+
25
+ def throttled?(ip)
26
+ return false if @limit <= 0 || @period <= 0
27
+ return unless @redis
28
+ key = throttled_key(ip)
29
+ value = @redis.incr(key)
30
+ @redis.expire key, @period * 2
31
+ if value > @limit
32
+ ban ip
33
+ true
34
+ else
35
+ false
36
+ end
37
+ end
38
+
39
+ def banned?(ip)
40
+ return false if @ip_ban_duration <= 0
41
+ return unless @redis
42
+ value = @redis.get(banned_key(ip)).to_i
43
+ if value > 0
44
+ Time.now.to_i <= value + @ip_ban_duration
45
+ else
46
+ false
47
+ end
48
+ end
49
+
50
+ def ban(ip)
51
+ return if @ip_ban_duration <= 0
52
+ @redis.set banned_key(ip), Time.now.to_i, ex: @ip_ban_duration
53
+ end
54
+
55
+ def throttled_key(ip)
56
+ "#{Config.redis[:prefix]}:Throttle:#{@id}:#{@period}_#{@limit}:#{Time.now.to_i / @period}:#{ip}"
57
+ end
58
+
59
+ def banned_key(ip)
60
+ "#{Config.redis[:prefix]}:BannedIP:#{ip}"
61
+ end
62
+ end
63
+ end
data/lib/tasks/db.rake ADDED
@@ -0,0 +1,42 @@
1
+ require_relative '../straight-server'
2
+
3
+ namespace :db do
4
+ task :environment do
5
+ include StraightServer::Initializer
6
+ ConfigDir.set!
7
+ create_config_files
8
+ read_config_file
9
+ connect_to_db
10
+ end
11
+
12
+ desc "Migrates the database"
13
+ task :migrate, [:step] => :environment do |t, args|
14
+ target = args[:step] && (step = args[:step].to_i) > 0 ?
15
+ current_migration_version + step : nil
16
+
17
+ Sequel::Migrator.run(StraightServer.db_connection, MIGRATIONS_ROOT, target: target)
18
+ dump_schema
19
+ end
20
+
21
+ desc "Rollbacks database migrations"
22
+ task :rollback, [:step] => :environment do |t, args|
23
+ target = args[:step] && (step = args[:step].to_i) > 0 ?
24
+ current_migration_version - step : 0
25
+
26
+ Sequel::Migrator.run(StraightServer.db_connection, MIGRATIONS_ROOT, target: target)
27
+ dump_schema
28
+ end
29
+
30
+ def current_migration_version
31
+ db = StraightServer.db_connection
32
+
33
+ Sequel::Migrator.migrator_class(MIGRATIONS_ROOT).new(db, MIGRATIONS_ROOT, {}).current
34
+ end
35
+
36
+ def dump_schema
37
+ StraightServer.db_connection.extension :schema_dumper
38
+ open('db/schema.rb', 'w') do |f|
39
+ f.puts StraightServer.db_connection.dump_schema_migration(same_db: false)
40
+ end
41
+ end
42
+ end
@@ -4,11 +4,12 @@ gateways_source: config
4
4
  environment: test
5
5
  count_orders: true
6
6
  expiration_overtime: 0
7
+ reuse_address_orders_threshold: 5
7
8
 
8
9
  gateways:
9
10
 
10
11
  default:
11
- pubkey: 'xpub-000'
12
+ pubkey: 'xpub6Arp6y5VVQzq3LWTHz7gGsGKAdM697RwpWgauxmyCybncqoAYim6P63AasNKSy3VUAYXFj7tN2FZ9CM9W7yTfmerdtAPU4amuSNjEKyDeo6'
12
13
  confirmations_required: 0
13
14
  order_class: "StraightServer::Order"
14
15
  secret: 'secret'
@@ -22,7 +23,7 @@ gateways:
22
23
  - Bitstamp
23
24
  active: true
24
25
  second_gateway:
25
- pubkey: 'xpub-001'
26
+ pubkey: 'xpub6AH1Ymkkrwk3TaMrVrXBCpcGajKc9a1dAJBTKr1i4GwYLgLk7WDvPtN1o1cAqS5DZ9CYzn3gZtT7BHEP4Qpsz24UELTncPY1Zsscsm3ajmX'
26
27
  confirmations_required: 0
27
28
  order_class: "StraightServer::Order"
28
29
  secret: 'secret'
@@ -6,6 +6,7 @@ RSpec.describe StraightServer::Gateway do
6
6
  @gateway = StraightServer::GatewayOnConfig.find_by_id(1)
7
7
  @order_mock = double("order mock")
8
8
  allow(@order_mock).to receive(:old_status)
9
+ allow(@order_mock).to receive(:reused).and_return(0)
9
10
  [:id, :gateway=, :save, :to_h, :id=].each { |m| allow(@order_mock).to receive(m) }
10
11
  @order_for_keychain_id_args = { amount: 1, keychain_id: 1, currency: nil, btc_denomination: nil }
11
12
  end
@@ -47,6 +48,76 @@ RSpec.describe StraightServer::Gateway do
47
48
  expect(@gateway.blockchain_adapters.map(&:class)).to eq([Straight::Blockchain::BlockchainInfoAdapter, Straight::Blockchain::MyceliumAdapter])
48
49
  end
49
50
 
51
+ it "updates last_keychain_id to the new value provided in keychain_id if it's larger than the last_keychain_id" do
52
+ @gateway.create_order(amount: 2252.706, currency: 'USD', signature: hmac_sha256('100', 'secret'), keychain_id: 100)
53
+ expect(@gateway.last_keychain_id).to eq(100)
54
+ @gateway.create_order(amount: 2252.706, currency: 'USD', signature: hmac_sha256('150', 'secret'), keychain_id: 150)
55
+ expect(@gateway.last_keychain_id).to eq(150)
56
+ @gateway.create_order(amount: 2252.706, currency: 'USD', signature: hmac_sha256('50', 'secret'), keychain_id: 50)
57
+ end
58
+
59
+ context "reusing addresses" do
60
+
61
+ # Config.reuse_address_orders_threshold for the test env is 5
62
+
63
+ before(:each) do
64
+ @gateway = StraightServer::GatewayOnConfig.find_by_id(2)
65
+ allow(@gateway).to receive(:order_status_changed).with(anything).and_return([])
66
+ allow(@gateway).to receive(:fetch_transactions_for).with(anything).and_return([])
67
+ create_list(:order, 4, status: StraightServer::Order::STATUSES[:expired], gateway_id: @gateway.id)
68
+ create_list(:order, 2, status: StraightServer::Order::STATUSES[:paid], gateway_id: @gateway.id)
69
+ @expired_orders_1 = create_list(:order, 5, status: StraightServer::Order::STATUSES[:expired], gateway_id: @gateway.id)
70
+ @expired_orders_2 = create_list(:order, 2, status: StraightServer::Order::STATUSES[:expired], gateway_id: @gateway.id)
71
+ end
72
+
73
+ it "finds all expired orders that follow in a row" do
74
+ expect(@gateway.send(:find_expired_orders_row).size).to eq(5)
75
+ expect(@gateway.send(:find_expired_orders_row).map(&:id)).to include(*@expired_orders_1.map(&:id))
76
+ expect(@gateway.send(:find_expired_orders_row).map(&:id)).not_to include(*@expired_orders_2.map(&:id))
77
+ end
78
+
79
+ it "picks an expired order which address is going to be reused" do
80
+ expect(@gateway.find_reusable_order).to eq(@expired_orders_1.last)
81
+ end
82
+
83
+ it "picks an expired order which address is going to be reused only when this address received no transactions" do
84
+ allow(@gateway).to receive(:fetch_transactions_for).with(@expired_orders_1.last.address).and_return(['transaction'])
85
+ expect(@gateway.find_reusable_order).to eq(nil)
86
+ end
87
+
88
+ it "creates a new order with a reused address" do
89
+ reused_order = @expired_orders_1.last
90
+ order = @gateway.create_order(amount: 2252.706, currency: 'USD')
91
+ expect(order.keychain_id).to eq(reused_order.keychain_id)
92
+ expect(order.address).to eq(@gateway.address_for_keychain_id(reused_order.keychain_id))
93
+ expect(order.reused).to eq(1)
94
+ end
95
+
96
+ it "doesn't increment last_keychain_id if order is reused" do
97
+ last_keychain_id = @gateway.last_keychain_id
98
+ order = @gateway.create_order(amount: 2252.706, currency: 'USD')
99
+ expect(@gateway.last_keychain_id).to eq(last_keychain_id)
100
+
101
+ order.status = StraightServer::Order::STATUSES[:paid]
102
+ order.save
103
+ order_2 = @gateway.create_order(amount: 2252.706, currency: 'USD')
104
+ expect(@gateway.last_keychain_id).to eq(last_keychain_id+1)
105
+ end
106
+
107
+ it "after the reused order was paid, gives next order a new keychain_id" do
108
+ order = @gateway.create_order(amount: 2252.706, currency: 'USD')
109
+ order.status = StraightServer::Order::STATUSES[:expired]
110
+ order.save
111
+ expect(order.keychain_id).to eq(@expired_orders_1.last.keychain_id)
112
+
113
+ order = @gateway.create_order(amount: 2252.706, currency: 'USD')
114
+ order.status = StraightServer::Order::STATUSES[:paid]
115
+ order.save
116
+ expect(@gateway.send(:find_expired_orders_row).map(&:id)).to be_empty
117
+ end
118
+
119
+ end
120
+
50
121
  context "callback url" do
51
122
 
52
123
  before(:each) do
@@ -75,7 +146,7 @@ RSpec.describe StraightServer::Gateway do
75
146
  it "signs the callback if gateway has a secret" do
76
147
  @gateway = StraightServer::GatewayOnConfig.find_by_id(1) # Gateway 1 requires signatures
77
148
  expect(@response_mock).to receive(:code).twice.and_return("200")
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')}")
149
+ expect(URI).to receive(:parse).with('http://localhost:3000/payment-callback?' + @order.to_http_params + "&signature=#{hmac_sha256(@order.id, 'secret')}")
79
150
  expect(Net::HTTP).to receive(:get_response).and_return(@response_mock)
80
151
  @gateway.order_status_changed(@order)
81
152
  end
@@ -83,10 +154,10 @@ RSpec.describe StraightServer::Gateway do
83
154
  it "receives random data in :data params and sends it back in a callback request" do
84
155
  @order.data = 'some random data'
85
156
  expect(@gateway).to receive(:order_for_keychain_id).with(@order_for_keychain_id_args).once.and_return(@order)
86
- @gateway.create_order(amount: 1, data: 'some random data')
157
+ @gateway.create_order(amount: 1, callback_data: 'some random data')
87
158
  expect(@response_mock).to receive(:code).twice.and_return("200")
88
159
  expect(Net::HTTP).to receive(:get_response).and_return(@response_mock)
89
- expect(URI).to receive(:parse).with('http://localhost:3001/payment-callback?' + @order.to_http_params + "&data=#{@order.data}")
160
+ expect(URI).to receive(:parse).with('http://localhost:3001/payment-callback?' + @order.to_http_params + "&callback_data=#{@order.data}")
90
161
  @gateway.order_status_changed(@order)
91
162
  end
92
163
 
@@ -152,12 +223,12 @@ RSpec.describe StraightServer::Gateway do
152
223
  expect(gateway1).to be_kind_of(StraightServer::GatewayOnConfig)
153
224
  expect(gateway2).to be_kind_of(StraightServer::GatewayOnConfig)
154
225
 
155
- expect(gateway1.pubkey).to eq('xpub-000')
226
+ expect(gateway1.pubkey).to eq('xpub6Arp6y5VVQzq3LWTHz7gGsGKAdM697RwpWgauxmyCybncqoAYim6P63AasNKSy3VUAYXFj7tN2FZ9CM9W7yTfmerdtAPU4amuSNjEKyDeo6')
156
227
  expect(gateway1.confirmations_required).to eq(0)
157
228
  expect(gateway1.order_class).to eq("StraightServer::Order")
158
229
  expect(gateway1.name).to eq("default")
159
230
 
160
- expect(gateway2.pubkey).to eq('xpub-001')
231
+ expect(gateway2.pubkey).to eq('xpub6AH1Ymkkrwk3TaMrVrXBCpcGajKc9a1dAJBTKr1i4GwYLgLk7WDvPtN1o1cAqS5DZ9CYzn3gZtT7BHEP4Qpsz24UELTncPY1Zsscsm3ajmX')
161
232
  expect(gateway2.confirmations_required).to eq(0)
162
233
  expect(gateway2.order_class).to eq("StraightServer::Order")
163
234
  expect(gateway2.name).to eq("second_gateway")
@@ -166,7 +237,8 @@ RSpec.describe StraightServer::Gateway do
166
237
  it "saves and retrieves last_keychain_id from the file in the .straight dir" do
167
238
  @gateway.check_signature = false
168
239
  expect(File.read("#{ENV['HOME']}/.straight/default_last_keychain_id").to_i).to eq(0)
169
- @gateway.increment_last_keychain_id!
240
+ @gateway.update_last_keychain_id
241
+ @gateway.save
170
242
  expect(File.read("#{ENV['HOME']}/.straight/default_last_keychain_id").to_i).to eq(1)
171
243
 
172
244
  expect(@gateway).to receive(:order_for_keychain_id).with(@order_for_keychain_id_args.merge({ keychain_id: 2})).once.and_return(@order_mock)
@@ -201,7 +273,8 @@ RSpec.describe StraightServer::Gateway do
201
273
  @gateway.check_signature = false
202
274
  @gateway.save
203
275
  expect(DB[:gateways][:name => 'default'][:last_keychain_id]).to eq(0)
204
- @gateway.increment_last_keychain_id!
276
+ @gateway.update_last_keychain_id
277
+ @gateway.save
205
278
  expect(DB[:gateways][:name => 'default'][:last_keychain_id]).to eq(1)
206
279
 
207
280
  expect(@gateway).to receive(:order_for_keychain_id).with(@order_for_keychain_id_args.merge({ keychain_id: 2})).once.and_return(@order_mock)
@@ -210,11 +283,17 @@ RSpec.describe StraightServer::Gateway do
210
283
  end
211
284
 
212
285
  it "encryptes and decrypts the gateway secret" do
213
- expect(@gateway.send(:encrypt_secret)).to eq("96c1c24edff5c1c2:6THJEZqg+2qlDhtWE2Tytg==")
214
- expect(@gateway.send(:decrypt_secret)).to eq("secret")
286
+ expect(@gateway.save)
287
+ expect(@gateway[:secret]).to eq("96c1c24edff5c1c2:6THJEZqg+2qlDhtWE2Tytg==")
215
288
  expect(@gateway.secret).to eq("secret")
216
289
  end
217
290
 
291
+ it "re-encrypts the new gateway secrect if it was changed" do
292
+ @gateway.save
293
+ @gateway.update(secret: 'new secret', update_secret: true)
294
+ expect(@gateway.secret).to eq("new secret")
295
+ end
296
+
218
297
  it "finds orders using #find_by_id method which is essentially an alias for Gateway[]" do
219
298
  @gateway.save
220
299
  expect(StraightServer::GatewayOnDB.find_by_id(@gateway.id)).to eq(@gateway)
@@ -13,6 +13,7 @@ RSpec.describe StraightServer::Order do
13
13
  allow(@gateway).to receive(:increment_order_counter!)
14
14
  allow(@gateway).to receive(:current_exchange_rate).and_return(111)
15
15
  allow(@gateway).to receive(:default_currency).and_return('USD')
16
+ allow(@gateway).to receive(:last_keychain_id).and_return(222)
16
17
  @order = create(:order, gateway_id: @gateway.id)
17
18
  allow(@gateway).to receive(:fetch_transactions_for).with(anything).and_return([])
18
19
  allow(@gateway).to receive(:order_status_changed).with(anything)
@@ -28,11 +29,11 @@ RSpec.describe StraightServer::Order do
28
29
 
29
30
  it "prepares data as http params" do
30
31
  allow(@order).to receive(:tid).and_return("tid1")
31
- expect(@order.to_http_params).to eq("order_id=#{@order.id}&amount=10&status=#{@order.status}&address=#{@order.address}&tid=tid1")
32
+ expect(@order.to_http_params).to eq("order_id=#{@order.id}&amount=10&amount_in_btc=#{@order.amount_in_btc(as: :string)}&status=#{@order.status}&address=#{@order.address}&tid=tid1&keychain_id=#{@order.keychain_id}&last_keychain_id=#{@order.gateway.last_keychain_id}")
32
33
  end
33
34
 
34
35
  it "generates a payment_id" do
35
- expect(@order.payment_id).to eq(@order.gateway.sign_with_secret("#{@order.id}#{@order.amount}#{@order.created_at}"))
36
+ expect(@order.payment_id).not_to be_nil
36
37
  end
37
38
 
38
39
  it "starts a periodic status check but subtracts the time passed from order creation from the duration of the check" do
@@ -72,6 +73,11 @@ RSpec.describe StraightServer::Order do
72
73
  expect(order.data[:exchange_rate]).to eq({ price: 111, currency: 'USD' })
73
74
  end
74
75
 
76
+ it "returns last_keychain_id for the gateway along with other order data" do
77
+ order = create(:order, gateway_id: @gateway.id)
78
+ expect(order.to_h).to include(keychain_id: order.keychain_id, last_keychain_id: @gateway.last_keychain_id)
79
+ end
80
+
75
81
  describe "DB interaction" do
76
82
 
77
83
  it "saves a new order into the database" do
@@ -111,17 +117,6 @@ RSpec.describe StraightServer::Order do
111
117
  expect( -> { create(:order, id: order.id, gateway_id: @gateway.id) }).to raise_error()
112
118
  end
113
119
 
114
- it "doesn't save order if the order with the same address exists" do
115
- order = create(:order, gateway_id: @gateway.id)
116
- expect( -> { create(:order, address: order.address) }).to raise_error()
117
- end
118
-
119
- it "doesn't save order if the order with the same keychain_id and gateway_id exists" do
120
- order = create(:order, gateway_id: @gateway.id)
121
- expect( -> { create(:order, keychain_id: order.id, gateway_id: order.gateway_id+1) }).not_to raise_error()
122
- expect( -> { create(:order, keychain_id: order.id, gateway_id: order.gateway_id) }).to raise_error()
123
- end
124
-
125
120
  it "doesn't save order if the amount is invalid" do
126
121
  expect( -> { create(:order, amount: 0) }).to raise_error()
127
122
  end
@@ -15,7 +15,7 @@ RSpec.describe StraightServer::OrdersController do
15
15
  it "creates an order and renders its attrs in json" do
16
16
  allow(StraightServer::Thread).to receive(:new) # ignore periodic status checks, we're not testing it here
17
17
  send_request "POST", '/gateways/2/orders', amount: 10
18
- expect(response).to render_json_with(status: 0, amount: 10, address: "address1", tid: nil, id: :anything)
18
+ expect(response).to render_json_with(status: 0, amount: 10, address: "address1", tid: nil, id: :anything, keychain_id: @gateway.last_keychain_id, last_keychain_id: @gateway.last_keychain_id)
19
19
  end
20
20
 
21
21
  it "renders 409 error when an order cannot be created due to some validation errors" do
@@ -32,10 +32,11 @@ RSpec.describe StraightServer::OrdersController do
32
32
  send_request "POST", '/gateways/2/orders', amount: 10
33
33
  end
34
34
 
35
- it "passes data param to Order which then saves it serialized" do
35
+ it "passes data and callback_data param to Order which then saves it serialized" do
36
36
  allow(StraightServer::Thread).to receive(:new) # ignore periodic status checks, we're not testing it here
37
- send_request "POST", '/gateways/2/orders', amount: 10, data: { hello: 'world' }
37
+ send_request "POST", '/gateways/2/orders', amount: 10, data: { hello: 'world' }, callback_data: 'some random data'
38
38
  expect(StraightServer::Order.last.data.hello).to eq('world')
39
+ expect(StraightServer::Order.last.callback_data).to eq('some random data')
39
40
  end
40
41
 
41
42
  it "renders 503 page when the gateway is inactive" do
@@ -43,6 +44,7 @@ RSpec.describe StraightServer::OrdersController do
43
44
  send_request "POST", '/gateways/2/orders', amount: 1
44
45
  expect(response[0]).to eq(503)
45
46
  expect(response[2]).to eq("The gateway is inactive, you cannot create order with it")
47
+ @gateway.active = true
46
48
  end
47
49
 
48
50
  it "finds gateway using hashed_id" do
@@ -50,6 +52,37 @@ RSpec.describe StraightServer::OrdersController do
50
52
  send_request "POST", "/gateways/#{@gateway.id}/orders", amount: 10
51
53
  end
52
54
 
55
+ it "warns about a deprecated order_id param" do
56
+ send_request "POST", "/gateways/#{@gateway.id}/orders", amount: 10, order_id: 1
57
+ expect(response[2]).to eq("Error: order_id is no longer a valid param. Use keychain_id instead and consult the documentation.")
58
+ end
59
+
60
+ it 'limits creation of orders without signature' do
61
+ new_config = StraightServer::Config.dup
62
+ new_config.throttle = {requests_limit: 1, period: 1}
63
+ stub_const 'StraightServer::Config', new_config
64
+ allow(StraightServer::Thread).to receive(:new)
65
+
66
+ send_request "POST", '/gateways/2/orders', amount: 10
67
+ expect(response).to render_json_with(status: 0, amount: 10, address: "address1", tid: nil, id: :anything, keychain_id: @gateway.last_keychain_id, last_keychain_id: @gateway.last_keychain_id)
68
+ send_request "POST", '/gateways/2/orders', amount: 10
69
+ expect(response).to eq [429, {}, "Too many requests, please try again later"]
70
+
71
+ @gateway1 = StraightServer::Gateway.find_by_id(1)
72
+ @gateway1.check_signature = true
73
+ 5.times do |i|
74
+ i += 1
75
+ send_request "POST", '/gateways/1/orders', amount: 10, keychain_id: i, signature: @gateway1.sign_with_secret(i)
76
+ expect(response[0]).to eq 200
77
+ expect(response).to render_json_with(status: 0, amount: 10, tid: nil, id: :anything, keychain_id: i, last_keychain_id: i)
78
+ end
79
+ end
80
+
81
+ it "warns you about the use of callback_data instead of data" do
82
+ allow(StraightServer::Thread).to receive(:new)
83
+ send_request "POST", '/gateways/2/orders', amount: 10, data: "I meant this to be callback_data"
84
+ expect(response).to render_json_with(WARNING: "Maybe you meant to use callback_data? The API has changed now. Consult the documentation.")
85
+ end
53
86
  end
54
87
 
55
88
  describe "show action" do
@@ -83,7 +116,6 @@ RSpec.describe StraightServer::OrdersController do
83
116
 
84
117
  it "finds order by payment_id" do
85
118
  allow(@order_mock).to receive(:status_changed?).and_return(false)
86
- expect(StraightServer::Order).to receive(:[]).with('payment_id').and_return(nil)
87
119
  expect(StraightServer::Order).to receive(:[]).with(:payment_id => 'payment_id').and_return(@order_mock)
88
120
  send_request "GET", '/gateways/2/orders/payment_id'
89
121
  expect(response).to eq([200, {}, "order json mock"])