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.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +5 -2
- data/Gemfile.travis +2 -0
- data/README.md +46 -19
- data/Rakefile +5 -2
- data/VERSION +1 -1
- data/db/migrations/003_add_payment_id_to_orders.rb +1 -1
- data/db/migrations/004_add_description_to_orders.rb +1 -1
- data/db/migrations/005_add_orders_expiration_period_to_gateways.rb +1 -1
- data/db/migrations/006_add_check_order_status_in_db_first_to_gateways.rb +1 -1
- data/db/migrations/007_add_active_switcher_to_gateways.rb +1 -1
- data/db/migrations/008_add_order_counters_to_gateways.rb +1 -1
- data/db/migrations/009_add_hashed_id_to_gateways.rb +2 -2
- data/db/migrations/010_add_address_reusability_orders.rb +19 -0
- data/db/migrations/011_add_callback_data_to_orders.rb +11 -0
- data/db/schema.rb +55 -0
- data/examples/client/client.html +1 -1
- data/examples/client/client.js +1 -1
- data/lib/straight-server/config.rb +3 -1
- data/lib/straight-server/gateway.rb +133 -34
- data/lib/straight-server/initializer.rb +8 -7
- data/lib/straight-server/order.rb +12 -5
- data/lib/straight-server/orders_controller.rb +45 -13
- data/lib/straight-server/throttler.rb +63 -0
- data/lib/tasks/db.rake +42 -0
- data/spec/.straight/config.yml +3 -2
- data/spec/lib/gateway_spec.rb +88 -9
- data/spec/lib/order_spec.rb +8 -13
- data/spec/lib/orders_controller_spec.rb +36 -4
- data/spec/lib/throttle_spec.rb +52 -0
- data/straight-server.gemspec +13 -6
- data/templates/config.yml +19 -2
- metadata +22 -6
- data/bin/goliath.log +0 -6
- 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
|
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',
|
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
|
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
|
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',
|
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,
|
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,
|
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
|
-
|
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
|
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['
|
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,
|
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 =
|
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
|
data/spec/.straight/config.yml
CHANGED
@@ -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: '
|
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: '
|
26
|
+
pubkey: 'xpub6AH1Ymkkrwk3TaMrVrXBCpcGajKc9a1dAJBTKr1i4GwYLgLk7WDvPtN1o1cAqS5DZ9CYzn3gZtT7BHEP4Qpsz24UELTncPY1Zsscsm3ajmX'
|
26
27
|
confirmations_required: 0
|
27
28
|
order_class: "StraightServer::Order"
|
28
29
|
secret: 'secret'
|
data/spec/lib/gateway_spec.rb
CHANGED
@@ -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(
|
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,
|
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 + "&
|
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('
|
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('
|
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.
|
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.
|
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.
|
214
|
-
expect(@gateway
|
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)
|
data/spec/lib/order_spec.rb
CHANGED
@@ -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).
|
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"])
|