Initial commit
This commit is contained in:
77
spec/api/v2/management/accounts_spec.rb
Normal file
77
spec/api/v2/management/accounts_spec.rb
Normal file
@@ -0,0 +1,77 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Deposits, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_accounts: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'get balance' do
|
||||
def request
|
||||
post_json '/api/v2/management/accounts/balance', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { { uid: member.uid, currency: 'usd'} }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:member) { create(:member, :barong) }
|
||||
|
||||
before do
|
||||
deposit = create(:deposit_usd, member: member)
|
||||
deposit.accept
|
||||
end
|
||||
|
||||
it 'returns the correct status code' do
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'contains the correct response data' do
|
||||
request
|
||||
expect(JSON.parse(response.body)).to include(
|
||||
'balance' => member.get_account(data[:currency]).balance.to_s,
|
||||
'locked' => member.get_account(data[:currency]).locked.to_s
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'get balances' do
|
||||
def request
|
||||
post_json '/api/v2/management/accounts/balances', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { { currency: 'usd'} }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let!(:members) { create_list(:member, 12, :barong) }
|
||||
let!(:deposits) do
|
||||
members.each do |member|
|
||||
create(:deposit_usd, member: member).accept
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the correct status code' do
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'paginates' do
|
||||
balances = Account.order(id: :asc).pluck(:balance)
|
||||
data.merge!(page: 1, limit: 4)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).map { |x| x.fetch('balance').to_f }).to eq balances[0...4].map(&:to_f)
|
||||
data.merge!(page: 3, limit: 4)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).map { |x| x.fetch('balance').to_f }).to eq balances[8...12].map(&:to_f)
|
||||
end
|
||||
|
||||
it 'contains the correct response data' do
|
||||
request
|
||||
expect(JSON.parse(response.body).map { |x| x.fetch('balance').to_f }).to eq Account.order(id: :asc).pluck(:balance).map(&:to_f)
|
||||
end
|
||||
end
|
||||
end
|
||||
374
spec/api/v2/management/beneficiaries_spec.rb
Normal file
374
spec/api/v2/management/beneficiaries_spec.rb
Normal file
@@ -0,0 +1,374 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Beneficiaries, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_beneficiaries: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
write_beneficiaries: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'beneficiary list' do
|
||||
def request
|
||||
post_json '/api/v2/management/beneficiaries/list', multisig_jwt_management_api_v1({ data: beneficiary_data }, *signers)
|
||||
end
|
||||
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:member) { create(:member, :level_3) }
|
||||
let(:beneficiary_data) do
|
||||
{
|
||||
uid: member.uid,
|
||||
}
|
||||
end
|
||||
|
||||
let!(:pending_beneficiaries_for_member) do
|
||||
create_list(:beneficiary, 2, member: member, state: :pending)
|
||||
end
|
||||
|
||||
let!(:active_beneficiaries_for_member) do
|
||||
create_list(:beneficiary, 3, member: member, state: :active)
|
||||
end
|
||||
|
||||
let!(:archived_beneficiaries_for_member) do
|
||||
create_list(:beneficiary, 2, member: member, state: :archived)
|
||||
end
|
||||
|
||||
let!(:other_member_beneficiaries) do
|
||||
create_list(:beneficiary, 5)
|
||||
end
|
||||
|
||||
context 'missing required params' do
|
||||
it do
|
||||
beneficiary_data.except!(:uid)
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(JSON.parse(response.body)['error']).to match(/uid is missing/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without currency and state' do
|
||||
it do
|
||||
request
|
||||
expect(response.status).to eq 200
|
||||
total_for_member = pending_beneficiaries_for_member.count + active_beneficiaries_for_member.count
|
||||
expect(response_body.size).to eq total_for_member
|
||||
end
|
||||
end
|
||||
|
||||
context 'non-existing currency' do
|
||||
it do
|
||||
beneficiary_data.merge!(currency: :uah)
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.currency.doesnt_exist/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'existing currency' do
|
||||
let!(:btc_beneficiaries_for_member) do
|
||||
create_list(:beneficiary, 3, member: member)
|
||||
end
|
||||
|
||||
it do
|
||||
beneficiary_data.merge!(currency: :btc)
|
||||
request
|
||||
expect(response.status).to eq 200
|
||||
expect(response_body.all? { |b| b['currency'] == 'btc' }).to be_truthy
|
||||
end
|
||||
|
||||
context 'fiat currency' do
|
||||
let!(:usd_beneficiary_for_member) { create(:beneficiary, currency: Currency.find('usd'), member: member) }
|
||||
|
||||
it do
|
||||
beneficiary_data.merge!(currency: :usd)
|
||||
request
|
||||
expect(response.status).to eq 200
|
||||
expect(response_body.all? { |b| b['currency'] == 'usd' }).to be_truthy
|
||||
expect(usd_beneficiary_for_member.id).to eq response_body.first['id']
|
||||
expect(usd_beneficiary_for_member.data[:account_number]).to eq response_body.first['data']['account_number']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid state' do
|
||||
it do
|
||||
beneficiary_data.merge!(state: :invalid)
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.beneficiary.invalid_state/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'existing state' do
|
||||
it do
|
||||
beneficiary_data.merge!(state: :pending)
|
||||
request
|
||||
expect(response.status).to eq 200
|
||||
expect(response_body.all? { |b| b['state'] == 'pending' }).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'both currency and state' do
|
||||
let!(:active_btc_beneficiaries_for_member) do
|
||||
create_list(:beneficiary, 3, member: member, state: :active)
|
||||
end
|
||||
|
||||
it do
|
||||
beneficiary_data.merge!(currency: :btc, state: :active)
|
||||
request
|
||||
expect(response.status).to eq 200
|
||||
expect(response_body.all? { |b| b['currency'] == 'btc' && b['state'] == 'active' }).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'create beneficiary' do
|
||||
def request
|
||||
post_json '/api/v2/management/beneficiaries', multisig_jwt_management_api_v1({ data: beneficiary_data }, *signers)
|
||||
end
|
||||
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:member) { create(:member, :level_3) }
|
||||
let(:beneficiary_data) do
|
||||
{
|
||||
currency: :btc,
|
||||
name: 'Personal Bitcoin wallet',
|
||||
description: 'Multisignature Bitcoin Wallet',
|
||||
uid: member.uid,
|
||||
state: 'active',
|
||||
data: {
|
||||
address: Faker::Blockchain::Bitcoin.address
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
context 'invalid params' do
|
||||
context 'missing required params' do
|
||||
%i[currency name data uid].each do |rp|
|
||||
context rp do
|
||||
it do
|
||||
beneficiary_data.except!(rp)
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(JSON.parse(response.body)['error']).to match(/#{rp} is missing/i)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'currency doesn\'t exist' do
|
||||
it do
|
||||
beneficiary_data.merge!(currency: :uah)
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.currency.doesnt_exist/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'name is too long' do
|
||||
it do
|
||||
beneficiary_data.merge!(name: Faker::Lorem.sentence(500))
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.beneficiary.too_long_name/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid state' do
|
||||
it do
|
||||
beneficiary_data.merge!(state: 'test')
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.beneficiary.invalid_state/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'description is too long' do
|
||||
it do
|
||||
beneficiary_data.merge!(description: Faker::Lorem.sentence(500))
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.beneficiary.too_long_description/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'data has invalid type' do
|
||||
it do
|
||||
beneficiary_data.merge!(data: 'data')
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.beneficiary.non_json_data/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'crypto beneficiary' do
|
||||
context 'nil address in data' do
|
||||
it do
|
||||
beneficiary_data[:data][:address] = nil
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.beneficiary.missing_address_in_data/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'data without address' do
|
||||
it do
|
||||
beneficiary_data[:data].delete(:address)
|
||||
beneficiary_data[:data][:memo] = :memo
|
||||
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.beneficiary.missing_address_in_data/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'disabled withdrawal for currency' do
|
||||
let(:currency) { Currency.find(:btc) }
|
||||
before do
|
||||
currency.update(withdrawal_enabled: false)
|
||||
end
|
||||
it do
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.currency.withdrawal_disabled/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid character in address' do
|
||||
before do
|
||||
beneficiary_data[:data][:address] = "'" + Faker::Blockchain::Bitcoin.address
|
||||
end
|
||||
it do
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.beneficiary.failed_to_create/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'duplicated address' do
|
||||
context 'same currency' do
|
||||
before do
|
||||
create(:beneficiary,
|
||||
member: member,
|
||||
currency_id: beneficiary_data[:currency],
|
||||
data: {address: beneficiary_data.dig(:data, :address)})
|
||||
end
|
||||
|
||||
it do
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.beneficiary.duplicate_address/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'different currencies' do
|
||||
before do
|
||||
create(:beneficiary,
|
||||
member: member,
|
||||
currency_id: :eth,
|
||||
data: {address: beneficiary_data.dig(:data, :address)})
|
||||
end
|
||||
|
||||
it do
|
||||
request
|
||||
expect(response.status).to eq 201
|
||||
end
|
||||
end
|
||||
|
||||
context 'truncates spaces in address' do
|
||||
let(:address) { Faker::Blockchain::Bitcoin.address }
|
||||
|
||||
before do
|
||||
beneficiary_data[:data][:address] = " " + address + " "
|
||||
end
|
||||
it do
|
||||
request
|
||||
expect(response.status).to eq 201
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(Beneficiary.find(result['id']).data['address']).to eq(address)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'destination tag in address' do
|
||||
before do
|
||||
beneficiary_data[:data][:address] = Faker::Blockchain::Bitcoin.address + "?dt=4"
|
||||
end
|
||||
it do
|
||||
request
|
||||
expect(response.status).to eq 201
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(Beneficiary.find(result['id']).data['address']).to eq(beneficiary_data[:data][:address])
|
||||
end
|
||||
end
|
||||
|
||||
context 'fiat beneficiary' do
|
||||
let(:beneficiary_data) do
|
||||
{
|
||||
currency: :usd,
|
||||
uid: member.uid,
|
||||
name: Faker::Bank.name,
|
||||
description: Faker::Company.catch_phrase,
|
||||
data: generate(:fiat_beneficiary_data)
|
||||
}
|
||||
end
|
||||
|
||||
context 'nil address in data' do
|
||||
it do
|
||||
beneficiary_data[:data].except!(:address)
|
||||
request
|
||||
expect(response.status).to eq 201
|
||||
expect(response_body['data']).to eq beneficiary_data[:data].with_indifferent_access
|
||||
end
|
||||
end
|
||||
|
||||
context 'nil data' do
|
||||
it do
|
||||
beneficiary_data[:data] = nil
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(JSON.parse(response.body)['error']).to match(/data is empty/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'duplicated address' do
|
||||
context 'same currency' do
|
||||
before do
|
||||
create(:beneficiary,
|
||||
member: member,
|
||||
currency_id: beneficiary_data[:currency],
|
||||
data: beneficiary_data[:data])
|
||||
end
|
||||
|
||||
it do
|
||||
request
|
||||
expect(response.status).to eq 201
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'valid params' do
|
||||
it 'creates beneficiary for member' do
|
||||
expect do
|
||||
request
|
||||
end.to change{ member.beneficiaries.count }.by(1)
|
||||
end
|
||||
|
||||
it 'creates beneficiary with active state' do
|
||||
request
|
||||
expect(response.status).to eq 201
|
||||
id = response_body['id']
|
||||
expect(Beneficiary.find_by!(id: id).state).to eq 'active'
|
||||
expect(Beneficiary.find_by!(id: id).data).to eq response_body['data']
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
203
spec/api/v2/management/currencies_spec.rb
Normal file
203
spec/api/v2/management/currencies_spec.rb
Normal file
@@ -0,0 +1,203 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Currencies, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_currencies: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
write_currencies: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'get currency by code' do
|
||||
def request
|
||||
post_json "/api/v2/management/currencies/#{currency.code}", multisig_jwt_management_api_v1({ data: {} }, *signers)
|
||||
end
|
||||
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:currency) { Currency.find(:usd) }
|
||||
|
||||
it 'returns currency by code' do
|
||||
request
|
||||
expect(JSON.parse(response.body).fetch('id')).to eq currency.code
|
||||
end
|
||||
|
||||
context 'currency code with dot' do
|
||||
let!(:currency) { create(:currency, :xagm_cx) }
|
||||
|
||||
it 'returns currency by code' do
|
||||
request
|
||||
expect(JSON.parse(response.body).fetch('id')).to eq currency.code
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'update currency' do
|
||||
def request
|
||||
put_json '/api/v2/management/currencies/update', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { {} }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:currency) { Currency.find(:btc) }
|
||||
|
||||
it 'should validate deposit_fee param' do
|
||||
data.merge!(id: currency.id, deposit_fee: -10.0)
|
||||
request
|
||||
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.invalid_deposit_fee/i)
|
||||
end
|
||||
|
||||
it 'should validate min_deposit_amount param' do
|
||||
data.merge!(id: currency.id, min_deposit_amount: -123.0)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.invalid_min_deposit_amount/i)
|
||||
end
|
||||
|
||||
it 'should validate min_collection_amount param' do
|
||||
data.merge!(id: currency.id, min_collection_amount: -100.0)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.invalid_min_collection_amount/i)
|
||||
end
|
||||
|
||||
it 'should validate withdraw_fee param' do
|
||||
data.merge!(id: currency.id, withdraw_fee: -100.0)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.invalid_withdraw_fee/i)
|
||||
end
|
||||
|
||||
it 'should validate min_withdraw_amount param' do
|
||||
data.merge!(id: currency.id, min_withdraw_amount: -1)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.invalid_min_withdraw_amount/i)
|
||||
end
|
||||
|
||||
it 'should validate withdraw_limit_24h param' do
|
||||
data.merge!(id: currency.id, withdraw_limit_24h: -1)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.invalid_withdraw_limit_24h/i)
|
||||
end
|
||||
|
||||
it 'should validate withdraw_limit_72h param' do
|
||||
data.merge!(id: currency.id, withdraw_limit_72h: -1)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.invalid_withdraw_limit_72h/i)
|
||||
end
|
||||
|
||||
it 'should validate options param' do
|
||||
data.merge!(id: currency.id, options: 'blah-blah')
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/non_json_options/i)
|
||||
end
|
||||
|
||||
it 'should validate visible param' do
|
||||
data.merge!(id: currency.id, visible: 'blah-blah')
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.non_boolean_visible/i)
|
||||
end
|
||||
|
||||
it 'should validate position param' do
|
||||
data.merge!(id: currency.id, position: 0)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.invalid_position/i)
|
||||
end
|
||||
|
||||
it 'should validate deposit_enabled param' do
|
||||
data.merge!(id: currency.id, deposit_enabled: 'blah-blah')
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.non_boolean_deposit_enabled/i)
|
||||
end
|
||||
|
||||
it 'should validate withdrawal_enabled param' do
|
||||
data.merge!(id: currency.id, withdrawal_enabled: 'blah-blah')
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/management.currency.non_boolean_withdrawal_enabled/i)
|
||||
end
|
||||
|
||||
it 'should check required params' do
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/id is missing/i)
|
||||
end
|
||||
|
||||
it 'should update currency' do
|
||||
data.merge!(id: currency.id, visible: 'true', withdraw_fee: '0.1')
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 200
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result.fetch('id')).to eq currency.id
|
||||
expect(result.fetch('visible')).to eq true
|
||||
expect(result.fetch('withdraw_fee')).to eq '0.1'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v2/management/currencies/list' do
|
||||
def request
|
||||
post_json "/api/v2/management/currencies/list", multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) { {} }
|
||||
|
||||
it 'lists visible currencies' do
|
||||
request
|
||||
expect(response).to have_http_status 200
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result.size).to eq Currency.count
|
||||
end
|
||||
|
||||
it 'lists visible coins' do
|
||||
data.merge!(type: 'coin')
|
||||
request
|
||||
expect(response).to have_http_status 200
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result.size).to eq Currency.coins.size
|
||||
end
|
||||
|
||||
it 'lists visible fiats' do
|
||||
data.merge!(type: 'fiat')
|
||||
request
|
||||
expect(response).to have_http_status 200
|
||||
|
||||
result = JSON.parse(response.body, symbolize_names: true)
|
||||
expect(result.size).to eq Currency.fiats.size
|
||||
expect(result.dig(0, :id)).to eq 'usd'
|
||||
end
|
||||
|
||||
it 'returns error in case of invalid type' do
|
||||
data.merge!(type: 'invalid')
|
||||
request
|
||||
expect(response).to have_http_status 422
|
||||
end
|
||||
end
|
||||
end
|
||||
269
spec/api/v2/management/deposits_spec.rb
Normal file
269
spec/api/v2/management/deposits_spec.rb
Normal file
@@ -0,0 +1,269 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Deposits, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_deposits: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
write_deposits: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'list deposits' do
|
||||
def request
|
||||
post_json '/api/v2/management/deposits', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { {} }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:members) { create_list(:member, 2, :barong) }
|
||||
|
||||
before do
|
||||
Deposit::STATES.tap do |states|
|
||||
(states.count * 2).times do
|
||||
create(:deposit_btc, member: members.sample, aasm_state: states.sample)
|
||||
create(:deposit_usd, member: members.sample, aasm_state: states.sample)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns deposits' do
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq Deposit.order(id: :desc).pluck(:tid)
|
||||
end
|
||||
|
||||
it 'paginates' do
|
||||
ids = Deposit.order(id: :desc).pluck(:tid)
|
||||
data.merge!(page: 1, limit: 4)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq ids[0...4]
|
||||
data.merge!(page: 3, limit: 4)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq ids[8...12]
|
||||
end
|
||||
|
||||
it 'filters by state' do
|
||||
Deposit::STATES.each do |state|
|
||||
data.merge!(state: state)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).count).to eq Deposit.where(aasm_state: state).count
|
||||
end
|
||||
end
|
||||
|
||||
it 'filters by member' do
|
||||
member = members.last
|
||||
data.merge!(uid: member.uid)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).count).to eq member.deposits.count
|
||||
end
|
||||
|
||||
it 'filters by currency' do
|
||||
data.merge!(currency: :usd)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).count).to eq Deposit.where(currency_id: :usd).count
|
||||
end
|
||||
end
|
||||
|
||||
describe 'create deposits' do
|
||||
let(:member) { create(:member, :barong) }
|
||||
let(:currency) { Currency.find(:usd) }
|
||||
let!(:account) { member.get_account(:usd) }
|
||||
let(:amount) { 750.77 }
|
||||
let :data do
|
||||
{ uid: member.uid,
|
||||
currency: currency.code,
|
||||
amount: amount.to_s }
|
||||
end
|
||||
let(:signers) { %i[alex jeff] }
|
||||
|
||||
def request
|
||||
post_json '/api/v2/management/deposits/new', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
it 'creates new fiat deposit with state «submitted»' do
|
||||
request
|
||||
expect(response.status).to eq 201
|
||||
record = Deposit.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.amount).to eq 750.77
|
||||
expect(record.aasm_state).to eq 'submitted'
|
||||
expect(record.account).to eq member.get_account(currency)
|
||||
expect(response_body['transfer_type']).to eq 'fiat'
|
||||
end
|
||||
|
||||
it 'can create fiat deposit and immediately accept it' do
|
||||
data.merge!(state: :accepted)
|
||||
expect { request }.to change { member.get_account(currency).balance }.by amount
|
||||
end
|
||||
|
||||
it 'denies access unless enough signatures are supplied' do
|
||||
data.merge!(state: :accepted)
|
||||
signers.clear.concat %i[james jeff]
|
||||
expect { request }.not_to(change { member.get_account(currency).balance })
|
||||
expect(response.status).to eq 401
|
||||
end
|
||||
|
||||
it 'validates member' do
|
||||
data.delete(:uid)
|
||||
request
|
||||
expect(response.body).to match(/uid is missing/i)
|
||||
data[:uid] = '1234567890'
|
||||
request
|
||||
expect(response.body).to match(/member must exist/i)
|
||||
end
|
||||
|
||||
it 'validates currency' do
|
||||
data.delete(:currency)
|
||||
request
|
||||
expect(response.body).to match(/currency is missing/i)
|
||||
data[:currency] = 'btc'
|
||||
request
|
||||
expect(response.body).to match(/currency does not have a valid value/i)
|
||||
end
|
||||
|
||||
it 'validates amount' do
|
||||
data.delete(:amount)
|
||||
request
|
||||
expect(response.body).to match(/amount is missing/i)
|
||||
data[:amount] = '-340.50'
|
||||
request
|
||||
expect(response.body).to match(/amount must be greater than 0/i)
|
||||
end
|
||||
|
||||
it 'validates state' do
|
||||
data[:state] = 'submitted'
|
||||
request
|
||||
expect(response.body).to match(/state does not have a valid value/i)
|
||||
end
|
||||
|
||||
context 'when coin instead of fiat is supplied' do
|
||||
let(:currency) { Currency.find(:btc) }
|
||||
it 'doesn\'t work' do
|
||||
request
|
||||
expect(response.body).to match(/currency does not have a valid value/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'extremely precise values' do
|
||||
it 'keeps precision for amount' do
|
||||
data.merge!(amount: '0.0000000123456789')
|
||||
request
|
||||
expect(response).to have_http_status(201)
|
||||
expect(Deposit.last.amount.to_s).to eq data[:amount]
|
||||
end
|
||||
end
|
||||
|
||||
context 'disabled currency' do
|
||||
before do
|
||||
currency.update(deposit_enabled: false)
|
||||
end
|
||||
|
||||
it 'returns error for enabled disabled deposit' do
|
||||
request
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response).to include_api_error('management.currency.deposit_disabled')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'get deposit' do
|
||||
def request
|
||||
post_json '/api/v2/management/deposits/get', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) { { tid: record.tid } }
|
||||
let(:record) { create(:deposit_btc, member: member) }
|
||||
let(:member) { create(:member, :barong) }
|
||||
|
||||
it 'returns deposit by TID' do
|
||||
request
|
||||
expect(JSON.parse(response.body).fetch('tid')).to eq record.tid
|
||||
end
|
||||
end
|
||||
|
||||
describe 'update deposit' do
|
||||
def request
|
||||
put_json '/api/v2/management/deposits/state', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:currency) { Currency.find(:usd) }
|
||||
let(:member) { create(:member, :barong) }
|
||||
let(:amount) { '500.90' }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) { { tid: record.tid } }
|
||||
let(:account) { member.get_account(currency) }
|
||||
let(:record) { Deposits::Fiat.create!(member: member, amount: amount, currency: currency) }
|
||||
|
||||
context 'coin deposit' do
|
||||
let(:record) { create(:deposit_btc) }
|
||||
|
||||
it 'works only with fiat deposits' do
|
||||
data.merge!(state: :accepted)
|
||||
request
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
it 'cancels deposit' do
|
||||
data.merge!(state: :canceled)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(Deposit.find_by_tid!(JSON.parse(response.body).fetch('tid')).aasm_state).to eq 'canceled'
|
||||
account.reload
|
||||
expect(account.balance).to eq 0
|
||||
expect(account.locked).to eq 0
|
||||
end
|
||||
|
||||
it 'accepts deposit' do
|
||||
data.merge!(state: :accepted)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(Deposit.find_by_tid!(JSON.parse(response.body).fetch('tid')).aasm_state).to eq 'accepted'
|
||||
account.reload
|
||||
expect(account.balance).to eq amount.to_d
|
||||
expect(account.locked).to eq 0
|
||||
end
|
||||
|
||||
it 'doesn\'t cancel deposit twice' do
|
||||
record.cancel!
|
||||
expect(record.aasm_state).to eq 'canceled'
|
||||
data.merge!(state: :canceled)
|
||||
expect { request }.not_to(change { record.reload.aasm_state })
|
||||
expect(response).to have_http_status(422)
|
||||
expect(record.account.balance).to eq 0
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
|
||||
it 'doesn\'t accept deposit twice' do
|
||||
record.accept!
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
data.merge!(state: :accepted)
|
||||
expect { request }.not_to(change { record.reload.aasm_state })
|
||||
expect(response).to have_http_status(422)
|
||||
expect(record.account.balance).to eq amount.to_d
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
|
||||
it 'validates state' do
|
||||
data.merge!(state: :rejected)
|
||||
request
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/state does not have a valid value/i)
|
||||
end
|
||||
|
||||
it 'denies access unless enough signatures are supplied' do
|
||||
data.merge!(state: :accepted)
|
||||
signers.clear.concat %i[james]
|
||||
expect { request }.not_to(change { member.get_account(:usd).balance })
|
||||
expect(response.status).to eq 401
|
||||
end
|
||||
end
|
||||
end
|
||||
191
spec/api/v2/management/engines_spec.rb
Normal file
191
spec/api/v2/management/engines_spec.rb
Normal file
@@ -0,0 +1,191 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Engines, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_engines: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
write_engines: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'POST /engines/get' do
|
||||
def request
|
||||
post_json "/api/v2/management/engines/get", multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) { params.merge(scope: :read_engines) }
|
||||
|
||||
let(:params) do
|
||||
engines_params
|
||||
end
|
||||
|
||||
context do
|
||||
let(:engines_params) { {} }
|
||||
|
||||
it 'lists of engines' do
|
||||
request
|
||||
expect(response).to be_successful
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result.size).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:engines_params) do
|
||||
{
|
||||
ordering: 'asc'
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns engines by ascending order' do
|
||||
request
|
||||
result = JSON.parse(response.body)
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(result.first['id']).to eq Engine.first.id
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:engines_params) do
|
||||
{
|
||||
limit: 1,
|
||||
page: 1
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns paginated engines' do
|
||||
request
|
||||
result = JSON.parse(response.body)
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.headers.fetch('Total')).to eq '2'
|
||||
expect(result.size).to eq 1
|
||||
|
||||
params[:page] = 2
|
||||
request
|
||||
result = JSON.parse(response.body)
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response.headers.fetch('Total')).to eq '2'
|
||||
expect(result.size).to eq 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /engines' do
|
||||
def request
|
||||
post_json "/api/v2/management/engines", multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) { params.merge(scope: :write_engines) }
|
||||
|
||||
let(:params) do
|
||||
engines_params
|
||||
end
|
||||
|
||||
context do
|
||||
let(:engines_params) do
|
||||
{
|
||||
name: 'new-engine',
|
||||
driver: 'new_driver',
|
||||
uid: 'UID123456',
|
||||
key: 'your_key',
|
||||
secret: 'your_secret',
|
||||
data: { some_data: 'some data' }
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates new engine' do
|
||||
request
|
||||
result = JSON.parse(response.body)
|
||||
expect(response).to be_successful
|
||||
expect(result['name']).to eq 'new-engine'
|
||||
expect(result['data'].blank?).to eq true
|
||||
|
||||
request
|
||||
expect(response).to have_http_status 422
|
||||
result = JSON.parse(response.body)
|
||||
expect(result['error']).to include('management.engine.duplicate_name')
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:engines_params) { {} }
|
||||
it 'checked required params' do
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
result = JSON.parse(response.body)
|
||||
expect(result['error']).to include('name is missing, driver is missing')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /engines/update' do
|
||||
def request
|
||||
post_json "/api/v2/management/engines/update", multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) { params.merge(scope: :write_engines) }
|
||||
|
||||
let(:params) do
|
||||
engines_params
|
||||
end
|
||||
|
||||
let!(:engine) { create(:engine) }
|
||||
|
||||
context do
|
||||
let(:engines_params) do
|
||||
{
|
||||
id: engine.id,
|
||||
name: 'Second Engine',
|
||||
driver: 'second_driver'
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates attributes' do
|
||||
request
|
||||
result = JSON.parse(response.body)
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(result['name']).to eq 'Second Engine'
|
||||
expect(result['driver']).to eq 'second_driver'
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:engines_params) do
|
||||
{
|
||||
id: engine.id,
|
||||
name: 'Second Engine',
|
||||
secret: 'my_secret'
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates secret' do
|
||||
request
|
||||
expect(response).to be_successful
|
||||
engine.reload
|
||||
expect(engine.secret).to eq('my_secret')
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:engines_params) { {} }
|
||||
it 'checkes required params' do
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
result = JSON.parse(response.body)
|
||||
expect(result['error']).to include('id is missing')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
spec/api/v2/management/entities/balance_spec.rb
Normal file
15
spec/api/v2/management/entities/balance_spec.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Entities::Balance do
|
||||
let(:member) { create(:member, :barong) }
|
||||
let(:record) { member.get_account(:usd) }
|
||||
before { record.update!(balance: 1000.85, locked: 330.55) }
|
||||
subject { OpenStruct.new API::V2::Management::Entities::Balance.represent(record).serializable_hash }
|
||||
|
||||
it do
|
||||
expect(subject.uid).to eq record.member.uid
|
||||
expect(subject.balance).to eq '1000.85'
|
||||
expect(subject.locked).to eq '330.55'
|
||||
end
|
||||
end
|
||||
42
spec/api/v2/management/entities/deposit_spec.rb
Normal file
42
spec/api/v2/management/entities/deposit_spec.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Entities::Deposit do
|
||||
context 'fiat' do
|
||||
let(:record) { create(:deposit_usd, member: create(:member, :barong)) }
|
||||
|
||||
subject { OpenStruct.new API::V2::Management::Entities::Deposit.represent(record).serializable_hash }
|
||||
|
||||
it do
|
||||
expect(subject.tid).to eq record.tid
|
||||
expect(subject.currency).to eq 'usd'
|
||||
expect(subject.uid).to eq record.member.uid
|
||||
expect(subject.type).to eq 'fiat'
|
||||
expect(subject.amount).to eq record.amount.to_s
|
||||
expect(subject.state).to eq record.aasm_state
|
||||
expect(subject.created_at).to eq record.created_at.iso8601
|
||||
expect(subject.completed_at).to eq record.completed_at&.iso8601
|
||||
expect(subject.respond_to?(:blockchain_txid)).to be_falsey
|
||||
expect(subject.respond_to?(:confirmations)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'coin' do
|
||||
let(:record) { create(:deposit_btc, member: create(:member, :barong)) }
|
||||
|
||||
subject { OpenStruct.new API::V2::Management::Entities::Deposit.represent(record).serializable_hash }
|
||||
|
||||
it do
|
||||
expect(subject.tid).to eq record.tid
|
||||
expect(subject.currency).to eq 'btc'
|
||||
expect(subject.uid).to eq record.member.uid
|
||||
expect(subject.type).to eq 'coin'
|
||||
expect(subject.amount).to eq record.amount.to_s
|
||||
expect(subject.state).to eq record.aasm_state
|
||||
expect(subject.created_at).to eq record.created_at.iso8601
|
||||
expect(subject.completed_at).to eq record.completed_at&.iso8601
|
||||
expect(subject.blockchain_txid).to eq record.txid
|
||||
expect(subject.blockchain_confirmations).to eq record.confirmations
|
||||
end
|
||||
end
|
||||
end
|
||||
64
spec/api/v2/management/entities/operation_spec.rb
Normal file
64
spec/api/v2/management/entities/operation_spec.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Entities::Operation do
|
||||
Operations::Account::PLATFORM_TYPES.each do |op_type|
|
||||
context op_type do
|
||||
let(:record) { create(op_type) }
|
||||
|
||||
subject { OpenStruct.new API::V2::Management::Entities::Operation.represent(record).serializable_hash }
|
||||
|
||||
it do
|
||||
expect(subject.code).to eq record.code
|
||||
expect(subject.currency).to eq record.currency_id
|
||||
expect(subject.created_at).to eq record.created_at.iso8601
|
||||
end
|
||||
|
||||
context 'credit' do
|
||||
it do
|
||||
expect(subject.credit).to eq record.credit
|
||||
expect(subject.respond_to?(:debit)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'debit' do
|
||||
let(:record) { create(:asset, :debit) }
|
||||
it do
|
||||
expect(subject.debit).to eq record.debit
|
||||
expect(subject.respond_to?(:credit)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Operations::Account::MEMBER_TYPES.each do |op_type|
|
||||
context op_type do
|
||||
let(:record) { create(op_type, :with_member) }
|
||||
|
||||
subject { OpenStruct.new API::V2::Management::Entities::Operation.represent(record).serializable_hash }
|
||||
|
||||
it do
|
||||
expect(subject.code).to eq record.code
|
||||
expect(subject.currency).to eq record.currency_id
|
||||
expect(subject.uid).to eq record.member.uid
|
||||
expect(subject.created_at).to eq record.created_at.iso8601
|
||||
end
|
||||
|
||||
context 'credit' do
|
||||
it do
|
||||
expect(subject.credit).to eq record.credit
|
||||
expect(subject.respond_to?(:debit)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'debit' do
|
||||
let(:record) { create(:asset, :debit) }
|
||||
|
||||
it do
|
||||
expect(subject.debit).to eq record.debit
|
||||
expect(subject.respond_to?(:credit)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
spec/api/v2/management/entities/trade_spec.rb
Normal file
43
spec/api/v2/management/entities/trade_spec.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Entities::Trade do
|
||||
let(:trade) do
|
||||
create :trade, :btcusd, maker_order: create(:order_ask, :btcusd), taker_order: create(:order_bid, :btcusd)
|
||||
end
|
||||
|
||||
subject { OpenStruct.new API::V2::Management::Entities::Trade.represent(trade, side: 'sell').serializable_hash }
|
||||
|
||||
it do
|
||||
expect(subject.id).to eq trade.id
|
||||
expect(subject.order_id).to be_nil
|
||||
expect(subject.price).to eq trade.price
|
||||
expect(subject.amount).to eq trade.amount
|
||||
expect(subject.total).to eq trade.total
|
||||
expect(subject.market).to eq trade.market_id
|
||||
expect(subject.side).to eq 'sell'
|
||||
expect(subject.created_at).to eq trade.created_at.iso8601
|
||||
expect(subject.maker_order_id).to eq trade.maker_order_id
|
||||
expect(subject.maker_order_id).to eq trade.maker_order_id
|
||||
expect(subject.maker_member_uid).to eq trade.maker.uid
|
||||
expect(subject.taker_member_uid).to eq trade.taker.uid
|
||||
end
|
||||
|
||||
|
||||
context 'sell order maker' do
|
||||
it { expect(subject.taker_type).to eq 'buy' }
|
||||
end
|
||||
|
||||
context 'buy order maker' do
|
||||
let(:trade) do
|
||||
create :trade, :btcusd, maker_order: create(:order_bid, :btcusd), taker_order: create(:order_ask, :btcusd)
|
||||
end
|
||||
|
||||
it { expect(subject.taker_type).to eq 'sell' }
|
||||
end
|
||||
|
||||
context 'empty side' do
|
||||
subject { OpenStruct.new API::V2::Management::Entities::Trade.represent(trade).serializable_hash }
|
||||
it { expect(subject.respond_to?(:side)).to be_falsey }
|
||||
end
|
||||
end
|
||||
69
spec/api/v2/management/entities/transfer_spec.rb
Normal file
69
spec/api/v2/management/entities/transfer_spec.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Entities::Transfer do
|
||||
let(:record) { create(:transfer) }
|
||||
subject { OpenStruct.new API::V2::Management::Entities::Transfer.represent(record.reload).serializable_hash }
|
||||
|
||||
it do
|
||||
expect(subject.key).to eq record.key
|
||||
expect(subject.category).to eq record.category
|
||||
expect(subject.description).to eq record.description
|
||||
end
|
||||
|
||||
context 'with operations' do
|
||||
let(:record) { create(:transfer_with_operations) }
|
||||
it do
|
||||
::Operations::Account::TYPES.map(&:pluralize).each do |op_t|
|
||||
expect(subject.respond_to?(op_t)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
it do
|
||||
record_assets = API::V2::Management::Entities::Operation
|
||||
.represent(record.reload.assets)
|
||||
expect(subject.assets.to_json).to eq record_assets.to_json
|
||||
end
|
||||
|
||||
it do
|
||||
record_expenses = API::V2::Management::Entities::Operation
|
||||
.represent(record.reload.expenses)
|
||||
expect(subject.expenses.to_json).to eq record_expenses.to_json
|
||||
end
|
||||
|
||||
it do
|
||||
record_liabilities = API::V2::Management::Entities::Operation
|
||||
.represent(record.reload.liabilities)
|
||||
expect(subject.liabilities.to_json).to eq record_liabilities.to_json
|
||||
end
|
||||
|
||||
it do
|
||||
record_revenues = API::V2::Management::Entities::Operation
|
||||
.represent(record.reload.revenues)
|
||||
expect(subject.revenues.to_json).to eq record_revenues.to_json
|
||||
end
|
||||
end
|
||||
|
||||
context 'with single operation type' do
|
||||
let(:record) { create(:transfer, :with_liabilities) }
|
||||
|
||||
it do
|
||||
expect(subject.respond_to?(:liabilities)).to be_truthy
|
||||
end
|
||||
|
||||
it do
|
||||
# TYPES - 'liabilities' = PLATFORM_TYPES
|
||||
::Operations::Account::PLATFORM_TYPES.map(&:pluralize).each do |op_t|
|
||||
expect(subject.respond_to?(op_t.to_sym)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without operations' do
|
||||
it do
|
||||
::Operations::Account::TYPES.map(&:pluralize).each do |op_t|
|
||||
expect(subject.respond_to?(op_t)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
48
spec/api/v2/management/entities/withdraw_spec.rb
Normal file
48
spec/api/v2/management/entities/withdraw_spec.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Entities::Withdraw do
|
||||
context 'fiat' do
|
||||
let(:rid) { Faker::Bank.iban }
|
||||
let(:member) { create(:member, :barong) }
|
||||
let(:record) { create(:usd_withdraw, :with_deposit_liability, member: member, rid: rid) }
|
||||
|
||||
subject { OpenStruct.new API::V2::Management::Entities::Withdraw.represent(record).serializable_hash }
|
||||
|
||||
it do
|
||||
expect(subject.tid).to eq record.tid
|
||||
expect(subject.rid).to eq rid
|
||||
expect(subject.currency).to eq 'usd'
|
||||
expect(subject.uid).to eq record.member.uid
|
||||
expect(subject.type).to eq 'fiat'
|
||||
expect(subject.amount).to eq record.amount.to_s
|
||||
expect(subject.note).to eq record.note
|
||||
expect(subject.fee).to eq record.fee.to_s
|
||||
expect(subject.respond_to?(:txid)).to be_falsey
|
||||
expect(subject.state).to eq record.aasm_state
|
||||
expect(subject.created_at).to eq record.created_at.iso8601
|
||||
end
|
||||
end
|
||||
|
||||
context 'coin' do
|
||||
let(:rid) { Faker::Blockchain::Bitcoin.address }
|
||||
let(:member) { create(:member, :barong) }
|
||||
let(:record) { create(:btc_withdraw, :with_deposit_liability, member: member, rid: rid) }
|
||||
|
||||
subject { OpenStruct.new API::V2::Management::Entities::Withdraw.represent(record).serializable_hash }
|
||||
|
||||
it do
|
||||
expect(subject.tid).to eq record.tid
|
||||
expect(subject.rid).to eq rid
|
||||
expect(subject.currency).to eq 'btc'
|
||||
expect(subject.uid).to eq record.member.uid
|
||||
expect(subject.type).to eq 'coin'
|
||||
expect(subject.amount).to eq record.amount.to_s
|
||||
expect(subject.note).to eq record.note
|
||||
expect(subject.fee).to eq record.fee.to_s
|
||||
expect(subject.blockchain_txid).to eq record.txid
|
||||
expect(subject.state).to eq record.aasm_state
|
||||
expect(subject.created_at).to eq record.created_at.iso8601
|
||||
end
|
||||
end
|
||||
end
|
||||
8
spec/api/v2/management/error_spec.rb
Normal file
8
spec/api/v2/management/error_spec.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
describe API::V2::Management::Exceptions::Base do
|
||||
it do
|
||||
expect(API::V2::Management::Exceptions::Base.new(message: 'Wrong argument.').inspect).to eq \
|
||||
'#<API::V2::Management::Exceptions::Base: Wrong argument.>'
|
||||
expect(API::V2::Management::Exceptions::Base.new(message: 'Wrong argument.', debug_message: 'Debug message.').inspect).to eq \
|
||||
'#<API::V2::Management::Exceptions::Base: Wrong argument. (Debug message.)>'
|
||||
end
|
||||
end
|
||||
120
spec/api/v2/management/jwt_authentication_middleware_spec.rb
Normal file
120
spec/api/v2/management/jwt_authentication_middleware_spec.rb
Normal file
@@ -0,0 +1,120 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::JWTAuthenticationMiddleware, type: :request do
|
||||
let(:member) { create(:member, :level_3) }
|
||||
let(:config) { management_api_v1_security_configuration }
|
||||
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
config.merge! \
|
||||
scopes: {
|
||||
tools: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }
|
||||
}
|
||||
end
|
||||
|
||||
it 'works in standard conditions' do
|
||||
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({}, :alex)
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it 'allows only POST, PUT, and DELETE' do
|
||||
get '/api/v2/management/timestamp'
|
||||
expect(response).to have_http_status(405)
|
||||
|
||||
patch '/api/v2/management/timestamp'
|
||||
expect(response).to have_http_status(405)
|
||||
|
||||
head '/api/v2/management/timestamp'
|
||||
expect(response).to have_http_status(405)
|
||||
end
|
||||
|
||||
it 'doesn\'t allow query parameters' do
|
||||
post '/api/v2/management/timestamp?foo=baz&baz=qux'
|
||||
expect(response).to have_http_status(400)
|
||||
expect(response.body).to match(/query parameters/i)
|
||||
end
|
||||
|
||||
it 'requires JSON in the request body' do
|
||||
post '/api/v2/management/timestamp', params: { foo: 'baz', baz: 'qux' }
|
||||
expect(response).to have_http_status(400)
|
||||
expect(response.body).to match(/only json/i)
|
||||
end
|
||||
|
||||
it 'denies access when not enough signatures are supplied' do
|
||||
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({})
|
||||
expect(response).to have_http_status(401)
|
||||
expect(response.body).to match(/not enough signatures/i)
|
||||
end
|
||||
|
||||
it 'denies access when token is expired' do
|
||||
config[:jwt][:verify_expiration] = true
|
||||
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ exp: 1.minute.ago.to_i }, :alex)
|
||||
expect(response).to have_http_status(401)
|
||||
expect(response.body).to match(/failed to verify jwt/i)
|
||||
end
|
||||
|
||||
context 'valid issuer' do
|
||||
before { config[:jwt][:verify_iss] = true }
|
||||
before { config[:jwt].merge!(iss: 'qux') }
|
||||
it 'validates issuer' do
|
||||
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ iss: 'qux' }, :alex)
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid issuer' do
|
||||
before { config[:jwt][:verify_iss] = true }
|
||||
before { config[:jwt].merge!(iss: 'qux') }
|
||||
it 'validates issuer' do
|
||||
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ iss: 'hacker' }, :alex)
|
||||
expect(response).to have_http_status(401)
|
||||
expect(response.body).to match(/failed to verify jwt/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'valid audience' do
|
||||
before { config[:jwt][:verify_aud] = true }
|
||||
before { config[:jwt].merge!(aud: 'qux') }
|
||||
it 'validates audience' do
|
||||
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ aud: 'qux' }, :alex)
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid audience' do
|
||||
before { config[:jwt][:verify_aud] = true }
|
||||
before { config[:jwt].merge!(aud: 'qux') }
|
||||
it 'validates audience' do
|
||||
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ aud: 'hacker' }, :alex)
|
||||
expect(response).to have_http_status(401)
|
||||
expect(response.body).to match(/failed to verify jwt/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'missing JWT ID' do
|
||||
before { config[:jwt][:verify_jti] = true }
|
||||
it 'requires JTI' do
|
||||
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({}, :alex)
|
||||
expect(response).to have_http_status(401)
|
||||
expect(response.body).to match(/failed to verify jwt/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'issued at in future' do
|
||||
before { config[:jwt][:verify_iat] = true }
|
||||
it 'denies access' do
|
||||
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ iat: 200.seconds.from_now.to_i }, :alex)
|
||||
expect(response).to have_http_status(401)
|
||||
expect(response.body).to match(/failed to verify jwt/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'issued at before future' do
|
||||
before { config[:jwt][:verify_iat] = true }
|
||||
it 'allows access' do
|
||||
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ iat: 3.seconds.ago.to_i }, :alex)
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
124
spec/api/v2/management/markets_spec.rb
Normal file
124
spec/api/v2/management/markets_spec.rb
Normal file
@@ -0,0 +1,124 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Markets, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
write_markets: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
read_markets: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
}
|
||||
end
|
||||
|
||||
describe 'update market' do
|
||||
def request
|
||||
put_json '/api/v2/management/markets/update', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { {} }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:market) { Market.find(:btcusd) }
|
||||
|
||||
it 'should validate min_price param' do
|
||||
data.merge!(id: market.id, min_price: -10.0)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/min_price does not have a valid value/i)
|
||||
end
|
||||
|
||||
it 'should validate min_amount param' do
|
||||
data.merge!(id: market.id, min_amount: -123.0)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/min_amount does not have a valid value/i)
|
||||
end
|
||||
|
||||
it 'should validate amount_precision param' do
|
||||
data.merge!(id: market.id, amount_precision: -100.0)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/amount_precision does not have a valid value/i)
|
||||
end
|
||||
|
||||
it 'should validate price_precision param' do
|
||||
data.merge!(id: market.id, price_precision: -100.0)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/price_precision does not have a valid value/i)
|
||||
end
|
||||
|
||||
it 'should validate max_price param' do
|
||||
data.merge!(id: market.id, max_price: -1)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/max_price does not have a valid value/i)
|
||||
end
|
||||
|
||||
it 'should validate position param' do
|
||||
data.merge!(id: market.id, position: -100.0)
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/position does not have a valid value/i)
|
||||
end
|
||||
|
||||
it 'should validate state param' do
|
||||
data.merge!(id: market.id, state: 'blah-blah')
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/state does not have a valid value/i)
|
||||
end
|
||||
|
||||
it 'should check required params' do
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/id is missing/i)
|
||||
end
|
||||
|
||||
it 'should update market' do
|
||||
data.merge!(id: market.id, state: 'disabled', min_amount: '0.1')
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 200
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result.fetch('id')).to eq market.id
|
||||
expect(result.fetch('state')).to eq 'disabled'
|
||||
expect(result.fetch('min_amount')).to eq '0.1'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'fetch markets list' do
|
||||
def request
|
||||
post_json '/api/v2/management/markets/list', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
|
||||
let(:data) { {} }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
|
||||
let(:expected_keys) do
|
||||
%w[id name base_unit quote_unit min_price max_price
|
||||
min_amount amount_precision price_precision state position created_at updated_at]
|
||||
end
|
||||
|
||||
it 'lists enabled markets' do
|
||||
request
|
||||
expect(response).to have_http_status 200
|
||||
result = JSON.parse(response.body)
|
||||
|
||||
expect(result.size).to eq Market.count
|
||||
result.each do |market|
|
||||
expect(market.keys).to eq expected_keys
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
93
spec/api/v2/management/members_spec.rb
Normal file
93
spec/api/v2/management/members_spec.rb
Normal file
@@ -0,0 +1,93 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Members, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
write_members: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex jeff] },
|
||||
}
|
||||
end
|
||||
|
||||
describe 'create member' do
|
||||
def request
|
||||
post_json '/api/v2/management/members', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { build(:member).slice(:uid, :email, :level, :role, :group, :state) }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
|
||||
it 'returns member' do
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body)).to include( data )
|
||||
end
|
||||
|
||||
context 'invalid params' do
|
||||
context 'email' do
|
||||
it 'returns status 422 and error' do
|
||||
data[:email] = 'fake_email'
|
||||
|
||||
request
|
||||
expect(response).to have_http_status(422)
|
||||
expect(JSON.parse(response.body)['errors']).to eq("Validation failed: Email is invalid")
|
||||
end
|
||||
end
|
||||
|
||||
context 'level' do
|
||||
it 'returns status 422 and error' do
|
||||
data[:level] = 'fake_level'
|
||||
|
||||
request
|
||||
expect(response).to have_http_status(422)
|
||||
expect(JSON.parse(response.body)['error']).to eq("level is invalid")
|
||||
end
|
||||
end
|
||||
|
||||
context 'role' do
|
||||
it 'returns status 422 and error' do
|
||||
data[:role] = 'fake_role'
|
||||
|
||||
request
|
||||
expect(response).to have_http_status(422)
|
||||
expect(JSON.parse(response.body)['errors']).to eq("Validation failed: Role is not included in the list")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'set user group' do
|
||||
def request
|
||||
post_json '/api/v2/management/members/group', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { {uid: member.uid, group: 'vip-1'} }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:member) { create(:member, :barong) }
|
||||
|
||||
it 'returns user with updated role' do
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body)['group']).to eq('vip-1')
|
||||
end
|
||||
|
||||
context 'invalid uid' do
|
||||
let(:data) { { uid: 'fake_uid', group: 'vip-1' } }
|
||||
it 'returns status 404 and error' do
|
||||
request
|
||||
expect(response).to have_http_status(404)
|
||||
expect(JSON.parse(response.body)['error']).to eq("Couldn't find record.")
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid record' do
|
||||
let(:data) { { uid: member.uid, group: 'vip-12222222222222222222222222222' } }
|
||||
it 'returns status 422 and error' do
|
||||
request
|
||||
expect(response).to have_http_status(422)
|
||||
expect(JSON.parse(response.body)['errors']).to eq("Validation failed: Group is too long (maximum is 32 characters)")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
361
spec/api/v2/management/operations_spec.rb
Normal file
361
spec/api/v2/management/operations_spec.rb
Normal file
@@ -0,0 +1,361 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Operations, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_operations: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
write_operations: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'list operations' do
|
||||
def request(op_type)
|
||||
post_json "/api/v2/management/#{op_type}", multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
def optional_request(op_type, data)
|
||||
post_json "/api/v2/management/#{op_type}", multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
Operations::Account::PLATFORM_TYPES.each do |op_type|
|
||||
context op_type do
|
||||
let(:data) { {} }
|
||||
let(:signers) { %i[alex] }
|
||||
let(:operations_number) { 15 }
|
||||
let!(:operations) { create_list(op_type, operations_number) }
|
||||
|
||||
before do
|
||||
request(op_type.to_s.pluralize)
|
||||
end
|
||||
|
||||
it { expect(response).to have_http_status(200) }
|
||||
|
||||
context 'filter by currency' do
|
||||
let(:data) { { currency: :btc } }
|
||||
it { expect(response).to have_http_status(200) }
|
||||
|
||||
it 'returns operations by currency' do
|
||||
operations = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
.where(currency_id: :btc)
|
||||
expect(JSON.parse(response.body).count).to eq operations.count
|
||||
expect(JSON.parse(response.body).map { |h| h['currency'] }).to\
|
||||
eq operations.pluck(:currency_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'pagination' do
|
||||
let(:data) { { page: 2, limit: 8 } }
|
||||
|
||||
it 'returns second page of operations' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).count).to eq 7
|
||||
credits = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
.order(id: :desc)
|
||||
.pluck(:credit)
|
||||
|
||||
# Consider that credit sequence is unique.
|
||||
expect(JSON.parse(response.body).map { |h| h['credit'].to_d }).to eq credits[8..15]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Operations::Account::MEMBER_TYPES.each do |op_type|
|
||||
context op_type do
|
||||
let(:data) { {} }
|
||||
let(:signers) { %i[alex] }
|
||||
let(:operations_number) { 15 }
|
||||
let!(:operations) { create_list(op_type, operations_number, :with_member) }
|
||||
|
||||
before do
|
||||
request(op_type.to_s.pluralize)
|
||||
end
|
||||
|
||||
it { expect(response).to have_http_status(200) }
|
||||
|
||||
context 'filter by currency' do
|
||||
let(:data) { { currency: :btc } }
|
||||
|
||||
it 'returns operations by currency' do
|
||||
expect(response).to have_http_status(200)
|
||||
operations = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
.where(currency_id: :btc)
|
||||
expect(JSON.parse(response.body).count).to eq operations.count
|
||||
expect(JSON.parse(response.body).map { |h| h['currency'] }).to\
|
||||
eq operations.pluck(:currency_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by uid' do
|
||||
let(:member) { create(:member, :barong) }
|
||||
let!(:member_operations) do
|
||||
create_list(op_type, operations_number, member_id: member.id)
|
||||
end
|
||||
let(:data) { { uid: member.uid } }
|
||||
|
||||
it 'returns operations by member UID' do
|
||||
expect(response).to have_http_status(200)
|
||||
request(op_type.to_s.pluralize)
|
||||
operations = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
.where(member: member)
|
||||
expect(JSON.parse(response.body).count).to eq operations.count
|
||||
expect(JSON.parse(response.body).map { |h| h['uid'] }).to\
|
||||
eq [member.uid] * operations_number
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by reference type' do
|
||||
let(:deposit_data) { { reference_type: 'deposit' } }
|
||||
let(:trade_data) { { reference_type: 'trade' } }
|
||||
let(:order_data) { { reference_type: 'order' } }
|
||||
|
||||
it { expect(response).to have_http_status(200) }
|
||||
|
||||
def equal_amount!(response, optional_field, op_type)
|
||||
operations = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
.where(optional_field)
|
||||
expect(JSON.parse(response.body).count).to eq operations.count
|
||||
end
|
||||
|
||||
it 'returns operations by reference type deposit' do
|
||||
optional_request(op_type.to_s.pluralize, deposit_data)
|
||||
equal_amount!(response, deposit_data, op_type)
|
||||
end
|
||||
|
||||
it 'returns operations by reference type trade' do
|
||||
optional_request(op_type.to_s.pluralize, trade_data)
|
||||
equal_amount!(response, trade_data, op_type)
|
||||
end
|
||||
|
||||
it 'returns operations by reference type order' do
|
||||
optional_request(op_type.to_s.pluralize, order_data)
|
||||
equal_amount!(response, order_data, op_type)
|
||||
end
|
||||
end
|
||||
|
||||
context 'time range' do
|
||||
let(:time_from) { 2.days.ago }
|
||||
let(:time_to) { 1.day.ago }
|
||||
|
||||
let(:data) { { time_from: time_from.to_i, time_to: time_to.to_i } }
|
||||
|
||||
it { expect(response).to have_http_status(200) }
|
||||
|
||||
it 'returns operations between 48h and 24h ago' do
|
||||
request(op_type.to_s.pluralize)
|
||||
operations = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
.where('created_at >= ?', time_from)
|
||||
.where('created_at < ?', time_to)
|
||||
expect(JSON.parse(response.body).count).to eq operations.count
|
||||
end
|
||||
end
|
||||
|
||||
context 'pagination' do
|
||||
let(:data) { { page: 2, limit: 8 } }
|
||||
|
||||
it { expect(response).to have_http_status(200) }
|
||||
|
||||
it 'returns second page of operations' do
|
||||
expect(JSON.parse(response.body).count).to eq 7
|
||||
credits = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
.order(id: :desc)
|
||||
.pluck(:credit)
|
||||
|
||||
# Consider that credit sequence is unique.
|
||||
expect(JSON.parse(response.body).map{ |h| h['credit'].to_d }).to eq credits[8..15]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'create operation' do
|
||||
def request(op_type)
|
||||
post_json "/api/v2/management/#{op_type}/new", multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
Operations::Account::PLATFORM_TYPES.each do |op_type|
|
||||
context op_type do
|
||||
let(:currency) { Currency.coins.sample }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) do
|
||||
{ currency: currency.code,
|
||||
code: Operations::Account.find_by(type: op_type, currency_type: currency.type).code}
|
||||
end
|
||||
|
||||
context 'credit' do
|
||||
let(:amount) { '0.2515' }
|
||||
before do
|
||||
data[:credit] = amount
|
||||
request(op_type.to_s.pluralize)
|
||||
end
|
||||
|
||||
it 'returns operation' do
|
||||
expect(response).to have_http_status 200
|
||||
expect(JSON.parse(response.body)['currency']).to eq currency.code.to_s
|
||||
expect(JSON.parse(response.body)['credit'].to_d).to eq amount.to_d
|
||||
expect(JSON.parse(response.body)['code']).to \
|
||||
eq Operations::Account.find_by(type: op_type,
|
||||
kind: :main,
|
||||
currency_type: currency.type).code
|
||||
end
|
||||
|
||||
it 'saves operation' do
|
||||
op_klass = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
expect { request(op_type.to_s.pluralize) }.to \
|
||||
change(op_klass, :count).by(1)
|
||||
end
|
||||
|
||||
context 'wrong account code' do
|
||||
before do
|
||||
data[:code] = (::Operations::Account.pluck(:code) - [data[:code]]).sample
|
||||
request(op_type.to_s.pluralize)
|
||||
end
|
||||
|
||||
it { expect(response).to have_http_status 422 }
|
||||
end
|
||||
end
|
||||
|
||||
context 'debit' do
|
||||
let(:amount) { '0.1545' }
|
||||
before do
|
||||
# Create credit operation to avoid negative balance.
|
||||
create(op_type, credit: amount)
|
||||
data[:debit] = amount
|
||||
request(op_type.to_s.pluralize)
|
||||
end
|
||||
|
||||
it 'returns operation' do
|
||||
expect(response).to have_http_status 200
|
||||
expect(JSON.parse(response.body)['currency']).to eq currency.code.to_s
|
||||
expect(JSON.parse(response.body)['debit'].to_d).to eq amount.to_d
|
||||
expect(JSON.parse(response.body)['code']).to \
|
||||
eq Operations::Account.find_by(type: op_type,
|
||||
kind: :main,
|
||||
currency_type: currency.type).code
|
||||
end
|
||||
|
||||
it 'saves operation' do
|
||||
op_klass = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
expect { request(op_type.to_s.pluralize) }.to \
|
||||
change(op_klass, :count).by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Operations::Account::MEMBER_TYPES.each do |op_type|
|
||||
context op_type do
|
||||
let(:currency) { Currency.coins.sample }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:member) { create(:member, :barong) }
|
||||
let(:data) do
|
||||
{ currency: currency.code,
|
||||
code: Operations::Account.find_by(type: op_type, currency_type: currency.type, kind: :main).code,
|
||||
uid: member.uid }
|
||||
end
|
||||
|
||||
context 'credit' do
|
||||
let(:amount) { '0.2515' }
|
||||
before do
|
||||
data[:credit] = amount
|
||||
request(op_type.to_s.pluralize)
|
||||
end
|
||||
|
||||
it 'returns operation' do
|
||||
expect(response).to have_http_status 200
|
||||
expect(JSON.parse(response.body)['uid']).to eq member.uid
|
||||
expect(JSON.parse(response.body)['currency']).to eq currency.code.to_s
|
||||
expect(JSON.parse(response.body)['credit'].to_d).to eq amount.to_d
|
||||
expect(JSON.parse(response.body)['code']).to \
|
||||
eq Operations::Account.find_by(type: op_type,
|
||||
kind: :main,
|
||||
currency_type: currency.type).code
|
||||
end
|
||||
|
||||
it 'saves operation' do
|
||||
op_klass = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
expect { request(op_type.to_s.pluralize) }.to \
|
||||
change(op_klass, :count).by(1)
|
||||
end
|
||||
|
||||
it 'updates legacy balance' do
|
||||
currency_id = JSON.parse(response.body)['currency']
|
||||
expect(member.get_account(currency_id).balance).to \
|
||||
eq JSON.parse(response.body)['credit'].to_d
|
||||
end
|
||||
|
||||
context 'wrong account code' do
|
||||
before do
|
||||
data[:code] = 999
|
||||
request(op_type.to_s.pluralize)
|
||||
end
|
||||
|
||||
it { expect(response).to have_http_status 422 }
|
||||
end
|
||||
end
|
||||
|
||||
context 'debit' do
|
||||
let(:amount) { '0.1545' }
|
||||
before do
|
||||
# Create credit operation to avoid negative balance.
|
||||
create(op_type, :with_member, credit: amount,
|
||||
member: member, currency: currency)
|
||||
data[:debit] = amount
|
||||
request(op_type.to_s.pluralize)
|
||||
end
|
||||
|
||||
it 'returns operation' do
|
||||
expect(response).to have_http_status 200
|
||||
expect(JSON.parse(response.body)['uid']).to eq member.uid
|
||||
expect(JSON.parse(response.body)['currency']).to eq currency.code.to_s
|
||||
expect(JSON.parse(response.body)['debit'].to_d).to eq amount.to_d
|
||||
expect(JSON.parse(response.body)['code']).to \
|
||||
eq Operations::Account.find_by(type: op_type,
|
||||
kind: :main,
|
||||
currency_type: currency.type).code
|
||||
end
|
||||
|
||||
it 'saves operation' do
|
||||
# Create one more credit operation to avoid negative balance.
|
||||
# So we can create one more debit operation.
|
||||
create(op_type, :with_member, credit: amount, member: member, currency: currency)
|
||||
op_klass = "operations/#{op_type}"
|
||||
.camelize
|
||||
.constantize
|
||||
expect { request(op_type.to_s.pluralize) }.to \
|
||||
change(op_klass, :count).by(1)
|
||||
end
|
||||
|
||||
it 'updates legacy balance' do
|
||||
currency_id = JSON.parse(response.body)['currency']
|
||||
expect(member.get_account(currency_id).balance).to \
|
||||
eq 0.to_d
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
237
spec/api/v2/management/orders_spec.rb
Normal file
237
spec/api/v2/management/orders_spec.rb
Normal file
@@ -0,0 +1,237 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Orders, type: :request do
|
||||
let(:member1) { create(:member, :level_3) }
|
||||
let(:member2) { create(:member, :level_3) }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let!(:finex_engine) { create(:engine, driver: 'finex-spot') }
|
||||
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_orders: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
write_orders: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }
|
||||
}
|
||||
|
||||
Market.find('btceth').update!(engine: finex_engine)
|
||||
end
|
||||
|
||||
describe 'POST /api/v2/management/orders' do
|
||||
before do
|
||||
create(:order_bid, :btcusd, member: member1, state: Order::CANCEL)
|
||||
create(:order_ask, :btcusd, member: member1, state: Order::WAIT)
|
||||
create(:order_ask, :btceth, member: member1, state: Order::DONE)
|
||||
create(:order_bid, :btcusd, member: member2, state: Order::CANCEL)
|
||||
create(:order_ask, :btcusd, member: member2, state: Order::WAIT)
|
||||
create(:order_ask, :btceth, member: member2, state: Order::DONE)
|
||||
end
|
||||
|
||||
def request
|
||||
post_json '/api/v2/management/orders', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { {} }
|
||||
|
||||
it 'returns all orders on the platform' do
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 200
|
||||
expect(response_body.count).to eq(Order.count)
|
||||
end
|
||||
|
||||
context 'by member' do
|
||||
let(:data) do
|
||||
{
|
||||
uid: member1.uid
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns only member orders' do
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 200
|
||||
expect(response_body.pluck('member_id').uniq).to eq([member1.id])
|
||||
end
|
||||
end
|
||||
|
||||
context 'by member, market, state and order type' do
|
||||
let(:data) do
|
||||
{
|
||||
uid: member1.uid,
|
||||
market: 'btcusd',
|
||||
state: 'wait',
|
||||
ord_type: 'limit'
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns only member orders on specific market with specific state and order type' do
|
||||
request
|
||||
|
||||
expect(response).to have_http_status 200
|
||||
expect(response_body.pluck('member_id').uniq).to eq([member1.id])
|
||||
expect(response_body.pluck('state').uniq).to eq(['wait'])
|
||||
expect(response_body.pluck('market').uniq).to eq(['btcusd'])
|
||||
expect(response_body.pluck('ord_type').uniq).to eq(['limit'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid params' do
|
||||
context 'member_uid' do
|
||||
it 'returns status 422 and error' do
|
||||
data[:uid] = 'invalid_uid'
|
||||
request
|
||||
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
end
|
||||
|
||||
context 'market' do
|
||||
it 'returns status 422 and error' do
|
||||
data[:market] = 'invalid_market'
|
||||
request
|
||||
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
end
|
||||
|
||||
context 'state' do
|
||||
it 'returns status 422 and error' do
|
||||
data[:state] = 'invalid_state'
|
||||
request
|
||||
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
end
|
||||
|
||||
context 'ord_type' do
|
||||
it 'returns status 422 and error' do
|
||||
data[:ord_type] = 'invalid_ord_type'
|
||||
request
|
||||
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v2/management/orders/:id/cancel' do
|
||||
let!(:peatio_order) { create(:order_ask, :btcusd, member: member1, state: Order::WAIT) }
|
||||
let!(:third_party_order) { create(:order_ask, :btceth, member: member1, state: Order::WAIT) }
|
||||
|
||||
def request(order_id)
|
||||
post_json "/api/v2/management/orders/#{order_id}/cancel", multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { {} }
|
||||
|
||||
it 'cancels an order on peatio market' do
|
||||
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: peatio_order.to_matching_attributes)
|
||||
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: peatio_order.as_json_for_third_party, type: 3).never
|
||||
request(peatio_order.id)
|
||||
expect(response).to have_http_status 200
|
||||
end
|
||||
|
||||
context 'third party order cancel' do
|
||||
it 'cancel an order on third party market' do
|
||||
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: third_party_order.to_matching_attributes).never
|
||||
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: third_party_order.as_json_for_third_party, type: 3)
|
||||
|
||||
request(third_party_order.id)
|
||||
expect(response).to have_http_status 200
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid params' do
|
||||
it 'returns status 404 and error' do
|
||||
request(0)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v2/management/orders/cancel' do
|
||||
let!(:member1_peatio_order) { create(:order_ask, :btcusd, member: member1, state: Order::WAIT) }
|
||||
let!(:member2_peatio_order) { create(:order_ask, :btcusd, member: member2, state: Order::WAIT) }
|
||||
let!(:member1_third_party_order) { create(:order_ask, :btceth, member: member1, state: Order::WAIT) }
|
||||
let!(:member2_third_party_order) { create(:order_ask, :btceth, member: member2, state: Order::WAIT) }
|
||||
|
||||
def request
|
||||
post_json '/api/v2/management/orders/cancel', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { {} }
|
||||
|
||||
context 'peatio order cancel' do
|
||||
|
||||
it 'cancels the orders on peatio market' do
|
||||
data[:market] = 'btcusd'
|
||||
|
||||
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member1_peatio_order.to_matching_attributes)
|
||||
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member2_peatio_order.to_matching_attributes)
|
||||
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: { market_id: 'btcusd' }, type: 4).never
|
||||
|
||||
request
|
||||
expect(response).to have_http_status 204
|
||||
end
|
||||
|
||||
it 'cancels the orders on peatio market' do
|
||||
data[:market] = 'btcusd'
|
||||
data[:uid] = member1.uid
|
||||
|
||||
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member1_peatio_order.to_matching_attributes)
|
||||
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: { market_id: 'btcusd' }, type: 4).never
|
||||
|
||||
request
|
||||
expect(response).to have_http_status 204
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'third party orders cancel' do
|
||||
it 'cancels the orders on third party market' do
|
||||
data[:market] = 'btceth'
|
||||
|
||||
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member1_peatio_order.to_matching_attributes).never
|
||||
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member2_peatio_order.to_matching_attributes).never
|
||||
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: { market_id: 'btceth' }, type: 4)
|
||||
|
||||
request
|
||||
expect(response).to have_http_status 204
|
||||
end
|
||||
|
||||
it 'cancels the orders on third party market' do
|
||||
data[:market] = 'btceth'
|
||||
data[:uid] = member1.uid
|
||||
|
||||
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member1_peatio_order.to_matching_attributes).never
|
||||
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: { market_id: 'btceth', member_uid: member1.uid }, type: 4)
|
||||
|
||||
request
|
||||
expect(response).to have_http_status 204
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid params' do
|
||||
context 'invalid market' do
|
||||
it 'returns status 422 and error' do
|
||||
data[:market] = 'btcbtc'
|
||||
request
|
||||
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid uid' do
|
||||
it 'returns status 422 and error' do
|
||||
data[:market] = 'btceth'
|
||||
data[:uid] = 'invalid_uid'
|
||||
request
|
||||
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
113
spec/api/v2/management/payment_address_spec.rb
Normal file
113
spec/api/v2/management/payment_address_spec.rb
Normal file
@@ -0,0 +1,113 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::PaymentAddress, type: :request do
|
||||
let(:member1) { create(:member, :level_3) }
|
||||
let(:member2) { create(:member, :level_3) }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
write_payment_addresses: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
}
|
||||
end
|
||||
|
||||
describe 'POST /api/v2/management/deposit_address/new' do
|
||||
let(:data) { { currency: 'eth', uid: member1.uid } }
|
||||
let(:address) { 'qwerty' }
|
||||
|
||||
def request
|
||||
post_json '/api/v2/management/deposit_address/new', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
before do
|
||||
WalletService.any_instance.stubs(:create_address!).returns({ address: address, secret: 'qwerty' })
|
||||
end
|
||||
|
||||
it 'generates new address' do
|
||||
request
|
||||
expect(response_body).to eq({"address"=>address, "currencies"=>[data[:currency]], "remote"=>false, "state"=>"active", "uid"=>data[:uid]})
|
||||
expect(response).to have_http_status 200
|
||||
end
|
||||
|
||||
context 'generates new address for btc' do
|
||||
it do
|
||||
data[:currency] = 'btc'
|
||||
request
|
||||
expect(response_body).to eq({"address"=>address, "currencies"=>[data[:currency]], "remote"=>false, "state"=>"active", "uid"=>data[:uid]})
|
||||
expect(response).to have_http_status 200
|
||||
end
|
||||
end
|
||||
|
||||
context 'generates new address with specified remote value' do
|
||||
it do
|
||||
data[:remote] = true
|
||||
request
|
||||
expect(response_body).to eq({"address"=>address, "currencies"=>[data[:currency]], "remote"=>true, "state"=>"active", "uid"=>data[:uid]})
|
||||
expect(response).to have_http_status 200
|
||||
end
|
||||
end
|
||||
|
||||
context 'missing required params' do
|
||||
context 'uid' do
|
||||
it do
|
||||
data.delete(:uid)
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/uid is missing/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'currency' do
|
||||
it do
|
||||
data.delete(:currency)
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/currency is missing/i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'non-existing params applied' do
|
||||
context 'uid' do
|
||||
it do
|
||||
data[:uid] = '123456'
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.payment_address.uid_doesnt_exist/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'currency' do
|
||||
it do
|
||||
data[:currency] = 'uah'
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.payment_address.currency_doesnt_exist/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'remote' do
|
||||
it do
|
||||
data[:remote] = 'remote'
|
||||
request
|
||||
expect(response.status).to eq 422
|
||||
expect(response.body).to match(/management.payment_address.non_boolean_remote/i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'wallet service raised an error' do
|
||||
before do
|
||||
WalletService.any_instance.stubs(:create_address!).raises(StandardError.new)
|
||||
end
|
||||
|
||||
it do
|
||||
request
|
||||
expect(response.body).to match(/management.payment_address.failed_to_generate/i)
|
||||
expect(response).to have_http_status 422
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
spec/api/v2/management/tools_spec.rb
Normal file
29
spec/api/v2/management/tools_spec.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Tools, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
tools: { permitted_signers: %i[alex jeff], mandatory_signers: %i[jeff] }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'management/timestamp' do
|
||||
let(:data) { {} }
|
||||
let(:signers) { %i[jeff] }
|
||||
|
||||
def request
|
||||
post_json '/api/v2/management/timestamp',
|
||||
multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
it 'returns current time in seconds' do
|
||||
now = Time.now
|
||||
request
|
||||
expect(response).to be_successful
|
||||
expect(JSON.parse(response.body).fetch('timestamp')).to be_between(now.iso8601, (now + 1).iso8601)
|
||||
end
|
||||
end
|
||||
end
|
||||
102
spec/api/v2/management/trades_spec.rb
Normal file
102
spec/api/v2/management/trades_spec.rb
Normal file
@@ -0,0 +1,102 @@
|
||||
describe API::V2::Management::Trades, type: :request do
|
||||
let(:member) do
|
||||
create(:member, :level_3).tap do |m|
|
||||
m.get_account(:btc).update_attributes(balance: 12.13, locked: 3.14)
|
||||
m.get_account(:usd).update_attributes(balance: 2014.47, locked: 0)
|
||||
end
|
||||
end
|
||||
|
||||
let(:second_member) do
|
||||
create(:member, :level_3).tap do |m|
|
||||
m.get_account(:btc).update_attributes(balance: 12.13, locked: 3.14)
|
||||
m.get_account(:usd).update_attributes(balance: 2014.47, locked: 0)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
let(:btcusd_ask) do
|
||||
create(
|
||||
:order_ask,
|
||||
:btcusd,
|
||||
price: '12.32'.to_d,
|
||||
volume: '123.12345678',
|
||||
member: member
|
||||
)
|
||||
end
|
||||
|
||||
let(:btceth_ask) do
|
||||
create(
|
||||
:order_ask,
|
||||
:btceth,
|
||||
price: '12.326'.to_d,
|
||||
volume: '123.1234',
|
||||
member: second_member
|
||||
)
|
||||
end
|
||||
|
||||
let(:btcusd_bid) do
|
||||
create(
|
||||
:order_bid,
|
||||
:btcusd,
|
||||
price: '12.32'.to_d,
|
||||
volume: '123.12345678',
|
||||
member: member
|
||||
)
|
||||
end
|
||||
|
||||
let(:btceth_bid) do
|
||||
create(
|
||||
:order_bid,
|
||||
:btceth,
|
||||
price: '12.326'.to_d,
|
||||
volume: '123.1234',
|
||||
member: second_member
|
||||
)
|
||||
end
|
||||
|
||||
let!(:btcusd_ask_trade) { create(:trade, :btcusd, maker_order: btcusd_ask, created_at: 2.days.ago) }
|
||||
let!(:btceth_ask_trade) { create(:trade, :btceth, maker_order: btceth_ask, created_at: 2.days.ago) }
|
||||
let!(:btcusd_bid_trade) { create(:trade, :btcusd, taker_order: btcusd_bid, created_at: 23.hours.ago) }
|
||||
let!(:btceth_bid_trade) { create(:trade, :btceth, taker_order: btceth_bid, created_at: 23.hours.ago) }
|
||||
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_trades: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
}
|
||||
end
|
||||
|
||||
def request
|
||||
post_json '/api/v2/management/trades', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { {} }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
|
||||
it 'returns all recent trades' do
|
||||
request
|
||||
expect(response).to be_successful
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result.count).to eq 4
|
||||
end
|
||||
|
||||
it 'returns trades by uid of user' do
|
||||
data.merge!(uid: member.uid)
|
||||
request
|
||||
expect(response).to be_successful
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result.count).to eq 2
|
||||
end
|
||||
|
||||
it 'returns trades by market' do
|
||||
data.merge!(market: 'btcusd')
|
||||
request
|
||||
expect(response).to be_successful
|
||||
|
||||
result = JSON.parse(response.body)
|
||||
expect(result.count).to eq 2
|
||||
end
|
||||
end
|
||||
72
spec/api/v2/management/trading_fees_spec.rb
Normal file
72
spec/api/v2/management/trading_fees_spec.rb
Normal file
@@ -0,0 +1,72 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::TradingFees, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_trading_fees: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
}
|
||||
end
|
||||
|
||||
describe '/fee_schedule/trading_fees' do
|
||||
def request
|
||||
post_json '/api/v2/management/fee_schedule/trading_fees', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) {}
|
||||
let(:signers) { %i[alex jeff] }
|
||||
|
||||
before do
|
||||
create(:trading_fee, maker: 0.0005, taker: 0.001, market_id: :btcusd, group: 'vip-0')
|
||||
create(:trading_fee, maker: 0.0008, taker: 0.001, market_id: :any, group: 'vip-0')
|
||||
create(:trading_fee, maker: 0.001, taker: 0.0012, market_id: :btcusd, group: :any)
|
||||
end
|
||||
|
||||
it 'returns all trading fees tables' do
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.headers.fetch('Total')).to eq('4')
|
||||
end
|
||||
|
||||
context 'group: vip-0, market: btcusd' do
|
||||
let(:data) { { group: 'vip-0', market_id: 'btcusd' } }
|
||||
it 'returns trading fee with btcusd market_id and vip-0 group' do
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).first['maker']).to eq('0.0005')
|
||||
expect(JSON.parse(response.body).first['taker']).to eq('0.001')
|
||||
expect(JSON.parse(response.body).first['group']).to eq('vip-0')
|
||||
expect(JSON.parse(response.body).first['market_id']).to eq('btcusd')
|
||||
expect(response.headers.fetch('Total')).to eq('1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'group: any, market: btcusd' do
|
||||
let(:data) { { group: 'any', market_id: 'btcusd' } }
|
||||
it 'returns trading fee with btcusd market_id and `any` group' do
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).first['maker']).to eq('0.001')
|
||||
expect(JSON.parse(response.body).first['taker']).to eq('0.0012')
|
||||
expect(JSON.parse(response.body).first['group']).to eq('any')
|
||||
expect(JSON.parse(response.body).first['market_id']).to eq('btcusd')
|
||||
expect(response.headers.fetch('Total')).to eq('1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'group: vip-0, market: any' do
|
||||
let(:data) { { group: 'vip-0', market_id: 'any' } }
|
||||
it 'returns trading fee with btcusd market_id and `any` group' do
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).first['maker']).to eq('0.0008')
|
||||
expect(JSON.parse(response.body).first['taker']).to eq('0.001')
|
||||
expect(JSON.parse(response.body).first['group']).to eq('vip-0')
|
||||
expect(JSON.parse(response.body).first['market_id']).to eq('any')
|
||||
expect(response.headers.fetch('Total')).to eq('1')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
485
spec/api/v2/management/transfers_spec.rb
Normal file
485
spec/api/v2/management/transfers_spec.rb
Normal file
@@ -0,0 +1,485 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Transfers, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_transfers: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
write_transfers: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'create operation' do
|
||||
def request
|
||||
post_json '/api/v2/management/transfers/new', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:currency) { Currency.coins.sample }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) do
|
||||
{ key: generate(:transfer_key),
|
||||
category: Transfer::CATEGORIES.sample,
|
||||
description: "Referral program payoffs (#{Time.now.to_date})",
|
||||
operations: operations }
|
||||
end
|
||||
|
||||
let(:valid_operation) do
|
||||
{ currency: :btc,
|
||||
amount: '0.0001',
|
||||
account_src: {
|
||||
code: 102
|
||||
},
|
||||
account_dst: {
|
||||
code: 102
|
||||
} }
|
||||
end
|
||||
|
||||
context 'automatically creates an account, if transfer will be send' do
|
||||
let!(:sender_member) { create(:member, :level_3) }
|
||||
let!(:receiver_member) { create(:member, :level_3) }
|
||||
let!(:deposit) { create(:deposit_btc, member: sender_member, amount: 1) }
|
||||
|
||||
let(:operation) do
|
||||
{
|
||||
currency: :btc,
|
||||
amount: '0.5',
|
||||
account_src: {
|
||||
code: 202,
|
||||
uid: sender_member.uid
|
||||
},
|
||||
account_dst: {
|
||||
code: 202,
|
||||
uid: receiver_member.uid
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:operations) { [operation] }
|
||||
|
||||
before do
|
||||
deposit.accept!
|
||||
deposit.process!
|
||||
deposit.dispatch!
|
||||
end
|
||||
|
||||
it do
|
||||
expect(receiver_member.accounts.count).to eq(0)
|
||||
|
||||
request
|
||||
expect(response).to have_http_status(201)
|
||||
expect(receiver_member.accounts.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'empty key' do
|
||||
let(:operations) { [valid_operation] }
|
||||
|
||||
before do
|
||||
data.delete(:key)
|
||||
request
|
||||
end
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/key is missing/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'empty category' do
|
||||
let(:operations) {[valid_operation]}
|
||||
|
||||
before do
|
||||
data.delete(:category)
|
||||
request
|
||||
end
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/category is missing/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'empty description' do
|
||||
let(:operations) {[valid_operation]}
|
||||
|
||||
before do
|
||||
data.delete(:description)
|
||||
request
|
||||
end
|
||||
|
||||
it { expect(response).to have_http_status(201) }
|
||||
end
|
||||
|
||||
context 'empty operations' do
|
||||
let(:operations) {[]}
|
||||
|
||||
before { request }
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/operations is empty/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid account code' do
|
||||
let(:operations) do
|
||||
valid_operation[:account_src][:code] = 999
|
||||
[valid_operation]
|
||||
end
|
||||
before { request }
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/does not have a valid value/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid currency' do
|
||||
let(:operations) do
|
||||
valid_operation[:currency] = :neo
|
||||
[valid_operation]
|
||||
end
|
||||
before { request }
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/does not have a valid value/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid amount' do
|
||||
let(:operations) do
|
||||
valid_operation[:amount] = -1
|
||||
[valid_operation]
|
||||
end
|
||||
before { request }
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/does not have a valid value/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'existing transfer key' do
|
||||
let(:operations) {[valid_operation]}
|
||||
before do
|
||||
t = create(:transfer)
|
||||
data[:key] = t.key
|
||||
request
|
||||
end
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/key has already been taken/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'debit liability on account with insufficient balance' do
|
||||
let!(:sender_member) { create(:member, :level_3) }
|
||||
let!(:receiver_member) { create(:member, :level_3) }
|
||||
|
||||
let!(:deposit) { create(:deposit_btc, member: sender_member, amount: 1) }
|
||||
let(:operation) do
|
||||
{
|
||||
currency: :btc,
|
||||
amount: '1.1',
|
||||
account_src: {
|
||||
code: 202,
|
||||
uid: sender_member.uid
|
||||
},
|
||||
account_dst: {
|
||||
code: 202,
|
||||
uid: receiver_member.uid
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:operations) {[operation]}
|
||||
|
||||
before { request }
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/account balance is insufficient/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'referral program story' do
|
||||
# In case of referral program some fees received by platform
|
||||
# during trading are returned to the referrer once per 4h for example.
|
||||
# We debit Revenue account balance and credit member Liabilities.
|
||||
|
||||
before do
|
||||
# Credit Revenue accounts.
|
||||
create(:revenue, currency_id: base_unit,
|
||||
code: coin_revenues_code, credit: 10)
|
||||
create(:revenue, currency_id: quote_unit,
|
||||
code: fiat_revenues_code, credit: 100)
|
||||
end
|
||||
|
||||
let(:referrer1) { create(:member, :barong) }
|
||||
let(:referrer2) { create(:member, :barong) }
|
||||
let(:referrer3) { create(:member, :barong) }
|
||||
|
||||
let(:base_unit) { Currency.coins.ids.sample }
|
||||
let(:quote_unit) { Currency.fiats.ids.sample }
|
||||
|
||||
let(:coin_liabilities_code) { 202 }
|
||||
let(:fiat_liabilities_code) { 201 }
|
||||
|
||||
let(:coin_revenues_code) { 302 }
|
||||
let(:fiat_revenues_code) { 301 }
|
||||
|
||||
# Consider we have BTC/USD market
|
||||
# Return all BTC/USD fees for 4h in single batch.
|
||||
# Balance changes:
|
||||
# Liability:
|
||||
# referrer1:
|
||||
# base_unit: 0.0001 + 0.0003
|
||||
# quote_unit: 0
|
||||
# referrer2:
|
||||
# base_unit: 0.00015
|
||||
# quote_unit: 0.05
|
||||
# referrer3:
|
||||
# base_unit: 0
|
||||
# quote_unit: 0.075
|
||||
# Revenue:
|
||||
# base_unit: -(0.0001 + 0.0003 + 0.00015)
|
||||
# quote_unit -(0.05 + 0.075)
|
||||
let(:operations) do
|
||||
[
|
||||
{
|
||||
currency: base_unit,
|
||||
amount: '0.0001',
|
||||
account_src: {
|
||||
code: coin_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: referrer1.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: base_unit,
|
||||
amount: '0.00015',
|
||||
account_src: {
|
||||
code: coin_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: referrer2.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: base_unit,
|
||||
amount: '0.0003',
|
||||
account_src: {
|
||||
code: coin_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: referrer1.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: quote_unit,
|
||||
amount: '0.075',
|
||||
account_src: {
|
||||
code: fiat_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: fiat_liabilities_code,
|
||||
uid: referrer3.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: quote_unit,
|
||||
amount: '0.05',
|
||||
account_src: {
|
||||
code: fiat_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: fiat_liabilities_code,
|
||||
uid: referrer2.uid
|
||||
}
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
it do
|
||||
request
|
||||
expect(response).to have_http_status 201
|
||||
end
|
||||
|
||||
it 'returns transfer with operations' do
|
||||
request
|
||||
expect(JSON.parse(response.body)['key']).to eq data[:key]
|
||||
expect(JSON.parse(response.body)['category']).to eq data[:category]
|
||||
expect(JSON.parse(response.body)['description']).to eq data[:description]
|
||||
expect(JSON.parse(response.body)['liabilities'].size).to eq operations.size
|
||||
expect(JSON.parse(response.body)['revenues'].size).to eq operations.size
|
||||
end
|
||||
|
||||
it 'saves liabilities' do
|
||||
expect { request }.to change(::Operations::Liability, :count).by(operations.size)
|
||||
end
|
||||
|
||||
it 'saves revenues' do
|
||||
expect { request }.to change(::Operations::Revenue, :count).by(operations.size)
|
||||
end
|
||||
|
||||
it 'updates legacy balances' do
|
||||
expect { request }.to change{ referrer1.get_account(base_unit).balance }.by(0.0001 + 0.0003).and \
|
||||
change{ referrer2.get_account(base_unit).balance }.by(0.00015).and \
|
||||
change{ referrer2.get_account(quote_unit).balance }.by(0.05).and \
|
||||
change{ referrer3.get_account(quote_unit).balance }.by(0.075)
|
||||
end
|
||||
|
||||
context 'wrong account code' do
|
||||
let(:operations) do
|
||||
[
|
||||
{
|
||||
currency: base_unit,
|
||||
amount: '0.0001',
|
||||
account_src: {
|
||||
code: fiat_revenues_code # Wrong code because base_unit is coin.
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: referrer2.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: quote_unit,
|
||||
amount: '0.05',
|
||||
account_src: {
|
||||
code: fiat_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code, # Wrong code because quote_unit is fiat.
|
||||
uid: referrer2.uid
|
||||
}
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
it do
|
||||
request
|
||||
expect(response).to have_http_status 422
|
||||
end
|
||||
|
||||
it 'doesn\'t save transfer' do
|
||||
expect { request }.to_not change(Transfer, :count)
|
||||
end
|
||||
|
||||
it 'doesn\'t save liabilities' do
|
||||
expect { request }.to_not change(::Operations::Liability, :count)
|
||||
end
|
||||
|
||||
it 'doesn\'t save revenues' do
|
||||
expect { request }.to_not change(::Operations::Revenue, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'token distribution story' do
|
||||
# In token distribution story we credit member token balance
|
||||
# once member is signed in for the first time.
|
||||
before do
|
||||
# Add token-distribution Liabilities account.
|
||||
Rails.configuration.x.chart_of_accounts << coin_distribution_account
|
||||
create(:operations_account, coin_distribution_account)
|
||||
end
|
||||
|
||||
before do
|
||||
# 1. Credit main Assets account.
|
||||
# 2. Credit token-distribution Liabilities account.
|
||||
# So we keep Balance Sheet equal Income Statement.
|
||||
create(:asset, currency_id: coin,
|
||||
code: coin_assets_code, credit: 1000)
|
||||
create(:liability, currency_id: coin,
|
||||
code: coin_distribution_account_code, credit: 1000, member_id: nil)
|
||||
end
|
||||
|
||||
let(:member1) { create(:member, :barong) }
|
||||
let(:member2) { create(:member, :barong) }
|
||||
|
||||
let(:coin) { :trst }
|
||||
|
||||
let(:coin_assets_code) { 102 }
|
||||
|
||||
let(:coin_liabilities_code) { 202 }
|
||||
let(:coin_distribution_account_code) do
|
||||
coin_distribution_account[:code]
|
||||
end
|
||||
let(:coin_distribution_account) do
|
||||
{ code: 292,
|
||||
type: :liability,
|
||||
kind: 'token-distribution',
|
||||
currency_type: :coin,
|
||||
description: 'Token Distributions Liabilities Account',
|
||||
scope: :platform
|
||||
}
|
||||
end
|
||||
|
||||
# Balance changes:
|
||||
# Liability-main:
|
||||
# member1:
|
||||
# coin: 10
|
||||
# member2:
|
||||
# coin: 5
|
||||
# Liability-token-distribution:
|
||||
# coin: -15
|
||||
let(:operations) do
|
||||
[
|
||||
{
|
||||
currency: coin,
|
||||
amount: 10,
|
||||
account_src: {
|
||||
code: coin_distribution_account_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: member1.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: coin,
|
||||
amount: 5,
|
||||
account_src: {
|
||||
code: coin_distribution_account_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: member2.uid
|
||||
}
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
it do
|
||||
request
|
||||
expect(response).to have_http_status 201
|
||||
end
|
||||
|
||||
it 'returns transfer with liabilities' do
|
||||
request
|
||||
expect(JSON.parse(response.body)['key']).to eq data[:key]
|
||||
expect(JSON.parse(response.body)['category']).to eq data[:category]
|
||||
expect(JSON.parse(response.body)['description']).to eq data[:description]
|
||||
# Two liability operation for each token-distribution operation.
|
||||
expect(JSON.parse(response.body)['liabilities'].size).to eq operations.size * 2
|
||||
end
|
||||
|
||||
it 'saves liabilities' do
|
||||
expect { request }.to change(::Operations::Liability, :count).by(operations.size * 2)
|
||||
end
|
||||
|
||||
it 'updates legacy balance' do
|
||||
expect { request }.to change{ member1.get_account(coin).balance }.by(10).and \
|
||||
change{ member2.get_account(coin).balance }.by(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
468
spec/api/v2/management/withdraws_spec.rb
Normal file
468
spec/api/v2/management/withdraws_spec.rb
Normal file
@@ -0,0 +1,468 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Withdraws, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_withdraws: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
write_withdraws: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'list withdraws' do
|
||||
def request
|
||||
post_json '/api/v2/management/withdraws', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:data) { {} }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:members) { create_list(:member, 2, :barong) }
|
||||
|
||||
before do
|
||||
Withdraw::STATES.tap do |states|
|
||||
(states.count * 2).times do
|
||||
create(:btc_withdraw, :with_deposit_liability, sum: 1, member: members.sample, aasm_state: states.sample, rid: Faker::Blockchain::Bitcoin.address)
|
||||
create(:usd_withdraw, :with_deposit_liability, sum: 1, member: members.sample, aasm_state: states.sample, rid: Faker::Bank.iban)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns withdraws' do
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq Withdraw.order(id: :desc).pluck(:tid)
|
||||
end
|
||||
|
||||
it 'filters by member' do
|
||||
member = members.last
|
||||
data.merge!(uid: member.uid)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).count).to eq member.withdraws.count
|
||||
end
|
||||
|
||||
it 'filters by currency' do
|
||||
data.merge!(currency: :usd)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).count).to eq Withdraw.where(currency_id: :usd).count
|
||||
end
|
||||
|
||||
it 'filters by state' do
|
||||
Withdraw::STATES.each do |state|
|
||||
data.merge!(state: state)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).count).to eq Withdraw.where(aasm_state: state).count
|
||||
end
|
||||
end
|
||||
|
||||
it 'paginates' do
|
||||
ids = Withdraw.order(id: :desc).pluck(:tid)
|
||||
data.merge!(page: 1, limit: 4)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq ids[0...4]
|
||||
data.merge!(page: 3, limit: 4)
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq ids[8...12]
|
||||
end
|
||||
end
|
||||
|
||||
describe 'create withdraw' do
|
||||
def request
|
||||
post_json '/api/v2/management/withdraws/new', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:member) { create(:member, :barong) }
|
||||
let(:currency) { Currency.find(:btc) }
|
||||
let(:amount) { 0.1575 }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let :data do
|
||||
{ uid: member.uid,
|
||||
currency: currency.code,
|
||||
amount: amount.to_s,
|
||||
rid: Faker::Blockchain::Bitcoin.address }
|
||||
end
|
||||
let(:account) { member.get_account(currency) }
|
||||
let(:balance) { 1.2 }
|
||||
before { account.plus_funds(balance) }
|
||||
|
||||
context 'crypto withdraw' do
|
||||
it 'creates new withdraw and immediately submits it' do
|
||||
request
|
||||
expect(response).to have_http_status(201)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.sum).to eq 0.1575
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(record.rid).to eq data[:rid]
|
||||
expect(record.account).to eq account
|
||||
expect(record.account.balance).to eq (1.2 - amount)
|
||||
expect(record.account.locked).to eq amount
|
||||
expect(response_body['transfer_type']).to eq 'crypto'
|
||||
end
|
||||
|
||||
context 'disabled currency' do
|
||||
before do
|
||||
currency.update(withdrawal_enabled: false)
|
||||
end
|
||||
|
||||
it 'returns error for disabled withdrawal' do
|
||||
request
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response).to include_api_error('management.currency.withdrawal_disabled')
|
||||
end
|
||||
end
|
||||
|
||||
context 'withdrawal with beneficiary' do
|
||||
let(:beneficiary) { create(:beneficiary, state: :active, currency: currency) }
|
||||
let(:data) do
|
||||
{ uid: member.uid,
|
||||
currency: currency.code,
|
||||
amount: amount.to_s,
|
||||
beneficiary_id: beneficiary.id }
|
||||
end
|
||||
|
||||
it 'creates new withdraw and immediately submits it' do
|
||||
request
|
||||
expect(response).to have_http_status(201)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.sum).to eq 0.1575
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(record.rid).to eq beneficiary.rid
|
||||
expect(record.account).to eq account
|
||||
expect(record.account.balance).to eq (1.2 - amount)
|
||||
expect(record.account.locked).to eq amount
|
||||
end
|
||||
|
||||
context 'pending beneficiary' do
|
||||
before do
|
||||
beneficiary.update(state: :pending)
|
||||
end
|
||||
|
||||
it 'returns error for pending beneficiary' do
|
||||
request
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response).to include_api_error('management.beneficiary.invalid_state_for_withdrawal')
|
||||
end
|
||||
end
|
||||
|
||||
context 'withdrawal with note' do
|
||||
let(:member) { create(:member, :barong) }
|
||||
let(:currency) { Currency.find(:btc) }
|
||||
let(:amount) { 0.1575 }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let :data do
|
||||
{ uid: member.uid,
|
||||
currency: currency.code,
|
||||
amount: amount.to_s,
|
||||
rid: Faker::Blockchain::Bitcoin.address,
|
||||
note: 'Withdraw money'
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns new withdraw with correct note' do
|
||||
request
|
||||
expect(JSON.parse(response.body)['note']).to eq 'Withdraw money'
|
||||
end
|
||||
end
|
||||
|
||||
context 'withdrawal with transfer_type' do
|
||||
let(:member) { create(:member, :barong) }
|
||||
let(:currency) { Currency.find(:btc) }
|
||||
let(:amount) { 0.1575 }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let :data do
|
||||
{ uid: member.uid,
|
||||
currency: currency.code,
|
||||
amount: amount.to_s,
|
||||
rid: Faker::Blockchain::Bitcoin.address,
|
||||
transfer_type: 'card'
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns new withdraw with correct transfer_type' do
|
||||
request
|
||||
expect(JSON.parse(response.body)['transfer_type']).to eq 'card'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'action: :process' do
|
||||
it 'creates new withdraw and immediately submits it' do
|
||||
data.merge!(action: 'process')
|
||||
request
|
||||
expect(response).to have_http_status(201)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.sum).to eq 0.1575
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(record.rid).to eq data[:rid]
|
||||
expect(record.account).to eq account
|
||||
expect(record.account.balance).to eq (1.2 - amount)
|
||||
expect(record.account.locked).to eq amount
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid withdraw' do
|
||||
before do
|
||||
data[:action] = :process
|
||||
data[:amount] = '1000'
|
||||
end
|
||||
it 'validates enough balance' do
|
||||
request
|
||||
expect(response).to have_http_status(422)
|
||||
expect(JSON.parse(response.body)).to eq("errors"=>["Account balance is insufficient"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'extremely precise values' do
|
||||
before { Currency.any_instance.stubs(:withdraw_fee).returns(BigDecimal(0)) }
|
||||
before { Currency.any_instance.stubs(:precision).returns(16) }
|
||||
it 'keeps precision for amount' do
|
||||
currency.update!(precision: 16)
|
||||
data.merge!(amount: '0.0000000123456789')
|
||||
request
|
||||
expect(response).to have_http_status(201)
|
||||
expect(Withdraw.last.sum.to_s).to eq data[:amount]
|
||||
end
|
||||
end
|
||||
|
||||
context 'fiat withdraw' do
|
||||
let(:currency) { Currency.find(:usd) }
|
||||
let(:amount) { 5 }
|
||||
let(:balance) { 20 }
|
||||
|
||||
it 'creates new withdraw with state set to «submitted»' do
|
||||
request
|
||||
expect(response).to have_http_status(201)
|
||||
expect(account.reload.balance).to eq(15)
|
||||
expect(account.reload.locked).to eq 5
|
||||
expect(Withdraw.last.aasm_state).to eq 'accepted'
|
||||
end
|
||||
|
||||
context 'action: :process' do
|
||||
it 'creates new withdraw with state set to «submitted»' do
|
||||
data.merge!(action: :process)
|
||||
request
|
||||
expect(response).to have_http_status(201)
|
||||
expect(account.reload.balance).to eq(15)
|
||||
expect(account.reload.locked).to eq 0
|
||||
expect(Withdraw.last.aasm_state).to eq 'succeed'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'get withdraw' do
|
||||
def request
|
||||
post_json '/api/v2/management/withdraws/get', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) { { tid: record.tid } }
|
||||
let(:record) { create(:btc_withdraw, :with_deposit_liability, member: member) }
|
||||
let(:member) { create(:member, :barong) }
|
||||
|
||||
it 'returns withdraw by TID' do
|
||||
request
|
||||
expect(JSON.parse(response.body).fetch('tid')).to eq record.tid
|
||||
end
|
||||
end
|
||||
|
||||
describe 'update withdraw' do
|
||||
def request
|
||||
put_json '/api/v2/management/withdraws/action', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:currency) { Currency.find(:usd) }
|
||||
let(:member) { create(:member, :barong) }
|
||||
let(:amount) { 160.79 }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) { { tid: record.tid } }
|
||||
let(:account) { member.get_account(currency) }
|
||||
let(:record) { "Withdraws::#{currency.type.camelize}".constantize.create!(member: member, sum: amount, rid: Faker::Bank.iban, currency: currency) }
|
||||
let(:balance) { 800.77 }
|
||||
before { account.plus_funds(balance) }
|
||||
|
||||
context 'crypto withdraws' do
|
||||
let(:currency) { Currency.find(:btc) }
|
||||
|
||||
context 'action: :process' do
|
||||
before { data[:action] = :process }
|
||||
|
||||
it 'processes prepared withdraws' do
|
||||
expect(record.aasm_state).to eq 'prepared'
|
||||
expect(account.reload.balance).to eq balance
|
||||
expect(account.reload.locked).to eq 0
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(record.account.balance).to eq (balance - amount)
|
||||
expect(record.account.locked).to eq amount
|
||||
end
|
||||
|
||||
it 'processes submitted withdraws' do
|
||||
record.accept!
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(account.reload.balance).to eq (balance - amount)
|
||||
expect(account.reload.locked).to eq amount
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(record.account.balance).to eq (balance - amount)
|
||||
expect(record.account.locked).to eq amount
|
||||
end
|
||||
|
||||
it 'processes accepted withdraws' do
|
||||
record.accept!
|
||||
record.accept!
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(account.reload.balance).to eq (balance - amount)
|
||||
expect(account.reload.locked).to eq amount
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(record.account.balance).to eq (balance - amount)
|
||||
expect(record.account.locked).to eq amount
|
||||
end
|
||||
end
|
||||
|
||||
context 'action: :cancel' do
|
||||
before { data[:action] = :cancel }
|
||||
|
||||
it 'cancels prepared withdraws' do
|
||||
expect(record.aasm_state).to eq 'prepared'
|
||||
expect(account.reload.balance).to eq balance
|
||||
expect(account.reload.locked).to eq 0
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'canceled'
|
||||
expect(record.account.balance).to eq balance
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
|
||||
it 'cancels submitted withdraws' do
|
||||
record.accept!
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(account.reload.balance).to eq (balance - amount)
|
||||
expect(account.reload.locked).to eq amount
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'canceled'
|
||||
expect(record.account.balance).to eq balance
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
|
||||
it 'cancels accepted withdraws' do
|
||||
record.accept!
|
||||
record.accept!
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(account.reload.balance).to eq (balance - amount)
|
||||
expect(account.reload.locked).to eq amount
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'canceled'
|
||||
expect(record.account.balance).to eq balance
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'fiat withdraws' do
|
||||
context 'action: :process' do
|
||||
before { data[:action] = :process }
|
||||
|
||||
it 'processes prepared withdraws' do
|
||||
expect(record.aasm_state).to eq 'prepared'
|
||||
expect(account.reload.balance).to eq balance
|
||||
expect(account.reload.locked).to eq 0
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'succeed'
|
||||
expect(record.account.balance).to eq (balance - amount)
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
|
||||
it 'processes accepted withdraws' do
|
||||
record.accept!
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(account.reload.balance).to eq (balance - amount)
|
||||
expect(account.reload.locked).to eq amount
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'succeed'
|
||||
expect(record.account.balance).to eq (balance - amount)
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
|
||||
it 'processes accepted withdraws' do
|
||||
record.accept!
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(account.reload.balance).to eq (balance - amount)
|
||||
expect(account.reload.locked).to eq amount
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'succeed'
|
||||
expect(record.account.balance).to eq (balance - amount)
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'action: :cancel' do
|
||||
before { data[:action] = :cancel }
|
||||
|
||||
it 'cancels prepared withdraws' do
|
||||
expect(record.aasm_state).to eq 'prepared'
|
||||
expect(account.reload.balance).to eq balance
|
||||
expect(account.reload.locked).to eq 0
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'canceled'
|
||||
expect(record.account.balance).to eq balance
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
|
||||
it 'cancels accepted withdraws' do
|
||||
record.accept!
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(account.reload.balance).to eq (balance - amount)
|
||||
expect(account.reload.locked).to eq amount
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'canceled'
|
||||
expect(record.account.balance).to eq balance
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
|
||||
it 'cancels accepted withdraws' do
|
||||
record.accept!
|
||||
record.accept!
|
||||
expect(record.aasm_state).to eq 'accepted'
|
||||
expect(account.reload.balance).to eq (balance - amount)
|
||||
expect(account.reload.locked).to eq amount
|
||||
request
|
||||
expect(response).to have_http_status(200)
|
||||
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
|
||||
expect(record.aasm_state).to eq 'canceled'
|
||||
expect(record.account.balance).to eq balance
|
||||
expect(record.account.locked).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user