straight-server 0.2.2 → 0.2.3

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 (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"])