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