Initial commit

This commit is contained in:
Yaser
2026-08-13 19:50:53 +03:30
commit 38084458fe
879 changed files with 95198 additions and 0 deletions

26
spec/api/swagger_spec.rb Normal file
View File

@@ -0,0 +1,26 @@
# encoding: UTF-8
# frozen_string_literal: true
describe 'Swagger', type: :request do
it "returns APIv2 swagger docs" do
expect do
get "/api/v2/swagger"
expect(response).to have_http_status 200
end.not_to raise_error
end
it "returns APIv2 management swagger docs" do
expect do
get "/api/v2/management/swagger"
expect(response).to have_http_status 200
end.not_to raise_error
end
it "returns APIv2 admin swagger docs" do
expect do
get "/api/v2/admin/swagger"
expect(response).to have_http_status 200
end.not_to raise_error
end
end

View File

@@ -0,0 +1,285 @@
# frozen_string_literal: true
describe API::V2::Account::Balances, type: :request do
let(:member) { create(:member, :level_3) }
let(:deposit_btc) { create(:deposit, :deposit_btc, member: member, amount: 10) }
let(:deposit_eth) { create(:deposit, :deposit_eth, member: member, amount: 30.5) }
let(:withdraw) { create(:btc_withdraw, member: member, sum: 5) }
let(:token) { jwt_for(member) }
let(:response_body) { { 'currency' => 'eth', 'balance' => '30.5', 'locked' => '0.0', 'deposit_address' => nil } }
before do
Ability.stubs(:user_permissions).returns({'member'=>{'read'=>['Operations::Account']}})
end
before do
deposit_btc.accept!
deposit_btc.process!
deposit_btc.dispatch
deposit_eth.accept!
deposit_eth.process!
deposit_eth.dispatch
withdraw.accept!
end
describe 'GET api/v2/account/balances' do
before do
member.get_account('usd')
member.get_account('eth')
member.get_account('trst')
member.get_account('ring')
member.get_account('eur')
Currency.find(:eur).update!(visible: true)
end
context 'all balances' do
before { api_get '/api/v2/account/balances', token: token }
it 'returns current user balances' do
expect(response).to have_http_status 200
result = JSON.parse(response.body)
expect(result).to contain_exactly(
{ 'currency' => 'btc', 'balance' => '5.0', 'locked' => '5.0', 'deposit_address' => nil },
{ 'currency' => 'eth', 'balance' => '30.5', 'locked' => '0.0', 'deposit_address' => nil },
{ 'currency' => 'usd', 'balance' => '0.0', 'locked' => '0.0' },
{ 'currency' => 'trst', 'balance' => '0.0', 'locked' => '0.0', 'deposit_address' => nil },
{ 'currency' => 'ring', 'balance' => '0.0', 'locked' => '0.0', 'deposit_address' => nil },
{ 'currency' => 'eur', 'balance' => '0.0', 'locked' => '0.0' }
)
end
end
context 'use nonzero parameter == true' do
before { api_get '/api/v2/account/balances', token: token, params: {nonzero: true} }
it 'returns nonzero balances' do
expect(response).to have_http_status 200
result = JSON.parse(response.body)
expect(result).to contain_exactly(
{ 'currency' => 'btc', 'balance' => '5.0', 'locked' => '5.0', 'deposit_address' => nil },
{ 'currency' => 'eth', 'balance' => '30.5', 'locked' => '0.0', 'deposit_address' => nil },
)
end
end
context 'use nonzero parameter == false' do
before { api_get '/api/v2/account/balances', token: token, params: {nonzero: false} }
it 'returns all balances' do
expect(response).to have_http_status 200
result = JSON.parse(response.body)
expect(result).to contain_exactly(
{ 'currency' => 'btc', 'balance' => '5.0', 'locked' => '5.0', 'deposit_address' => nil },
{ 'currency' => 'eth', 'balance' => '30.5', 'locked' => '0.0', 'deposit_address' => nil },
{ 'currency' => 'usd', 'balance' => '0.0', 'locked' => '0.0' },
{ 'currency' => 'trst', 'balance' => '0.0', 'locked' => '0.0', 'deposit_address' => nil },
{ 'currency' => 'ring', 'balance' => '0.0', 'locked' => '0.0', 'deposit_address' => nil },
{ 'currency' => 'eur', 'balance' => '0.0', 'locked' => '0.0' },
)
end
end
context 'use nonzero parameter == string' do
before { api_get '/api/v2/account/balances', token: token, params: {nonzero: "token"} }
it 'returns all balances' do
expect(response).to have_http_status 422
result = JSON.parse(response.body)
expect(result).to contain_exactly(["errors", ["account.balances.invalid_nonzero"]])
end
end
context 'pagination' do
before { api_get '/api/v2/account/balances', {token: token, params: {limit: 2} } }
it 'limited user balances' do
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total').to_i).to eq member.accounts.count
expect(result.size).to eq(2)
end
end
context 'disable currency' do
before do
Currency.find(:eth).update(visible: false)
api_get '/api/v2/account/balances', token: token
end
it 'returns only balances of enabled currencies' do
result = JSON.parse(response.body)
expect(result.count).to eq 5
end
end
context 'filters' do
context 'currency_code' do
it 'filters by currency_code 1' do
api_get '/api/v2/account/balances', token: token, params: { search: {currency_code: 't'}}
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('currency')).to contain_exactly('btc', 'eth', 'trst')
end
it 'filters by currency_code 2' do
api_get '/api/v2/account/balances', token: token, params: { search: {currency_code: 'TrSt'}}
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('currency')).to contain_exactly('trst')
end
it 'filters by currency_code 3' do
api_get '/api/v2/account/balances', token: token, params: { search: {currency_code: 'abc'}}
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.blank?).to be_truthy
end
end
context 'currency_name' do
it 'filters by currency_name 1' do
api_get '/api/v2/account/balances', token: token, params: { search: {currency_name: 'Et'}}
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('currency')).to contain_exactly('eth', 'trst')
end
it 'filters by currency_name 2' do
api_get '/api/v2/account/balances', token: token, params: { search: {currency_name: 'dollar'}}
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('currency')).to contain_exactly('usd')
end
it 'filters by currency_name 3' do
api_get '/api/v2/account/balances', token: token, params: { search: {currency_name: 'abc'}}
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.blank?).to be_truthy
end
end
context 'currency_code & currency_name' do
it 'filters by code or name 1' do
api_get '/api/v2/account/balances', token: token, params: { search: {currency_name: 'abc', currency_code: 'TrSt'}}
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('currency')).to contain_exactly('trst')
end
it 'filters by code or name 2' do
api_get '/api/v2/account/balances', token: token, params: { search: {currency_name: 'Trust', currency_code: 'abc'}}
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('currency')).to contain_exactly('trst')
end
it 'filters by code or name 3' do
api_get '/api/v2/account/balances', token: token, params: { search: {currency_name: 'Eu', currency_code: 'ri'}}
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('currency')).to contain_exactly('ring', 'eur', 'eth')
end
end
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
before { api_get '/api/v2/account/balances', {token: token, params: {limit: 2} } }
it 'renders unauthorized error' do
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
context 'email changed' do
let(:new_member_email) { Faker::Internet.email }
it do
old_member_email = member.email
member.email = new_member_email
api_get '/api/v2/account/balances', {token: jwt_for(member), params: {limit: 2} }
expect(response).to be_successful
member.reload
expect(member.email).to eq new_member_email
expect(member.email).to_not eq old_member_email
end
end
end
describe 'GET api/v2/account/balances/:currency' do
before { api_get '/api/v2/account/balances/eth', token: token }
it 'returns current user balance by currency' do
expect(response).to have_http_status 200
result = JSON.parse(response.body)
expect(result).to match response_body
end
context 'currency code with dot' do
let!(:currency) { create(:currency, :xagm_cx) }
let!(:account) { ::Account.create(currency_id: 'xagm.cx', member_id: member.id)}
it 'returns current user balance by currency' do
api_get "/api/v2/account/balances/#{currency.code}", token: token
expect(response).to have_http_status 200
result = JSON.parse(response.body)
expect(result['currency']).to eq currency.code
end
end
context 'invalid currency' do
before { api_get '/api/v2/account/balances/somecoin', token: token }
it do
expect(response).to have_http_status 422
expect(response).to include_api_error('account.currency.doesnt_exist')
end
end
context 'disable currency' do
before do
Currency.find(:eth).update(visible: false)
api_get '/api/v2/account/balances/eth', token: token
end
it do
expect(response).to have_http_status 422
end
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
before { api_get '/api/v2/account/balances/eth', token: token }
it 'renders unauthorized error' do
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
end

View File

@@ -0,0 +1,867 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Account::Beneficiaries, 'GET', type: :request do
let(:endpoint) { '/api/v2/account/beneficiaries' }
let(:member) { create(:member, :level_3) }
let(:token) { jwt_for(member) }
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
def response_body
JSON.parse(response.body)
end
before do
Ability.stubs(:user_permissions).returns({'member'=>{'read'=>['Beneficiary'],'update'=>['Beneficiary'],
'create'=> ['Beneficiary'],'destroy'=> ['Beneficiary']}})
end
context 'without JWT' do
it do
get endpoint
expect(response.status).to eq 401
end
end
# TODO: Not enough level spec.
# TODO: Paginate spec.
context 'without currency and state' do
it do
api_get endpoint, token: token
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
api_get endpoint, params: { currency: :uah }, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.currency.doesnt_exist')
end
end
context 'existing currency' do
let!(:btc_beneficiaries_for_member) do
create_list(:beneficiary, 3, member: member)
end
it do
api_get endpoint, params: { currency: :btc }, token: token
expect(response.status).to eq 200
expect(response_body.all? { |b| b['currency'] == 'btc' }).to be_truthy
end
end
context 'invalid state' do
it do
api_get endpoint, params: { state: :invalid }, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.invalid_state')
end
end
context 'existing state' do
it do
api_get endpoint, params: { state: :pending }, token: token
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
api_get endpoint, params: { currency: :btc, state: :active }, token: token
expect(response.status).to eq 200
expect(response_body.all? { |b| b['currency'] == 'btc' && b['state'] == 'active' }).to be_truthy
end
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
let!(:active_btc_beneficiaries_for_member) do
create_list(:beneficiary, 3, member: member, state: :active)
end
it 'renders unauthorized error' do
api_get endpoint, params: { currency: :btc, state: :active }, token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
describe API::V2::Account::Beneficiaries, 'GET /:id', type: :request do
let(:endpoint) { '/api/v2/account/beneficiaries' }
let(:member) { create(:member, :level_3) }
let(:token) { jwt_for(member) }
def response_body
JSON.parse(response.body)
end
context 'pending beneficiary' do
let(:endpoint) { "/api/v2/account/beneficiaries/#{pending_beneficiary.id}" }
let!(:pending_beneficiary) { create(:beneficiary, member: member) }
it do
api_get endpoint, token: token
expect(response.status).to eq 200
expect(response_body['id']).to eq pending_beneficiary.id
end
end
context 'active beneficiary' do
let(:endpoint) { "/api/v2/account/beneficiaries/#{active_beneficiary.id}" }
let!(:active_beneficiary) { create(:beneficiary, state: :active, member: member) }
it do
api_get endpoint, token: token
expect(response.status).to eq 200
expect(response_body['id']).to eq active_beneficiary.id
end
end
context 'fiat beneficiary' do
let!(:fiat_beneficiary) { create(:beneficiary, currency: Currency.find('usd'), member: member) }
let(:endpoint) { "/api/v2/account/beneficiaries/#{fiat_beneficiary.id}" }
it do
api_get endpoint, token: token
expect(response.status).to eq 200
expect(response_body['id']).to eq fiat_beneficiary.id
expect(response_body['data']['account_number']).to eq fiat_beneficiary.masked_account_number
end
end
context 'archived beneficiary' do
let(:endpoint) { "/api/v2/account/beneficiaries/#{archived_beneficiary.id}" }
let!(:archived_beneficiary) { create(:beneficiary, state: :archived, member: member) }
it do
api_get endpoint, token: token
expect(response.status).to eq 404
end
end
context 'other member beneficiary' do
let(:endpoint) { "/api/v2/account/beneficiaries/#{pending_beneficiary.id}" }
let(:member2) { create(:member, :level_3) }
let!(:pending_beneficiary) { create(:beneficiary, member: member2) }
it do
api_get endpoint, token: token
expect(response.status).to eq 404
end
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
let(:endpoint) { "/api/v2/account/beneficiaries/#{pending_beneficiary.id}" }
let(:member2) { create(:member, :level_3) }
let!(:pending_beneficiary) { create(:beneficiary, member: member2) }
it 'renders unauthorized error' do
api_get endpoint, token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
describe API::V2::Account::Beneficiaries, 'POST', type: :request do
let(:endpoint) { '/api/v2/account/beneficiaries' }
let(:member) { create(:member, :level_3) }
let(:token) { jwt_for(member) }
let(:beneficiary_data) do
{
currency: :btc,
name: 'Personal Bitcoin wallet',
description: 'Multisignature Bitcoin Wallet',
data: {
address: Faker::Blockchain::Bitcoin.address
}
}
end
def response_body
JSON.parse(response.body)
end
context 'without JWT' do
it do
post endpoint
expect(response.status).to eq 401
end
end
context 'invalid params' do
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_post endpoint, params: beneficiary_data.merge(description: Faker::String.random(120)), token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
context 'missing required params' do
%i[currency name data].each do |rp|
context rp do
it do
api_post endpoint, params: beneficiary_data.except(rp), token: token
expect(response.status).to eq 422
expect(response).to include_api_error("account.beneficiary.missing_#{rp}")
end
end
end
end
context 'currency doesn\'t exist' do
it do
api_post endpoint, params: beneficiary_data.merge(currency: :uah), token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.currency.doesnt_exist')
end
end
context 'name is too long' do
it do
api_post endpoint, params: beneficiary_data.merge(name: Faker::String.random(65)), token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.too_long_name')
end
end
context 'description is too long' do
it do
api_post endpoint, params: beneficiary_data.merge(description: Faker::String.random(256)), token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.too_long_description')
end
end
context 'data has invalid type' do
it do
api_post endpoint, params: beneficiary_data.merge(data: 'data'), token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.non_json_data')
end
end
context 'crypto beneficiary' do
context 'nil address in data' do
it do
beneficiary_data[:data][:address] = nil
api_post endpoint, params: beneficiary_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.missing_address_in_data')
end
end
context 'data without address' do
it do
beneficiary_data[:data].delete(:address)
beneficiary_data[:data][:memo] = :memo
api_post endpoint, params: beneficiary_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.missing_address_in_data')
end
end
context 'disabled withdrawal for currency' do
let(:currency) { Currency.find(:btc) }
before do
currency.update(withdrawal_enabled: false)
end
it do
api_post endpoint, params: beneficiary_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.currency.withdrawal_disabled')
end
end
context 'invalid character in address' do
before do
beneficiary_data[:data][:address] = "'" + Faker::Blockchain::Bitcoin.address
end
it do
api_post endpoint, params: beneficiary_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.failed_to_create')
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
api_post endpoint, params: beneficiary_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.duplicate_address')
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
api_post endpoint, params: beneficiary_data, token: token
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
api_post endpoint, params: beneficiary_data, token: token
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
api_post endpoint, params: beneficiary_data, token: token
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
# TODO: Test nil full_name in data for both fiat and crypto.
end
context 'fiat beneficiary' do
let(:fiat_beneficiary_data) do
{
currency: :usd,
name: Faker::Bank.name,
description: Faker::Company.catch_phrase,
data: generate(:fiat_beneficiary_data)
}
end
context 'nil address in data' do
it do
fiat_beneficiary_data[:data].delete(:address)
api_post endpoint, params: fiat_beneficiary_data, token: token
expect(response.status).to eq 201
expect(response_body['data']['account_number']).not_to eq fiat_beneficiary_data[:data][:account_number]
end
end
context 'nil data' do
it do
fiat_beneficiary_data[:data] = nil
api_post endpoint, params: fiat_beneficiary_data.except(:data), token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.empty_data')
end
end
context 'duplicated address' do
context 'same currency' do
before do
create(:beneficiary,
member: member,
currency_id: fiat_beneficiary_data[:currency],
data: fiat_beneficiary_data[:data])
end
it do
api_post endpoint, params: fiat_beneficiary_data, token: token
expect(response.status).to eq 201
end
end
end
end
end
context 'valid params' do
it 'creates beneficiary for member' do
expect do
api_post endpoint, params: beneficiary_data, token: token
end.to change{ member.beneficiaries.count }.by(1)
end
it 'creates beneficiary with pending state' do
api_post endpoint, params: beneficiary_data, token: token
expect(response.status).to eq 201
id = response_body['id']
expect(Beneficiary.find_by!(id: id).state).to eq 'pending'
end
end
end
describe API::V2::Account::Beneficiaries, 'PATCH /activate', type: :request do
let(:member) { create(:member, :level_3) }
let(:token) { jwt_for(member) }
def response_body
JSON.parse(response.body)
end
context 'invalid params' do
let!(:pending_beneficiary) { create(:beneficiary, member: member) }
let(:activation_data) do
{ id: pending_beneficiary.id,
pin: pending_beneficiary.pin }
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
let(:endpoint) do
"/api/v2/account/beneficiaries/#{pending_beneficiary.id}/activate"
end
it 'renders unauthorized error' do
api_patch endpoint, params: activation_data, token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
context 'id has invalid type' do
let(:endpoint) do
"/api/v2/account/beneficiaries/id/activate"
end
it do
api_patch endpoint, params: activation_data.merge(id: :id), token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.non_integer_id')
end
end
context 'pin has invalid type' do
let(:endpoint) do
"/api/v2/account/beneficiaries/#{pending_beneficiary.id}/activate"
end
it do
api_patch endpoint, params: activation_data.merge(pin: :pin), token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.non_integer_pin')
end
end
end
context 'pending beneficiary' do
let(:endpoint) do
"/api/v2/account/beneficiaries/#{pending_beneficiary.id}/activate"
end
let(:activation_data) do
{ id: pending_beneficiary.id,
pin: pending_beneficiary.pin }
end
let!(:pending_beneficiary) { create(:beneficiary, member: member) }
context 'valid pin' do
it do
api_patch endpoint, params: activation_data, token: token
expect(response.status).to eq 200
expect(response_body['id']).to eq pending_beneficiary.id
expect(response_body['state']).to eq 'active'
end
end
context 'invalid pin' do
it do
activation_data[:pin] = activation_data[:pin] + 1
api_patch endpoint, params: activation_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.invalid_pin')
end
end
end
context 'active beneficiary' do
let(:endpoint) do
"/api/v2/account/beneficiaries/#{active_beneficiary.id}/activate"
end
let(:activation_data) do
{ id: active_beneficiary.id,
pin: active_beneficiary.pin }
end
let!(:active_beneficiary) { create(:beneficiary, state: :active, member: member) }
context 'valid pin' do
it do
api_patch endpoint, params: activation_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.cant_activate')
end
end
context 'invalid pin' do
it do
activation_data[:pin] = activation_data[:pin] + 1
api_patch endpoint, params: activation_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.cant_activate')
end
end
end
context 'archived beneficiary' do
let(:endpoint) do
"/api/v2/account/beneficiaries/#{archived_beneficiary.id}/activate"
end
let(:activation_data) do
{ id: archived_beneficiary.id,
pin: archived_beneficiary.pin }
end
let!(:archived_beneficiary) { create(:beneficiary, state: :archived, member: member) }
context 'any pin' do
it do
api_patch endpoint, params: activation_data, token: token
expect(response.status).to eq 404
end
end
end
context 'other user beneficiary' do
let(:member2) { create(:member, :level_3) }
let(:endpoint) do
"/api/v2/account/beneficiaries/#{pending_beneficiary.id}/activate"
end
let(:activation_data) do
{ id: pending_beneficiary.id,
pin: pending_beneficiary.pin }
end
let!(:pending_beneficiary) { create(:beneficiary, member: member2) }
context 'any pin' do
it do
api_patch endpoint, params: activation_data, token: token
expect(response.status).to eq 404
end
end
end
end
describe API::V2::Account::Beneficiaries, 'PATCH /resend_pin', type: :request do
let(:member) { create(:member, :level_3) }
let(:token) { jwt_for(member) }
def response_body
JSON.parse(response.body)
end
context 'invalid params' do
let!(:pending_beneficiary) { create(:beneficiary, member: member) }
let(:resend_data) do
{ id: pending_beneficiary.id }
end
context 'id has invalid type' do
let(:endpoint) do
"/api/v2/account/beneficiaries/id/resend_pin"
end
it do
api_patch endpoint, params: resend_data.merge(id: :id), token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.non_integer_id')
end
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
let(:endpoint) do
"/api/v2/account/beneficiaries/#{pending_beneficiary.id}/resend_pin"
end
it 'renders unauthorized error' do
api_patch endpoint, params: resend_data, token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
context 'pending beneficiary' do
let(:endpoint) do
"/api/v2/account/beneficiaries/#{pending_beneficiary.id}/resend_pin"
end
let(:resend_data) do
{ id: pending_beneficiary.id }
end
let!(:pending_beneficiary) { create(:beneficiary, member: member) }
context '1 minute from last request on create or resend passed' do
it do
pending_beneficiary.update(sent_at: 1.minute.ago)
api_patch endpoint, params: resend_data, token: token
expect(response.status).to eq 204
end
end
context '1 minute from last request on create or resend not passed' do
it do
api_patch endpoint, params: resend_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.cant_resend_within_1_minute')
expect(response_body.include?("sent_at")).to eq true
end
end
end
context 'active beneficiary' do
let(:endpoint) do
"/api/v2/account/beneficiaries/#{active_beneficiary.id}/resend_pin"
end
let(:resend_data) do
{ id: active_beneficiary.id }
end
let!(:active_beneficiary) { create(:beneficiary, state: :active, member: member) }
context '1 minute from last request on create or resend passed' do
it do
api_patch endpoint, params: resend_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.cant_resend')
end
end
context '1 minute from last request on create or resend not passed' do
it do
api_patch endpoint, params: resend_data, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.cant_resend')
end
end
end
context 'archived beneficiary' do
let(:endpoint) do
"/api/v2/account/beneficiaries/#{archived_beneficiary.id}/resend_pin"
end
let(:resend_data) do
{ id: archived_beneficiary.id }
end
let!(:archived_beneficiary) { create(:beneficiary, state: :archived, member: member) }
it do
api_patch endpoint, params: resend_data, token: token
expect(response.status).to eq 404
end
end
context 'other user beneficiary' do
let(:member2) { create(:member, :level_3) }
let(:endpoint) do
"/api/v2/account/beneficiaries/#{pending_beneficiary.id}/resend_pin"
end
let(:activation_data) do
{ id: pending_beneficiary.id,
pin: pending_beneficiary.pin }
end
let!(:pending_beneficiary) { create(:beneficiary, member: member2) }
it do
api_patch endpoint, params: activation_data, token: token
expect(response.status).to eq 404
end
end
end
describe API::V2::Account::Beneficiaries, 'DELETE /:id', type: :request do
let(:member) { create(:member, :level_3) }
let(:token) { jwt_for(member) }
def response_body
JSON.parse(response.body)
end
context 'invalid params' do
let!(:pending_beneficiary) { create(:beneficiary, member: member) }
let(:activation_data) do
{ id: pending_beneficiary.id,
pin: pending_beneficiary.pin }
end
context 'id has invalid type' do
let(:endpoint) do
"/api/v2/account/beneficiaries/id"
end
it do
api_delete endpoint, token: token
expect(response.status).to eq 422
expect(response).to include_api_error('account.beneficiary.non_integer_id')
end
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
let(:endpoint) do
"/api/v2/account/beneficiaries/#{pending_beneficiary.id}"
end
it 'renders unauthorized error' do
api_delete endpoint, token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
context 'pending beneficiary' do
let(:endpoint) do
"/api/v2/account/beneficiaries/#{pending_beneficiary.id}"
end
let!(:pending_beneficiary) { create(:beneficiary, member: member) }
it do
api_delete endpoint, token: token
expect(response.status).to eq 204
expect(response.body).to be_empty
end
end
context 'active beneficiary' do
let(:endpoint) do
"/api/v2/account/beneficiaries/#{active_beneficiary.id}"
end
let!(:active_beneficiary) { create(:beneficiary, state: :active, member: member) }
it do
api_delete endpoint, token: token
expect(response.status).to eq 204
expect(response.body).to be_empty
end
end
context 'archived beneficiary' do
let(:endpoint) do
"/api/v2/account/beneficiaries/#{archived_beneficiary.id}"
end
let!(:archived_beneficiary) { create(:beneficiary, state: :archived, member: member) }
it do
api_delete endpoint, token: token
expect(response.status).to eq 404
end
end
context 'other user beneficiary' do
let(:member2) { create(:member, :level_3) }
let(:endpoint) do
"/api/v2/account/beneficiaries/#{pending_beneficiary.id}"
end
let!(:pending_beneficiary) { create(:beneficiary, member: member2) }
it do
api_delete endpoint, token: token
expect(response.status).to eq 404
end
end
end

View File

@@ -0,0 +1,271 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Account::Deposits, type: :request do
let(:member) { create(:member, :level_3) }
let(:other_member) { create(:member, :level_3) }
let(:token) { jwt_for(member) }
let(:level_0_member) { create(:member, :level_0) }
let(:level_0_member_token) { jwt_for(level_0_member) }
before do
Ability.stubs(:user_permissions).returns({'member'=>{'read'=>['Deposit', 'PaymentAddress']}})
end
describe 'GET /api/v2/account/deposits' do
before do
create(:deposit_btc, member: member, updated_at: 5.days.ago)
create(:deposit_usd, member: member, updated_at: 5.days.ago)
create(:deposit_usd, member: member, txid: 1, amount: 520, updated_at: 5.hour.ago)
create(:deposit_btc, member: member, txid: 'test', amount: 111, updated_at: 2.hour.ago)
create(:deposit_usd, member: other_member, txid: 10)
end
it 'requires authentication' do
api_get '/api/v2/account/deposits'
expect(response.code).to eq '401'
end
it 'returns with auth token deposits' do
api_get '/api/v2/account/deposits', token: token
expect(response).to be_successful
end
it 'returns all deposits num' do
api_get '/api/v2/account/deposits', token: token
result = JSON.parse(response.body)
expect(result.size).to eq 4
expect(response.headers.fetch('Total')).to eq '4'
end
it 'returns limited deposits' do
api_get '/api/v2/account/deposits', params: { limit: 2, page: 1 }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 2
expect(response.headers.fetch('Total')).to eq '4'
api_get '/api/v2/account/deposits', params: { limit: 1, page: 2 }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 1
expect(response.headers.fetch('Total')).to eq '4'
end
it 'filters deposits by state' do
api_get '/api/v2/account/deposits', params: { state: 'canceled' }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 0
d = create(:deposit_btc, member: member, aasm_state: :canceled)
api_get '/api/v2/account/deposits', params: { state: 'canceled' }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 1
expect(result.first['txid']).to eq d.txid
end
it 'filters deposits by multiple states' do
create(:deposit_btc, member: member, aasm_state: :rejected)
api_get '/api/v2/account/deposits', params: { state: ['canceled', 'rejected'] }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 1
create(:deposit_btc, member: member, aasm_state: :canceled)
api_get '/api/v2/account/deposits', params: { state: ['canceled', 'rejected'] }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 2
end
it 'returns deposits for the last two days' do
api_get '/api/v2/account/deposits', params: { limit: 5, page: 1, time_from: 2.days.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 2
expect(response.headers.fetch('Total')).to eq '2'
end
it 'returns deposits before 2 days ago' do
api_get '/api/v2/account/deposits', params: { time_to: 2.days.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 2
expect(response.headers.fetch('Total')).to eq '2'
end
it 'returns deposits for currency usd' do
api_get '/api/v2/account/deposits', params: { currency: 'usd' }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 2
expect(result.all? { |d| d['currency'] == 'usd' }).to be_truthy
end
it 'returns deposits with txid filter' do
api_get '/api/v2/account/deposits', params: { txid: Deposit.first.txid }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 1
expect(result.all? { |d| d['txid'] == Deposit.first.txid }).to be_truthy
end
it 'returns deposits for currency btc' do
api_get '/api/v2/account/deposits', params: { currency: 'btc' }, token: token
result = JSON.parse(response.body)
expect(response.headers.fetch('Total')).to eq '2'
expect(result.all? { |d| d['currency'] == 'btc' }).to be_truthy
end
it 'return 404 if txid not exist' do
api_get '/api/v2/account/deposits/5', token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'returns 404 if txid not belongs_to you ' do
api_get '/api/v2/account/deposits/10', token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'returns deposit txid if exist' do
api_get '/api/v2/account/deposits/1', token: token
result = JSON.parse(response.body)
expect(response.code).to eq '200'
expect(result['amount']).to eq '520.0'
end
it 'returns deposit no time limit ' do
api_get '/api/v2/account/deposits/test', token: token
result = JSON.parse(response.body)
expect(response.code).to eq '200'
expect(result['amount']).to eq '111.0'
end
it 'denies access to unverified member' do
api_get '/api/v2/account/deposits', token: level_0_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('account.deposit.not_permitted')
end
context 'fail' do
it 'validates time_from param' do
api_get '/api/v2/account/deposits', params: { time_from: 'btc' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.deposit.non_integer_time_from')
end
it 'validates time_to param' do
api_get '/api/v2/account/deposits', params: { time_to: [] }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.deposit.non_integer_time_to')
end
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_get '/api/v2/account/deposits/test', token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
describe 'GET /api/v2/account/deposit_address/:currency' do
let(:currency) { :bch }
context 'failed' do
it 'validates currency' do
api_get "/api/v2/account/deposit_address/dildocoin", token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('account.currency.doesnt_exist')
end
it 'validates currency address format' do
api_get '/api/v2/account/deposit_address/btc', params: { address_format: 'cash' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('account.deposit_address.doesnt_support_cash_address_format')
end
it 'validates currency with address_format param' do
api_get '/api/v2/account/deposit_address/abc', params: { address_format: 'cash' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('account.currency.doesnt_exist')
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_get '/api/v2/account/deposit_address/btc', token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
context 'successful' do
context 'eth address' do
let(:currency) { :eth }
let(:wallet) { Wallet.deposit.joins(:currencies).find_by(currencies: { id: currency }) }
before { member.payment_address(wallet.id).update!(address: '2N2wNXrdo4oEngp498XGnGCbru29MycHogR') }
it 'expose data about eth address' do
api_get "/api/v2/account/deposit_address/#{currency}", token: token
expect(response.body).to eq '{"currencies":["eth"],"address":"2n2wnxrdo4oengp498xgngcbru29mychogr","state":"active"}'
end
it 'pending user address state' do
member.payment_address(wallet.id).update!(address: nil)
api_get "/api/v2/account/deposit_address/#{currency}", token: token
expect(response.body).to eq '{"currencies":["eth"],"address":null,"state":"pending"}'
end
context 'currency code with dot' do
let!(:currency) { create(:currency, :xagm_cx) }
it 'returns information about specified deposit address' do
api_get "/api/v2/account/deposit_address/#{currency.code}", token: token
expect(response).to have_http_status 200
expect(response.body).to eq '{"currencies":["eth","xagm.cx"],"address":"2n2wnxrdo4oengp498xgngcbru29mychogr","state":"active"}'
end
end
it 'exposes non-remote addresses' do
member.payment_address(wallet.id).update!(remote: true)
api_get "/api/v2/account/deposit_address/#{currency}", token: token
expect(response.body).to eq '{"currencies":["eth"],"address":null,"state":"pending"}'
end
end
end
context 'disabled deposit for currency' do
let(:currency) { :btc }
before { Currency.find(currency).update!(deposit_enabled: false) }
it 'returns error' do
api_get "/api/v2/account/deposit_address/#{currency}", token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('account.currency.deposit_disabled')
end
end
end
end

View File

@@ -0,0 +1,196 @@
# frozen_string_literal: true
describe API::V2::Account::InternalTransfers, type: :request do
let(:endpoint) { '/api/v2/account/internal_transfers' }
let(:member) { create(:member, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0', username: 'membertest') }
let(:member_receiver) { create(:member, :level_3, email: 'receiver@gmail.com', uid: 'ID84BF61C8H0', username: 'test1') }
let(:token) { jwt_for(member) }
describe 'GET /api/v2/account/internal_transfers' do
let!(:internal_transfer_btc) { create_list(:internal_transfer_btc, 4, :with_deposit_liability, sender: member) }
let!(:internal_transfer_usd) { create_list(:internal_transfer_usd, 6, :with_deposit_liability, sender: member_receiver) }
let!(:internal_transfer_usd) { create_list(:internal_transfer_usd, 3, :with_deposit_liability, receiver: member) }
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_get endpoint, token: token, params: { limit: 100 }
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
it 'requires authentication' do
get endpoint
expect(response.code).to eq '401'
end
it 'validates currency param' do
api_get endpoint, params: { currency: 'FOO' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.currency.doesnt_exist')
end
it 'returns internal transfers for all currencies by default' do
api_get endpoint, params: { limit: 100 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '7'
expect(result.map { |x| x['currency'] }.uniq.sort).to eq %w[ btc usd ]
end
it 'returns all internal transfers' do
api_get endpoint, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 7
expect(response.headers.fetch('Total')).to eq '7'
end
it 'returns internal transfers of BTC currency' do
api_get endpoint, params: { currency: 'btc' }, token: token
result = JSON.parse(response.body)
expect(result.count).to eq 4
end
it 'returns internal transfers of USD currency' do
api_get endpoint, params: { currency: 'usd' }, token: token
result = JSON.parse(response.body)
expect(result.count).to eq 3
end
end
describe "create internal transfer" do
let(:currency) { Currency.visible.sample; Currency.find(:eth) }
let(:amount) { 0.15 }
let :data do
{ username_or_uid: member_receiver.uid,
currency: currency.code,
amount: amount,
otp: 123456 }
end
let(:account) { member.get_account(currency) }
let(:balance) { 1.2 }
before { account.plus_funds(balance) }
before { Vault::TOTP.stubs(:validate?).returns(true) }
it 'validates missing params' do
data.except!(:otp, :amount, :currency, :username_or_uid)
api_post endpoint, params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.internaltransfer.missing_otp')
expect(response).to include_api_error('account.internaltransfer.missing_amount')
expect(response).to include_api_error('account.internaltransfer.missing_currency')
expect(response).to include_api_error('account.internaltransfer.empty_username_or_uid')
end
it 'requires otp' do
data[:otp] = nil
api_post endpoint, params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.internaltransfer.empty_otp')
end
it 'validates otp code' do
Vault::TOTP.stubs(:validate?).returns(false)
api_post endpoint, params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.internal_transfer.invalid_otp')
end
it 'requires amount' do
data[:amount] = nil
api_post endpoint, params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.internal_transfer.non_positive_amount')
end
it 'validates negative amount' do
data[:amount] = -1
api_post endpoint, params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.internal_transfer.non_positive_amount')
end
it 'validates enough balance' do
data[:amount] = 100
api_post endpoint, params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.internal_transfer.insufficient_balance')
end
it 'validates amount type' do
data[:amount] = 'one'
api_post endpoint, params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.internal_transfer.non_decimal_amount')
end
it 'requires currency' do
data[:currency] = nil
api_post endpoint, params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.currency.doesnt_exist')
end
it 'creates new internal_transfer' do
api_post endpoint, params: data, token: token
expect(response).to have_http_status(201)
record = InternalTransfer.last
expect(record.sender_id).to eq member.id
expect(record.receiver_id).to eq member_receiver.id
expect(record.amount).to eq amount
expect(record.currency).to eq currency
end
it 'creates new internal_transfer using username' do
data[:username_or_uid] = member_receiver.username
api_post endpoint, params: data, token: token
expect(response).to have_http_status(201)
record = InternalTransfer.last
expect(record.sender_id).to eq member.id
expect(record.receiver_id).to eq member_receiver.id
expect(record.amount).to eq amount
expect(record.currency).to eq currency
end
it 'should change balance for receiver and sender after transfer' do
api_post endpoint, params: data, token: token
account.reload.balance
expect(response).to have_http_status(201)
record = InternalTransfer.last
expect(account.balance).to eq (balance - amount)
expect(member_receiver.get_account(currency.code).balance).to eq amount
end
it 'not allow create new internal_transfer to yourself' do
data[:username_or_uid] = member.uid
api_post endpoint, params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.internal_transfer.can_not_tranfer_to_yourself')
end
end
end

View File

@@ -0,0 +1,103 @@
# frozen_string_literal: true
describe API::V2::Account::Stats, type: :request do
let(:member) { create(:member, :level_3) }
let(:token) { jwt_for(member) }
before do
Ability.stubs(:user_permissions).returns({'member'=>{'read'=>['StatsMemberPnl']}})
end
describe 'GET /api/v2/account/stats/pnl' do
let!(:eth) { Currency.find('eth') }
let!(:btc) { Currency.find('btc') }
let!(:pnl1) { create(:stats_member_pnl, pnl_currency_id: eth.id, currency_id: btc.id,
total_credit: 0.1, total_credit_fees: 0.01, total_debit_fees: 0.02, total_credit_value: 0.3, total_debit: 0.2,
total_debit_value: 10.0, average_balance_price: 0.42, member: member)}
let!(:pnl2) { create(:stats_member_pnl, pnl_currency_id: btc.id, currency_id: eth.id,
total_credit: 0.1, total_credit_fees: 0.01, total_debit_fees: 0.02, total_credit_value: 0.3, total_debit: 0.2,
total_debit_value: 10.0, average_balance_price: 0.21, member: member)}
it 'returns all user pnls for all pnl currencies' do
api_get '/api/v2/account/stats/pnl', token: token
expect(response).to be_successful
expect(response_body.count).to eq 2
expect(response_body[0]['currency']).to eq(pnl1.currency_id)
expect(response_body[0]['pnl_currency']).to eq(pnl1.pnl_currency_id)
expect(response_body[0]['total_credit'].to_f).to eq(pnl1.total_credit)
expect(response_body[0]['total_credit_value'].to_f).to eq(pnl1.total_credit_value)
expect(response_body[0]['total_debit'].to_f).to eq(pnl1.total_debit)
expect(response_body[0]['total_debit_value'].to_f).to eq(pnl1.total_debit_value)
expect(response_body[0]['average_buy_price'].to_f.round(9)).to eq( (pnl1.total_credit_value / (pnl1.total_credit)).to_f)
expect(response_body[0]['average_sell_price'].to_f.round(9)).to eq(pnl1.total_debit_value / (pnl1.total_debit))
expect(response_body[0]['average_balance_price'].to_f).to eq(0.42)
expect(response_body[1]['currency']).to eq(pnl2.currency_id)
expect(response_body[1]['pnl_currency']).to eq(pnl2.pnl_currency_id)
expect(response_body[1]['total_credit'].to_f).to eq(pnl2.total_credit)
expect(response_body[1]['total_credit_value'].to_f).to eq(pnl2.total_credit_value)
expect(response_body[1]['total_debit'].to_f).to eq(pnl2.total_debit)
expect(response_body[1]['total_debit_value'].to_f).to eq(pnl2.total_debit_value)
expect(response_body[1]['average_buy_price'].to_f.round(9)).to eq( (pnl2.total_credit_value / (pnl2.total_credit)).to_f)
expect(response_body[1]['average_sell_price'].to_f.round(9)).to eq(pnl2.total_debit_value / (pnl2.total_debit))
expect(response_body[1]['average_balance_price'].to_f).to eq(0.21)
end
it 'returns user pnls for pnl currency eth' do
api_get '/api/v2/account/stats/pnl?pnl_currency=eth', token: token
expect(response).to be_successful
expect(response_body.count).to eq 1
expect(response_body[0]['currency']).to eq(pnl1.currency_id)
expect(response_body[0]['pnl_currency']).to eq(pnl1.pnl_currency_id)
expect(response_body[0]['total_credit'].to_f).to eq(pnl1.total_credit)
expect(response_body[0]['total_credit_value'].to_f).to eq(pnl1.total_credit_value)
expect(response_body[0]['total_debit'].to_f).to eq(pnl1.total_debit)
expect(response_body[0]['total_debit_value'].to_f).to eq(pnl1.total_debit_value)
expect(response_body[0]['average_buy_price'].to_f.round(9)).to eq( (pnl1.total_credit_value / (pnl1.total_credit)).to_f)
expect(response_body[0]['average_sell_price'].to_f.round(9)).to eq(pnl1.total_debit_value / (pnl1.total_debit))
expect(response_body[0]['average_balance_price'].to_f).to eq(0.42)
end
context 'avarage sell price equal to 0' do
let!(:usd) { Currency.find('usd') }
let!(:pnl) { create(:stats_member_pnl, pnl_currency_id: usd.id, currency_id: btc.id,
total_credit: 0.1, total_credit_fees: 0.01, total_debit_fees: 0.0, total_credit_value: 0.3, total_debit: 0.0,
total_debit_value: 0.0, average_balance_price: 0.12, member: member)}
it 'return user pnl with zero avarage sell price' do
api_get '/api/v2/account/stats/pnl?pnl_currency=usd', token: token
expect(response).to be_successful
expect(response_body.count).to eq 1
expect(response_body[0]['currency']).to eq(pnl.currency_id)
expect(response_body[0]['pnl_currency']).to eq(pnl.pnl_currency_id)
expect(response_body[0]['total_credit'].to_f).to eq(pnl.total_credit)
expect(response_body[0]['total_credit_value'].to_f).to eq(pnl.total_credit_value)
expect(response_body[0]['total_debit'].to_f).to eq(pnl.total_debit)
expect(response_body[0]['total_debit_value'].to_f).to eq(pnl.total_debit_value)
expect(response_body[0]['average_buy_price'].to_f.round(9)).to eq( (pnl.total_credit_value / (pnl.total_credit)).to_f)
expect(response_body[0]['average_sell_price'].to_f.round(9)).to eq 0
expect(response_body[0]['average_balance_price'].to_f).to eq(0.12)
end
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_get '/api/v2/account/stats/pnl?pnl_currency=usd', token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
end

View File

@@ -0,0 +1,296 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Account::Transactions, type: :request do
describe 'GET /api/v2/account/transactions' do
let(:member) { create(:member, :level_3) }
let(:token) { jwt_for(member) }
let(:btc_account) { member.get_account('btc') }
let(:usd_account) { member.get_account('usd') }
let(:balance) { 100000 }
before do
Ability.stubs(:user_permissions).returns({'member'=>{'read'=>['Deposit', 'Withdraw']}})
end
context 'successful' do
before do
btc_account.plus_funds(balance)
usd_account.plus_funds(balance)
create_list(:deposit_usd, 4, member: member, updated_at: 5.hour.ago)
create_list(:usd_withdraw, 4, member: member, updated_at: 5.hour.ago)
create_list(:deposit_btc, 3, member: member, updated_at: 10.hour.ago)
create_list(:btc_withdraw, 3, member: member, updated_at: 10.hour.ago)
create_list(:deposit_usd, 5, member: member, updated_at: 5.days.ago)
create_list(:usd_withdraw, 5, member: member, updated_at: 5.days.ago)
end
it 'returns all deposits and withdraws num' do
api_get '/api/v2/account/transactions', token: token
result = JSON.parse(response.body)
expect(result.size).to eq 24
expect(response.headers.fetch('Total')).to eq '24'
end
it 'returns limited deposits and withdraws' do
api_get '/api/v2/account/transactions', params: { limit: 2, page: 1 }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 2
expect(response.headers.fetch('Total')).to eq '24'
api_get '/api/v2/account/transactions', params: { limit: 1, page: 2 }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 1
expect(response.headers.fetch('Total')).to eq '24'
end
it 'returns deposits and withdraws for the last two days' do
api_get '/api/v2/account/transactions', params: { time_from: 2.days.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 14
expect(response.headers.fetch('Total')).to eq '14'
end
it 'returns deposits and withdraws before 2 days ago' do
api_get '/api/v2/account/transactions', params: { time_to: 2.days.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 10
expect(response.headers.fetch('Total')).to eq '10'
end
it 'returns newest mixed up withdraws and deposits depending on updated_at' do
api_get '/api/v2/account/transactions', params: { limit: 8, page: 1 }, token: token
result = JSON.parse(response.body)
expect(result.select { |t| t['type'] == 'Withdraw' }.count).to eq 4
expect(result.select { |t| t['type'] == 'Deposit' }.count).to eq 4
end
it 'returns the oldest mixed up withdraws and deposits depending on updated_at' do
api_get '/api/v2/account/transactions', params: { limit: 8, page: 2, time_from: 2.days.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(result.select { |t| t['type'] == 'Withdraw' }.count).to eq 3
expect(result.select { |t| t['type'] == 'Deposit' }.count).to eq 3
end
it 'returns sorted transactions in descending order' do
api_get '/api/v2/account/transactions', token: token
result = JSON.parse(response.body)
update_time = result.pluck('updated_at')
expect(update_time).to eq(update_time.sort { |a, b| b <=> a })
end
it 'returns sorted transactions in ascending order' do
api_get '/api/v2/account/transactions', params: { order_by: 'asc' }, token: token
result = JSON.parse(response.body)
update_time = result.pluck('updated_at')
expect(update_time).to eq(update_time.sort)
end
it 'returns only transactions with BTC currency' do
api_get '/api/v2/account/transactions', params: { currency: 'btc' }, token: token
result = JSON.parse(response.body)
expect(result.count).to eq 6
end
it 'returns only transactions with USD currency' do
api_get '/api/v2/account/transactions', params: { currency: 'USD' }, token: token
result = JSON.parse(response.body)
expect(result.count).to eq 18
end
it 'returns nil in confirmations field for fiat' do
api_get '/api/v2/account/transactions', params: { currency: 'USD' }, token: token
result = JSON.parse(response.body)
expect(result.pluck('confimations').none?).to be_truthy
end
it 'returns valid number in confirmations field for coin' do
api_get '/api/v2/account/transactions', params: { currency: 'btc' }, token: token
result = JSON.parse(response.body)
expect(result.pluck('confimations').any? { |c| c.nil? ? true : c > 1 }).to be_truthy
end
it 'returns transaction with txid filter' do
api_get '/api/v2/account/transactions', params: { txid: Deposits::Coin.first.txid }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 1
expect(result.all? { |d| d['txid'] == Deposits::Coin.first.txid }).to be_truthy
end
context 'state filters' do
before do
create_list(:deposit_usd, 4, member: member, updated_at: 5.hour.ago, aasm_state: 'accepted')
create_list(:deposit_usd, 4, member: member, updated_at: 5.hour.ago, aasm_state: 'rejected')
create_list(:usd_withdraw, 4, member: member, updated_at: 5.days.ago, aasm_state: 'accepted')
create_list(:usd_withdraw, 4, member: member, updated_at: 5.days.ago, aasm_state: 'rejected')
end
it 'returns transactions with more than one deposit state' do
expect(Deposit.count).to eq 20
api_get '/api/v2/account/transactions', params: { deposit_state: ['accepted', 'submitted'] }, token: token
result = JSON.parse(response.body)
expect(result.select { |t| t['type'] == 'Deposit' }.count).to eq 16
expect(result.select { |t| t['type'] == 'Deposit' }.pluck('state').uniq).to match_array(['submitted', 'accepted'])
end
it 'returns transactions with one deposit state' do
expect(Deposit.count).to eq 20
api_get '/api/v2/account/transactions', params: { deposit_state: 'submitted' }, token: token
result = JSON.parse(response.body)
expect(result.select { |t| t['type'] == 'Deposit' }.count).to eq 12
expect(result.select { |t| t['type'] == 'Deposit' }.pluck('state').uniq).to eq (['submitted'])
end
it 'returns transactions with more than one withdraw state' do
expect(Withdraw.count).to eq 20
api_get '/api/v2/account/transactions', params: { withdraw_state: ['accepted', 'prepared'] }, token: token
result = JSON.parse(response.body)
expect(result.select { |t| t['type'] == 'Withdraw' }.count).to eq 16
expect(result.select { |t| t['type'] == 'Withdraw' }.pluck('state').uniq).to match_array(['prepared', 'accepted'])
end
it 'returns transactions with one withdraw state' do
expect(Withdraw.count).to eq 20
api_get '/api/v2/account/transactions', params: { withdraw_state: 'prepared' }, token: token
result = JSON.parse(response.body)
expect(result.select { |t| t['type'] == 'Withdraw' }.count).to eq 12
expect(result.select { |t| t['type'] == 'Withdraw' }.pluck('state').uniq).to eq (['prepared'])
end
it 'returns transactions with one withdraw state and one deposit state' do
expect(Withdraw.count).to eq 20
expect(Deposit.count).to eq 20
api_get '/api/v2/account/transactions', params: { withdraw_state: 'rejected', deposit_state: 'rejected' }, token: token
result = JSON.parse(response.body)
expect(result.select { |t| t['type'] == 'Deposit' }.count).to eq 4
expect(result.select { |t| t['type'] == 'Withdraw' }.count).to eq 4
expect(result.select { |t| t['type'] == 'Deposit' }.pluck('state').uniq).to eq (['rejected'])
expect(result.select { |t| t['type'] == 'Withdraw' }.pluck('state').uniq).to eq (['rejected'])
end
it 'returns transactions with more than one withdraw state and more that one deposit state' do
expect(Withdraw.count).to eq 20
expect(Deposit.count).to eq 20
api_get '/api/v2/account/transactions', params: { withdraw_state: ['rejected', 'accepted'], deposit_state: ['rejected', 'accepted'] }, token: token
result = JSON.parse(response.body)
expect(result.select { |t| t['type'] == 'Deposit' }.count).to eq 8
expect(result.select { |t| t['type'] == 'Withdraw' }.count).to eq 8
expect(result.select { |t| t['type'] == 'Deposit' }.pluck('state').uniq).to match_array(['accepted','rejected'])
expect(result.select { |t| t['type'] == 'Withdraw' }.pluck('state').uniq).to match_array(['accepted','rejected'])
end
end
end
context 'fail' do
before do
btc_account.plus_funds(balance)
usd_account.plus_funds(balance)
end
it 'requires authentication' do
api_get '/api/v2/account/transactions'
expect(response.code).to eq '401'
end
it 'validates currency param' do
api_get '/api/v2/account/transactions', params: { currency: 'bar' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.transactions.currency_doesnt_exist')
end
it 'validates order_by param' do
api_get '/api/v2/account/transactions', params: { order_by: 'foo' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.transactions.order_by_invalid')
end
it 'validates time_from param' do
api_get '/api/v2/account/transactions', params: { time_from: 'btc' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.transactions.non_integer_time_from')
end
it 'validates time_to param' do
api_get '/api/v2/account/transactions', params: { time_to: [] }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.transactions.non_integer_time_to')
end
it 'validates deposit state param' do
api_get '/api/v2/account/transactions', params: { deposit_state: [] }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.transactions.invalid_deposit_state')
end
it 'validates withdraw state param' do
api_get '/api/v2/account/transactions', params: { withdraw_state: [] }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.transactions.invalid_withdraw_state')
end
it 'validates page param' do
api_get '/api/v2/account/transactions', params: { page: -1 }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.transactions.non_positive_page')
api_get '/api/v2/account/transactions', params: { page: 'btc' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.transactions.non_integer_page')
end
it 'validates limit param' do
api_get '/api/v2/account/transactions', params: { limit: 1001 }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.transactions.invalid_limit')
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_get '/api/v2/account/transactions', params: { limit: 1000 }, token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
end
end

View File

@@ -0,0 +1,406 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Account::Withdraws, type: :request do
let(:member) { create(:member, :level_3) }
let(:token) { jwt_for(member) }
let(:level_0_member) { create(:member, :level_0) }
let(:level_0_member_token) { jwt_for(level_0_member) }
before do
Ability.stubs(:user_permissions).returns({'member'=>{'read'=>['Withdraw'],'create'=>['Withdraw']}})
end
describe 'GET /api/v2/account/withdraws' do
let!(:btc_withdraws) { create_list(:btc_withdraw, 20, :with_deposit_liability, member: member, updated_at: 5.days.ago) }
let!(:usd_withdraws) { create_list(:usd_withdraw, 20, :with_deposit_liability, member: member, updated_at: 2.hour.ago) }
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_get '/api/v2/account/withdraws', token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
it 'requires authentication' do
get '/api/v2/account/withdraws'
expect(response.code).to eq '401'
end
it 'validates currency param' do
api_get '/api/v2/account/withdraws', params: { currency: 'FOO' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.currency.doesnt_exist')
end
it 'validates page param' do
api_get '/api/v2/account/withdraws', params: { page: -1 }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.withdraw.non_positive_page')
end
it 'validates limit param' do
api_get '/api/v2/account/withdraws', params: { limit: 9999 }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.withdraw.invalid_limit')
end
it 'validates time_from param' do
api_get '/api/v2/account/withdraws', params: { time_from: 'btc' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.withdraw.non_integer_time_from')
end
it 'validates time_to param' do
api_get '/api/v2/account/withdraws', params: { time_to: [] }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('account.withdraw.non_integer_time_to')
end
it 'returns withdraws for all currencies by default' do
api_get '/api/v2/account/withdraws', params: { limit: 100 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '40'
expect(result.map { |x| x['currency'] }.uniq.sort).to eq %w[ btc usd ]
end
it 'returns withdraws specified currency' do
api_get '/api/v2/account/withdraws', params: { currency: 'btc', limit: 100 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '20'
expect(result.map { |x| x['currency'] }.uniq.sort).to eq %w[ btc ]
end
it 'returns withdraws with txid filter' do
api_get '/api/v2/account/withdraws', params: { rid: btc_withdraws.first.rid }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 1
expect(result.all? { |d| d['rid'] == btc_withdraws.first.rid }).to be_truthy
end
it 'filters withdraws by multiple states' do
create(:usd_withdraw, member: member, aasm_state: :rejected)
api_get '/api/v2/account/withdraws', params: { state: ['canceled', 'rejected'] }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 1
create(:usd_withdraw, member: member, aasm_state: :canceled)
api_get '/api/v2/account/withdraws', params: { state: ['canceled', 'rejected'] }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 2
end
it 'returns withdraws for the last two days' do
api_get '/api/v2/account/withdraws', params: { time_from: 2.days.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 20
expect(response.headers.fetch('Total')).to eq '20'
end
it 'returns withdraws before 2 days ago' do
api_get '/api/v2/account/withdraws', params: { time_to: 2.days.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 20
expect(response.headers.fetch('Total')).to eq '20'
end
it 'paginates withdraws' do
ordered_withdraws = btc_withdraws.sort_by(&:id).reverse
api_get '/api/v2/account/withdraws', params: { currency: 'btc', limit: 10, page: 1 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '20'
expect(result.first['id']).to eq ordered_withdraws[0].id
api_get '/api/v2/account/withdraws', params: { currency: 'btc', limit: 10, page: 2 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '20'
expect(result.first['id']).to eq ordered_withdraws[10].id
end
it 'sorts withdraws' do
ordered_withdraws = btc_withdraws.sort_by(&:id).reverse
api_get '/api/v2/account/withdraws', params: { currency: 'btc', limit: 100 }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.map { |x| x['id'] }).to eq ordered_withdraws.map(&:id)
end
it 'denies access to unverified member' do
api_get '/api/v2/account/withdraws', token: level_0_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('account.withdraw.not_permitted')
end
end
describe 'create withdraw' do
let(:currency) { Currency.visible.sample; Currency.find(:usd) }
let(:amount) { 0.15 }
let(:beneficiary) do
create(:beneficiary, member: member, state: :active, currency: currency)
end
let :data do
{ uid: member.uid,
currency: currency.code,
amount: amount,
beneficiary_id: beneficiary.id,
otp: 123456 }
end
let(:account) { member.get_account(currency) }
let(:balance) { 1.2 }
let(:long_note) { (0...257).map { (65 + rand(26)).chr }.join }
before { account.plus_funds(balance) }
before { Vault::TOTP.stubs(:validate?).returns(true) }
context 'disabled account withdrawal API' do
before { ENV['ENABLE_ACCOUNT_WITHDRAWAL_API'] = 'false' }
after { ENV['ENABLE_ACCOUNT_WITHDRAWAL_API'] = 'true' }
it 'doesn\'t allow account withdrawal API call' do
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.disabled_api')
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
data[:amount] = '0.0000000123456789'
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(201)
expect(Withdraw.last.sum.to_s).to eq data[:amount]
end
end
it 'validates missing params' do
data.except!(:otp, :amount, :currency, :beneficiary_id)
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.missing_otp')
expect(response).to include_api_error('account.withdraw.missing_amount')
expect(response).to include_api_error('account.withdraw.missing_currency')
expect(response).to include_api_error('account.withdraw.missing_beneficiary_id')
end
context 'invalid beneficiary_id' do
context 'non-existing' do
it do
data[:beneficiary_id] = data[:beneficiary_id] + 1
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.beneficiary.doesnt_exist')
end
end
context 'archived' do
before { beneficiary.update(state: :archived) }
it do
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.beneficiary.doesnt_exist')
end
end
context 'pending' do
before { beneficiary.update(state: :pending) }
it do
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.beneficiary.invalid_state_for_withdrawal')
end
end
end
it 'requires beneficiary_id' do
data[:beneficiary_id] = nil
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.empty_beneficiary_id')
end
it 'validates beneficiary_id type' do
data[:beneficiary_id] = 'beneficiary_id'
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.non_integer_beneficiary_id')
end
it 'requires otp' do
data[:otp] = nil
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.empty_otp')
end
it 'validates otp code' do
Vault::TOTP.stubs(:validate?).returns(false)
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.invalid_otp')
end
it 'requires amount' do
data[:amount] = nil
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.non_positive_amount')
end
it 'validates negative amount' do
data[:amount] = -1
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.non_positive_amount')
end
it 'validates enough balance' do
data[:amount] = 100
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.insufficient_balance')
end
it 'validates amount type' do
data[:amount] = 'one'
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.non_decimal_amount')
end
it 'validates amount precision' do
data[:amount] = 0.123456789123456789
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.invalid_amount')
end
it 'requires currency' do
data[:currency] = nil
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.currency.doesnt_exist')
end
it 'disabled currency' do
data[:currency] = :eur
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.currency.doesnt_exist')
end
context 'disabled withdrawal for currency' do
let(:currency) { Currency.find('btc') }
before { currency.update!(withdrawal_enabled: false) }
it 'returns error' do
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('account.currency.withdrawal_disabled')
end
end
it 'creates new withdraw and immediately submits it' do
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status(201)
record = Withdraw.last
expect(record.sum).to eq amount
expect(record.aasm_state).to eq 'accepted'
expect(record.account).to eq account
expect(record.account.balance).to eq(1.2 - amount)
expect(record.account.locked).to eq amount
end
it 'creates new withdraw with note' do
api_post '/api/v2/account/withdraws', params: data.merge(note: 'Test note'), token: token
expect(response).to have_http_status(201)
result = JSON.parse(response.body)
expect(result['note']).to eq 'Test note'
record = Withdraw.last
expect(record.note).to eq 'Test note'
end
it 'doesnt create new withdraw with too long note' do
api_post '/api/v2/account/withdraws', params: data.merge(note: long_note), token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('account.withdraw.too_long_note')
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_post '/api/v2/account/withdraws', params: data, token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
describe 'GET /withdraws/sums' do
let!(:btc_withdraws) { create_list(:btc_withdraw, 2, :with_deposit_liability, member: member) }
let!(:usd_withdraws) { create_list(:usd_withdraw, 2, :with_deposit_liability, member: member) }
before do
btc_withdraws.map(&:accept!)
usd_withdraws.map(&:accept!)
end
it 'returns withdrawals sums' do
api_get '/api/v2/account/withdraws/sums', token: token
expect(response_body.key?('last_24_hours')).to be_truthy
expect(response_body.key?('last_1_month')).to be_truthy
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_get '/api/v2/account/withdraws/sums', token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
end

View File

@@ -0,0 +1,35 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Abilities, type: :request do
describe 'GET /api/v2/admin/abilities' do
context 'member role' do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
it 'get all roles and permissions' do
api_get '/api/v2/admin/abilities', token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result).to eq(
"create" => ["Deposits::Fiat"],
"manage" => ["Operations::Account", "Operations::Asset", "Operations::Expense", "Operations::Liability", "Operations::Revenue", "Member", "Account", "Beneficiary", "PaymentAddress", "Deposit", "Withdraw", "WithdrawLimit", "Blockchain", "Currency", "Engine", "Market", "TradingFee", "Wallet", "Adjustment", "InternalTransfer", "WhitelistedSmartContract"],
"read" => ["Trade", "Order"],
"update" => ["Order"],
)
end
end
context 'member role' do
let(:member) { create(:member, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(member) }
it 'get all roles and permissions' do
api_get '/api/v2/admin/abilities', token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result).to eq({})
end
end
end
end

View File

@@ -0,0 +1,420 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Adjustments, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:member) { create(:member) }
describe 'GET /api/v2/admin/adjustments' do
let!(:adjustments) do
create(:adjustment, currency_id: 'btc')
create(:adjustment, currency_id: 'btc')
create(:adjustment, currency_id: 'btc')
end
let!(:accepted) { create(:adjustment, currency_id: 'btc', receiving_account_number: "BTC-202-#{member.uid}").tap { |a| a.accept!(validator: member) } }
let!(:rejected) { create(:adjustment, currency_id: 'btc', receiving_account_number: "BTC-202-#{member.uid}").tap { |a| a.reject!(validator: member) } }
it 'get all adjustments' do
api_get '/api/v2/admin/adjustments', token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers['Total'].to_i).to eq Adjustment.count
expect(result.length).to eq Adjustment.count
end
context 'with rejected/accepted' do
it 'fetches operations from db' do
api_get '/api/v2/admin/adjustments', token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.length).to eq 5
expect(result[-1].key?('asset')).to be_truthy
expect(result[-1].key?('liability')).to be_truthy
expect(result[-1]['state']).to eq('rejected')
# We don't create operations for rejected adj.
expect(result[-1]['liability']['id'].nil?).to be_truthy
expect(result[-1]['asset']['id'].nil?).to be_truthy
expect(result[-2].key?('asset')).to be_truthy
expect(result[-2].key?('liability')).to be_truthy
expect(result[-2]['liability']['id']).to eq accepted.liability.id
expect(result[-2]['asset']['id']).to eq accepted.asset.id
expect(result[-2]['state']).to eq('accepted')
end
end
context 'with filters' do
let!(:adjustment_with_category) { create(:adjustment, currency_id: 'btc', category: 'balance_anomaly', receiving_account_number: "BTC-202-#{member.uid}") }
let!(:eth_adjustment) { create(:adjustment, currency_id: 'eth', receiving_account_number: "eth-202-#{member.uid}").tap { |a| a.accept!(validator: member) } }
it 'filter by accepted state' do
api_get '/api/v2/admin/adjustments', token: token, params: { state: 'accepted' }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers['Total'].to_i).to eq(Adjustment.where(state: 'accepted').count)
expect(result.first['id']).to eq(accepted.id)
end
it 'filter by rejected state' do
api_get '/api/v2/admin/adjustments', token: token, params: { state: 'rejected' }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers['Total'].to_i).to eq(Adjustment.where(state: 'rejected').count)
expect(result.first['id']).to eq(rejected.id)
end
it 'filters by eth currency' do
api_get '/api/v2/admin/adjustments', token: token, params: { currency: 'eth' }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers['Total'].to_i).to eq(Adjustment.where(currency_id: 'eth').count)
expect(result.first['id']).to eq(eth_adjustment.id)
end
it 'filters by btc currency' do
api_get '/api/v2/admin/adjustments', token: token, params: { currency: 'btc' }
expect(response).to be_successful
expect(response.headers['Total'].to_i).to eq(Adjustment.where(currency_id: 'btc').count)
end
it 'validates currency' do
api_get '/api/v2/admin/adjustments', token: token, params: { currency: 'uah' }
expect(response.status).to eq 422
expect(response).to include_api_error('admin.currency.doesnt_exist')
end
it 'filter by accepted category' do
api_get '/api/v2/admin/adjustments', token: token, params: { category: 'balance_anomaly' }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers['Total'].to_i).to eq(Adjustment.where(category: 'balance_anomaly').count)
expect(result.first['id']).to eq(adjustment_with_category.id)
end
end
end
describe 'GET /api/v2/admin/adjustments/:id' do
let!(:adjustment1) { create(:adjustment, currency_id: 'btc') }
let!(:adjustment2) { create(:adjustment, currency_id: 'eth', receiving_account_number: "ETH-202-#{member.uid}") }
let!(:adjustment3) { create(:adjustment, currency_id: 'eth', receiving_account_number: "ETH-302-#{member.uid}") }
it 'get specified adjustment' do
api_get "/api/v2/admin/adjustments/#{adjustment1.id}", token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['id']).to eq adjustment1.id
expect(result['currency']).to eq adjustment1.currency_id
end
it 'get specified adjustment' do
api_get "/api/v2/admin/adjustments/#{adjustment2.id}", token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['id']).to eq adjustment2.id
expect(result['currency']).to eq adjustment2.currency_id
expect(result['receiving_account_code']).to eq '202'
expect(result['receiving_member_uid']).to eq member.uid
end
it 'get specified adjustment' do
api_get "/api/v2/admin/adjustments/#{adjustment3.id}", token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['id']).to eq adjustment3.id
expect(result['currency']).to eq adjustment3.currency_id
expect(result['receiving_account_code']).to eq '302'
expect(result['receiving_member_uid'].blank?).to be_truthy
end
end
describe 'POST /api/v2/admin/adjustments/new' do
let(:params) do
{
reason: 'Adjustment',
description: 'sample sdjustment',
category: 'asset_registration',
amount: 100.0,
currency_id: :btc,
asset_account_code: 102,
receiving_account_code: 202,
receiving_member_uid: member.uid
}
end
it 'creates new adjustment' do
expect {
api_post '/api/v2/admin/adjustments/new', token: token, params: params
}.to change { Adjustment.count }.by 1
expect(response).to be_successful
end
it 'returns new adjustment and prebuild operations' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params
result = JSON.parse(response.body)
expect(result['reason']).to eq('Adjustment')
expect(result['description']).to eq('sample sdjustment')
expect(result['category']).to eq('asset_registration')
expect(result['amount']).to eq('100.0')
expect(result['currency']).to eq('btc')
expect(result.key?('asset')).to be_truthy
expect(result.key?('liability')).to be_truthy
end
it 'checks account decimal amount' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(amount: '100btc')
expect(response).not_to be_successful
expect(response).to include_api_error('admin.adjustment.non_decimal_amount')
end
it 'checks right asset_account_code' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(asset_account_code: 111)
expect(response).not_to be_successful
expect(response).to include_api_error('admin.adjustment.invalid_asset_account_code')
end
it 'checks right receiving_account_code' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(receiving_account_code: 444)
expect(response).not_to be_successful
expect(response).to include_api_error('admin.adjustment.invalid_receiving_account_code')
end
it 'validates right category' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(category: 'some_category')
expect(response).not_to be_successful
expect(response).to include_api_error('admin.adjustment.invalid_category')
end
it 'validates right currency' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(currency_id: 'uah')
expect(response).not_to be_successful
expect(response).to include_api_error('admin.adjustment.currency_doesnt_exist')
end
it 'validates coin and fiat accounts numbers' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(currency_id: 'btc', asset_account_code: 101)
expect(response).not_to be_successful
expect(response).to include_api_error('Prebuild operations are invalid')
end
context 'receiving_member_uid validatations' do
it 'requires for liability receiving account' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(receiving_account_code: 202).except(:receiving_member_uid)
expect(response).not_to be_successful
expect(response).to include_api_error('admin.adjustment.missing_receiving_member_uid')
end
it 'doesnt requires for revenue receiving account' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(receiving_account_code: 302).except(:receiving_member_uid)
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['reason']).to eq('Adjustment')
end
it 'doesnt requires for expense receiving account' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(receiving_account_code: 402).except(:receiving_member_uid)
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['reason']).to eq('Adjustment')
end
it 'creates adjustment with expense without member_uid' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(receiving_account_code: 402)
expect(response).not_to be_successful
expect(response).to include_api_error('admin.adjustment.redundant_receiving_member_uid')
end
it 'creates adjustment with revenue that contains member uid' do
api_post '/api/v2/admin/adjustments/new', token: token, params: params.merge(receiving_account_code: 302)
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['reason']).to eq('Adjustment')
expect(result['receiving_member_uid'].blank?).to be_truthy
adjustment_db = Adjustment.find(result['id'])
account_number_hash = Operations.split_account_number(account_number: adjustment_db.receiving_account_number)
expect(account_number_hash[:member_uid].present?).to be_truthy
end
end
end
describe 'POST /api/v2/admin/adjustments/action (accept)' do
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-202-#{member.uid}") }
it 'accepts adjustment' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
}.to change { adjustment.reload.state }.to('accepted')
.and change { Operations::Asset.count }.by(1)
.and change { Operations::Liability.count }.by(1)
expect(response).to be_successful
end
it 'udpates member\'s balance' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
}.to change { member.get_account(adjustment.currency).balance }.by(adjustment.amount)
end
it 'does not accept invalid asset_account_code.' do
adjustment.update(asset_account_code: 3000)
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
expect(adjustment.reload.state).to eq('pending')
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.adjustment.cannot_perform_accept_action')
end
it 'does not accept negative adjustment for sum bigger than member\'s balance' do
adjustment.update(amount: -10000000.0)
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
expect(adjustment.reload.state).to eq('pending')
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.adjustment.user_insufficient_balance')
end
it 'does not update member\'s balance if it is lower than negative adjustment' do
adjustment.update(amount: -10000000.0)
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
}.not_to change { member.get_account(adjustment.currency).balance }
end
context 'already accepted' do
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-202-#{member.uid}").tap { |a| a.accept!(validator: member) } }
it 'returns status and error' do
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.adjustment.cannot_perform_accept_action')
end
it 'does not udpate member\'s balance' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
}.not_to change { member.accounts }
end
it 'does not create operations' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
}.not_to change { Operations::Asset.count }
end
end
context 'already rejected' do
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-202-#{member.uid}").tap { |a| a.reject!(validator: member) } }
it 'returns status and error' do
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.adjustment.cannot_perform_accept_action')
end
it 'does not udpate member\'s balance' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
}.not_to change { member.accounts }
end
it 'does not create operations' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
}.not_to change { Operations::Asset.count }
end
end
end
describe 'POST /api/v2/admin/adjustments/action (reject)' do
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-202-#{member.uid}") }
it 'rejects adjustment' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :reject }
}.to change { adjustment.reload.state }.to('rejected')
end
it 'does not create operations' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :reject }
}.not_to change { Operations::Asset.count }
end
context 'already rejected' do
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-202-#{member.uid}").tap { |a| a.reject!(validator: member) } }
it 'returns status and error' do
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :accept }
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.adjustment.cannot_perform_accept_action')
end
it 'does not udpate member\'s balance' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :reject }
}.not_to change { member.accounts }
end
it 'does not create operations' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :reject }
}.not_to change { Operations::Asset.count }
end
end
context 'already accepted' do
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-202-#{member.uid}").tap { |a| a.accept!(validator: member) } }
it 'returns status and error' do
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :reject }
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.adjustment.cannot_perform_reject_action')
end
it 'does not udpate member\'s balance' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :reject }
}.not_to change { member.accounts }
end
it 'does not create operations' do
expect {
api_post '/api/v2/admin/adjustments/action', token: token, params: { id: adjustment.id, action: :reject }
}.not_to change { Operations::Asset.count }
end
end
end
end

View File

@@ -0,0 +1,92 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Beneficiaries, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:member) { create(:member, :level_3) }
let(:member_token) { jwt_for(level_3_member) }
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
let!(:fiat_beneficiary) {
create(:beneficiary, state: :active, currency: Currency.find('usd'))
}
describe 'GET /api/v2/admin/beneficiaries' do
let(:url) { '/api/v2/admin/beneficiaries' }
it 'get all beneficiaries' do
api_get url, token: token
expect(response_body.count).to eq(Beneficiary.count)
end
context 'ordering' do
it 'ascending by id' do
api_get url, token: token, params: { order_by: 'id', ordering: 'asc' }
expect(response_body.first['id']).to eq Beneficiary.first.id
end
it 'descending by id' do
api_get url, token: token, params: { order_by: 'id', ordering: 'desc' }
expect(response_body.first['id']).to eq Beneficiary.last.id
end
end
context 'filtering' do
it 'by member' do
api_get url, token: token, params: { uid: member.uid }
expect(response_body.count).to eq(member.beneficiaries.count)
end
it 'by state' do
api_get url, token: token, params: { state: ['pending', 'archived'] }
expect(response_body.count).to eq(Beneficiary.where(state: ['pending', 'archived']).count)
end
context 'by currency' do
it 'by crypto currency' do
api_get url, token: token, params: { currency: ['eth', 'btc'] }
expect(response_body.count).to eq(Beneficiary.where(currency_id: ['eth', 'btc']).count)
end
it 'by fiat currency' do
api_get url, token: token, params: { currency: 'usd' }
expect(response_body.count).to eq(Beneficiary.where(currency_id: 'usd').count)
expect(response_body.last['data']).to eq Beneficiary.where(currency_id: 'usd').last.data
end
end
end
context 'actions' do
let(:url) { '/api/v2/admin/beneficiaries/actions' }
it 'by archived' do
api_post url, token: token, params: { action: 'archive', id: fiat_beneficiary.id }
expect(response_body['data']).to eq fiat_beneficiary.data.with_indifferent_access
expect(response_body['state']).to eq('archived')
end
end
end
end

View File

@@ -0,0 +1,450 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Blockchains, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
describe 'GET /api/v2/admin/blockchains/:id' do
let(:blockchain) { Blockchain.find_by(key: 'eth-rinkeby') }
it 'returns information about specified blockchain' do
api_get "/api/v2/admin/blockchains/#{blockchain.id}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.fetch('id')).to eq blockchain.id
expect(result.fetch('name')).to eq blockchain.name
end
it 'returns error in case of invalid id' do
api_get "/api/v2/admin/blockchains/#{Blockchain.last.id + 42}", token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'return error in case of not permitted ability' do
api_get "/api/v2/admin/blockchains/#{blockchain.id}", token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'GET /api/v2/admin/blockchains/clients' do
it 'get list of all available clients' do
api_get '/api/v2/admin/blockchains/clients', token: token
expect(JSON.parse(response.body)).to match_array Blockchain.clients.map &:to_s
end
end
describe 'GET /api/v2/admin/blockchains/:id/latest_block' do
let(:blockchain) { Blockchain.find_by(key: "eth-rinkeby") }
it 'returns error in case of invalid id' do
api_get "/api/v2/admin/blockchains/#{Blockchain.last.id + 42}/latest_block", token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.blockchain.latest_block')
end
it 'returns error in case of node inaccessibility' do
api_get "/api/v2/admin/blockchains/#{blockchain.id}/latest_block", token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.blockchain.latest_block')
end
context 'get latest_block' do
let(:blockchain) { Blockchain.find_by(key: "eth-rinkeby") }
around do |example|
WebMock.disable_net_connect!
example.run
WebMock.allow_net_connect!
end
let(:eth_blockchain) do
Ethereum::Blockchain.new.tap { |b| b.configure(server: 'http://127.0.0.1:8545') }
end
it 'returns node latest block' do
block_number = '0x16b916'
stub_request(:post, 'http://127.0.0.1:8545')
.with(body: { jsonrpc: '2.0',
id: 1,
method: :eth_blockNumber,
params: [] }.to_json)
.to_return(body: { result: block_number,
error: nil,
id: 1 }.to_json)
api_get "/api/v2/admin/blockchains/#{blockchain.id}/latest_block", token: token
expect(response.code).to eq '200'
expect(response_body).to eq 1489174
end
end
end
describe 'GET /api/v2/admin/blockchains' do
it 'lists of blockchains' do
api_get '/api/v2/admin/blockchains', token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq 3
end
it 'returns blockchains by ascending order' do
api_get '/api/v2/admin/blockchains', params: { ordering: 'asc', order_by: 'client'}, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.first['client']).to eq 'bitcoin'
end
it 'returns paginated blockchains' do
api_get '/api/v2/admin/blockchains', params: { limit: 2, page: 1 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '3'
expect(result.size).to eq 2
expect(result.first['key']).to eq 'eth-kovan'
api_get '/api/v2/admin/blockchains', params: { limit: 1, page: 2 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '3'
expect(result.size).to eq 1
expect(result.first['key']).to eq 'eth-rinkeby'
end
it 'returns blockchains filtered by key' do
api_get '/api/v2/admin/blockchains', params: { key: "eth-kovan" }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '1'
expect(result.size).to eq 1
expect(result.first['key']).to eq 'eth-kovan'
end
it 'returns error in case invalid blockchain key' do
api_get '/api/v2/admin/blockchains', params: { key: "inv" }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.blockchain.blockchain_key_doesnt_exist')
end
it 'returns blockchains filtered by client' do
api_get '/api/v2/admin/blockchains', params: { client: "parity" }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '1'
expect(result.size).to eq 1
expect(result.first['name']).to eq 'Ethereum Kovan'
end
it 'returns error in case invalid blockchain client' do
api_get '/api/v2/admin/blockchains', params: { client: "inv" }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.blockchain.blockchain_client_doesnt_exist')
end
it 'returns blockchains filtered by status' do
api_get '/api/v2/admin/blockchains', params: { status: "active" }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '3'
expect(result.size).to eq 3
expect(result.map { |r| r["status"]}).to all eq "active"
end
it 'returns error in case invalid blockchain status' do
api_get '/api/v2/admin/blockchains', params: { status: "inv" }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.blockchain.blockchain_status_doesnt_exist')
end
it 'returns blockchains filtered by name' do
api_get '/api/v2/admin/blockchains', params: { name: "Ethereum Kovan" }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '1'
expect(result.size).to eq 1
expect(result.first['name']).to eq 'Ethereum Kovan'
end
it 'returns error in case invalid blockchain name' do
api_get '/api/v2/admin/blockchains', params: { name: "inv" }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.blockchain.blockchain_name_doesnt_exist')
end
it 'return error in case of not permitted ability' do
api_get "/api/v2/admin/blockchains", token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/blockchains/new' do
it 'creates new blockchain' do
api_post '/api/v2/admin/blockchains/new', token: token, params: { key: 'test-blockchain', name: 'Test', client: 'geth',server: 'http://127.0.0.1', height: 123333, explorer_transaction: 'test', explorer_address: 'test'}
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['key']).to eq 'test-blockchain'
end
it 'long blockchain key' do
api_post '/api/v2/admin/blockchains/new', token: token, params: { key: Faker::String.random(1024), name: 'Test', client: 'geth',server: 'http://127.0.0.1', height: 123333, explorer_transaction: 'test', explorer_address: 'test'}
expect(response).not_to be_successful
expect(response).to include_api_error('admin.blockchain.key_too_long')
end
it 'long blockchain name' do
api_post '/api/v2/admin/blockchains/new', token: token, params: { key: Faker::String.random(24), name: Faker::String.random(1024), client: 'geth',server: 'http://127.0.0.1', height: 123333, explorer_transaction: 'test', explorer_address: 'test'}
expect(response).not_to be_successful
expect(response).to include_api_error('admin.blockchain.name_too_long')
end
it 'validate height param' do
api_post '/api/v2/admin/blockchains/new', token: token, params: { key: 'test-blockchain', name: 'Test', client: 'geth',server: 'http://127.0.0.1', height: -123333, explorer_transaction: 'test', explorer_address: 'test', status: 'active', min_confirmations: 6, step: 2 }
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.blockchain.non_positive_height')
end
it 'validate min_confirmations param' do
api_post '/api/v2/admin/blockchains/new', token: token, params: { key: 'test-blockchain', name: 'Test', client: 'geth',server: 'http://127.0.0.1', height: 123333, explorer_transaction: 'test', explorer_address: 'test', status: 'active', min_confirmations: -6, step: 2 }
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.blockchain.non_positive_min_confirmations')
end
it 'validate status param' do
api_post '/api/v2/admin/blockchains/new', token: token, params: { key: 'test-blockchain', name: 'Test', client: 'geth',server: 'http://127.0.0.1', height: 123333, explorer_transaction: 'test', explorer_address: 'test', status: 'actived', min_confirmations: 6, step: 2 }
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.blockchain.invalid_status')
end
it 'validate client param' do
api_post '/api/v2/admin/blockchains/new', token: token, params: { key: 'test-blockchain', name: 'Test', client: 'gezz',server: 'http://127.0.0.1', height: 123333, explorer_transaction: 'test', explorer_address: 'test', status: 'active', min_confirmations: 6, step: 2 }
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.blockchain.invalid_client')
end
it 'checked required params' do
api_post '/api/v2/admin/blockchains/new', token: token, params: { }
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.blockchain.missing_key')
expect(response).to include_api_error('admin.blockchain.missing_name')
expect(response).to include_api_error('admin.blockchain.missing_client')
expect(response).to include_api_error('admin.blockchain.missing_height')
end
it 'validates server' do
api_post '/api/v2/admin/blockchains/new', token: token, params: { key: 'test-blockchain', name: 'Test', client: 'geth',server: 'not_a_url', height: 123333, explorer_transaction: 'test', explorer_address: 'test'}
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.blockchain.invalid_server')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/blockchains/new', token: level_3_member_token, params: { key: 'test-blockchain', name: 'Test', client: 'geth', server: 'http://127.0.0.1', height: 123333, explorer_transaction: 'test', explorer_address: 'test', status: 'active', min_confirmations: 6, step: 2 }
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'key already exists' do
api_post '/api/v2/admin/blockchains/new', token: token, params: { key: Blockchain.first.key, name: 'Test', client: 'geth',server: 'http://127.0.0.1', height: 123333, explorer_transaction: 'test', explorer_address: 'test'}
expect(response.status).to eq 422
end
end
describe 'POST /api/v2/admin/blockchains/update' do
context 'permissions' do
let(:support) { create(:member, :admin, :level_3, role: :support, email: 'example@gmail.com', uid: 'ID73BF61C8H1') }
let(:support_token) { jwt_for(support) }
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/blockchains/update', params: { key: 'test-blockchain', id: Blockchain.first.id }, token: support_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'returns updated blockchain' do
api_post '/api/v2/admin/blockchains/update', params: { name: 'Test Blockchain', id: Blockchain.first.id }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['name']).to eq 'Test Blockchain'
end
end
it 'returns updated blockchain' do
api_post '/api/v2/admin/blockchains/update', params: { key: 'test-blockchain', id: Blockchain.first.id }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['key']).to eq 'test-blockchain'
end
it 'returns updated blockchain' do
api_post '/api/v2/admin/blockchains/update', token: token, params: { key: 'Test-blockchain ', id: Blockchain.first.id }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['key']).to eq 'test-blockchain'
end
it 'long blockchain key' do
api_post '/api/v2/admin/blockchains/update', token: token, params: { key: Faker::String.random(1024) }
expect(response).not_to be_successful
expect(response).to include_api_error('admin.blockchain.key_too_long')
end
it 'long blockchain name' do
api_post '/api/v2/admin/blockchains/update', token: token, params: { name: Faker::String.random(1024) }
expect(response).not_to be_successful
expect(response).to include_api_error('admin.blockchain.name_too_long')
end
it 'validate height param' do
api_post '/api/v2/admin/blockchains/update', token: token, params: { height: -123333 }
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.blockchain.non_positive_height')
end
it 'checked required params' do
api_post '/api/v2/admin/blockchains/update', token: level_3_member_token, params: { key: 'test-blockchain'}
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.blockchain.missing_id')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/blockchains/update', token: level_3_member_token, params: { id: Blockchain.first.id }
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/blockchains/process_block' do
context 'returns error' do
it 'in case of not permitted ability' do
api_post '/api/v2/admin/blockchains/process_block', token: level_3_member_token, params: { block_number: 1, id: Blockchain.last.id }
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'when blockchain doesnt exist' do
api_post "/api/v2/admin/blockchains/process_block", params: { block_number: 1, id: Blockchain.last.id + 1 }, token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'when blockchain is not accessible' do
api_post "/api/v2/admin/blockchains/process_block", params: { block_number: 1, id: Blockchain.last.id }, token: token
expect(response).to include_api_error('admin.blockchain.process_block')
end
end
context 'successful' do
let!(:blockchain) { Blockchain.find_by(key: 'btc-testnet') }
let(:service) { BlockchainService.new(blockchain) }
let!(:currency) { create(:currency, :btc, id: 'fake') }
let(:block_number) { 3 }
let!(:member) { create(:member) }
let!(:fake_blockchain) { create(:blockchain, 'fake-testnet') }
let!(:wallet) { create(:wallet, :fake_deposit) }
before do
Blockchain.any_instance.stubs(:blockchain_api).returns(service)
service.stubs(:latest_block_number).returns(4)
clear_redis
PaymentAddress.create!(member: member,
wallet: wallet,
address: 'fake_address')
end
context 'deposit' do
let(:transaction) { Peatio::Transaction.new(hash: 'fake_txid', from_addresses: ['fake_address'], to_address: 'fake_address', amount: 5, block_number: block_number, currency_id: 'fake', txout: 4, status: 'success') }
let(:expected_block) { Peatio::Block.new(block_number, [transaction]) }
before do
service.adapter.stubs(:fetch_block!).returns(expected_block)
end
it 'detects in the block' do
expect(Deposits::Coin.where(currency: currency).exists?).to be false
api_post '/api/v2/admin/blockchains/process_block', token: token, params: { block_number: block_number, id: blockchain.id }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(Deposits::Coin.where(currency: currency).exists?).to be true
end
it 'doesn\'t update height of blockchain' do
blockchain_height = blockchain.height
expect(blockchain_height).not_to eq (block_number)
api_post '/api/v2/admin/blockchains/process_block', token: token, params: { block_number: block_number, id: blockchain.id }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['height']).not_to eq block_number
expect(result['height']).to eq blockchain_height
end
end
context 'withdraw' do
let!(:member_account) { member.get_account(:fake).tap { |ac| ac.update!(balance: 50, locked: 10) } }
let!(:withdrawal) do
Withdraw.create!(member: member,
currency: currency,
amount: 1,
txid: "fake_hash",
rid: 'fake_address',
sum: 1,
type: Withdraws::Coin,
aasm_state: :confirming)
end
let!(:transaction) do
Peatio::Transaction.new(hash: 'fake_hash', to_address: 'fake_address', amount: 1, block_number: block_number, currency_id: currency.id, txout: 10, status: 'pending')
end
let!(:succeed_transaction) do
Peatio::Transaction.new(hash: 'fake_hash', to_address: 'fake_address', from_addresses: ['fake_address'], amount: 1, block_number: block_number, currency_id: currency.id, txout: 10, status: 'success')
end
let(:expected_block) { Peatio::Block.new(block_number, [transaction]) }
before do
service.adapter.stubs(:fetch_block!).returns(expected_block)
service.adapter.stubs(:fetch_transaction).with(transaction).returns(succeed_transaction)
end
it 'detects successfuly in the block' do
expect(Withdraws::Coin.find_by(currency: currency, txid: transaction.hash).succeed?).to be false
api_post '/api/v2/admin/blockchains/process_block', token: token, params: { block_number: block_number, id: blockchain.id }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(Withdraws::Coin.find_by(currency: currency, txid: transaction.hash).succeed?).to be true
end
end
end
end
end

View File

@@ -0,0 +1,467 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Currencies, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
describe 'GET /api/v2/admin/currencies/:code' do
let(:fiat) { Currency.find(:usd) }
let(:coin) { Currency.find(:btc) }
let(:expected_for_fiat) do
%w[code type deposit_fee withdraw_fee withdraw_limit_24h withdraw_limit_72h min_collection_amount base_factor precision position]
end
let(:expected_for_coin) do
expected_for_fiat.concat(%w[blockchain_key base_factor precision subunits options])
end
it 'returns information about specified currency' do
api_get "/api/v2/admin/currencies/#{coin.code}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.fetch('code')).to eq coin.code
end
it 'returns correct keys for fiat' do
api_get "/api/v2/admin/currencies/#{fiat.code}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expected_for_fiat.each { |key| expect(result).to have_key key }
(expected_for_coin - expected_for_fiat).each do |key|
expect(result).not_to have_key key
end
end
context 'currency code with dot' do
let!(:currency) { create(:currency, :xagm_cx) }
it 'returns information about specified currency' do
api_get "/api/v2/admin/currencies/#{currency.code}", token: token
result = JSON.parse(response.body)
expect(result.fetch('code')).to eq currency.code
end
end
it 'returns correct keys for coin' do
api_get "/api/v2/admin/currencies/#{coin.code}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expected_for_coin.each { |key| expect(result).to have_key key }
end
it 'returns ordered by position currencies' do
api_get "/api/v2/admin/currencies/", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('position')).to eq Currency.ordered.pluck(:position)
end
it 'returns error in case of invalid code' do
api_get '/api/v2/admin/currencies/invalid', token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.doesnt_exist')
end
it 'return error in case of not permitted ability' do
api_get "/api/v2/admin/currencies/#{coin.code}", token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'GET /api/v2/admin/currencies' do
it 'list of currencies' do
api_get '/api/v2/admin/currencies', token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.count
end
it 'list of coins' do
api_get '/api/v2/admin/currencies', params: { type: 'coin' }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.coins.size
end
it 'list of fiats' do
api_get '/api/v2/admin/currencies', params: { type: 'fiat' }, token: token
expect(response).to be_successful
result = JSON.parse(response.body, symbolize_names: true)
expect(result.size).to eq Currency.fiats.size
expect(result.dig(0, :code)).to eq 'usd'
end
it 'list of deposit enabled currencies' do
api_get '/api/v2/admin/currencies', params: { deposit_enabled: true }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.deposit_enabled.count
end
it 'list of deposit disabled currencies' do
api_get '/api/v2/admin/currencies', params: { deposit_enabled: false }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.where(deposit_enabled: false).count
end
it 'returns error in case of invalid deposit_enabled type' do
api_get '/api/v2/admin/currencies', params: { deposit_enabled: 'invalid' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_boolean_deposit_enabled')
end
it 'list of withdrawal enabled currencies' do
api_get '/api/v2/admin/currencies', params: { withdrawal_enabled: true }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.withdrawal_enabled.count
end
it 'list of withdrawal disabled currencies' do
api_get '/api/v2/admin/currencies', params: { withdrawal_enabled: false }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.where(withdrawal_enabled: false).count
end
it 'returns error in case of invalid withdrawal_enabled type' do
api_get '/api/v2/admin/currencies', params: { withdrawal_enabled: 'invalid' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_boolean_withdrawal_enabled')
end
it 'list of visible currencies' do
api_get '/api/v2/admin/currencies', params: { visible: true }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.visible.count
end
it 'list of not visible currencies' do
api_get '/api/v2/admin/currencies', params: { visible: false }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.where(visible: false).count
end
it 'returns error in case of invalid visible type' do
api_get '/api/v2/admin/currencies', params: { visible: 'invalid' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_boolean_visible')
end
it 'list of visible coins' do
api_get '/api/v2/admin/currencies', params: { visible: true, type: 'coin' }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.coins.select { |c| c['visible'] == true }.count
end
it 'list of not visible coins' do
api_get '/api/v2/admin/currencies', params: { visible: false, type: 'coin' }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.coins.where(visible: false).count
end
it 'returns error in case of invalid type' do
api_get '/api/v2/admin/currencies', params: { type: 'invalid' }, token: token
expect(response).to have_http_status 422
end
it 'returns currencies by ascending order' do
api_get '/api/v2/admin/currencies', params: { ordering: 'asc', order_by: 'code'}, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.first['code']).to eq 'btc'
end
it 'returns paginated currencies' do
api_get '/api/v2/admin/currencies', params: { limit: 3, page: 1 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '6'
expect(result.size).to eq 3
expect(result.first['code']).to eq 'usd'
api_get '/api/v2/admin/currencies', params: { limit: 3, page: 2 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '6'
expect(result.size).to eq 3
expect(result.first['code']).to eq 'eth'
end
it 'return error in case of not permitted ability' do
api_get '/api/v2/admin/currencies', token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/currencies/new' do
it 'create coin' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', blockchain_key: 'btc-testnet' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['type']).to eq 'coin'
end
it 'create token' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', blockchain_key: 'btc-testnet', parent_id: 'btc' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['type']).to eq 'coin'
expect(result['parent_id']).to eq 'btc'
end
it 'create fiat' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', type: 'fiat' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['type']).to eq 'fiat'
end
it 'validate blockchain_key param' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', blockchain_key: 'test-blockchain' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.blockchain_key_doesnt_exist')
end
it 'validate type param' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', blockchain_key: 'test-blockchain' , type: 'test'}, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.invalid_type')
end
it 'validate visible param' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', type: 'fiat', visible: '123'}, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_boolean_visible')
end
it 'validate parent_id param' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', type: 'coin', parent_id: 'trst'}, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.parent_id_doesnt_exist')
end
it 'validate deposit_enabled param' do
api_post '/api/v2/admin/currencies/new', params: { code: Currency.first.id, deposit_enabled: '123' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_boolean_deposit_enabled')
end
it 'validate withdrawal_enabled param' do
api_post '/api/v2/admin/currencies/new', params: { code: Currency.first.id, withdrawal_enabled: '123' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_boolean_withdrawal_enabled')
end
it 'validate options param' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', type: 'fiat', options: 'test'}, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_json_options')
end
it 'verifies subunits >= 0' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', blockchain_key: 'btc-testnet', subunits: -1 }, token: token
expect(response).to include_api_error 'admin.currency.invalid_subunits'
expect(response).not_to be_successful
end
it 'verifies subunits <= 18' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', blockchain_key: 'btc-testnet', subunits: 19 }, token: token
expect(response).to include_api_error 'admin.currency.invalid_subunits'
expect(response).not_to be_successful
end
it 'creates 1_000_000_000_000_000_000 base_factor' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', blockchain_key: 'btc-testnet', subunits: 18 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['base_factor']).to eq 1_000_000_000_000_000_000
expect(result['subunits']).to eq 18
end
it 'return error while putting base_factor and subunit params' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', blockchain_key: 'btc-testnet', subunits: 18, base_factor: 1 }, token: token
result = JSON.parse(response.body)
expect(response.code).to eq '422'
expect(result['errors']).to eq(['admin.currency.one_of_base_factor_subunits_fields'])
end
it 'creates currency with 1000 base_factor' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', blockchain_key: 'btc-testnet', base_factor: 1000 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['base_factor']).to eq 1000
expect(result['subunits']).to eq 3
end
it 'checked required params' do
api_post '/api/v2/admin/currencies/new', params: { }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.missing_code')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/currencies/new', params: { code: 'test', blockchain_key: 'btc-testnet' }, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/currencies/update' do
context 'permissions' do
let(:support) { create(:member, :admin, :level_3, role: :support, email: 'example@gmail.com', uid: 'ID73BF61C8H1') }
let(:support_token) { jwt_for(support) }
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.find_by(type: 'fiat').code, precision: 1 }, token: support_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'updates fiat' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.find_by(type: 'fiat').code, name: 'Test' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['name']).to eq 'Test'
end
end
it 'update fiat' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.find_by(type: 'fiat').code, min_collection_amount: 1.2 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['min_collection_amount']).to eq '1.2'
end
it 'update coin' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.find_by(type: 'coin').code, min_collection_amount: 1.2 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['min_collection_amount']).to eq '1.2'
end
it 'validate blockchain_key param' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.find_by(type: 'coin').code, blockchain_key: 'test' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.blockchain_key_doesnt_exist')
end
it 'validate parent_id param' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.find_by(type: 'coin').code, parent_id: 'trst' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.parent_id_doesnt_exist')
end
it 'validate position param' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.find_by(type: 'coin').code, position: 0 }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.invalid_position')
end
it 'validate visible param' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.first.id, visible: '123' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_boolean_visible')
end
it 'validate deposit_enabled param' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.first.id, deposit_enabled: '123' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_boolean_deposit_enabled')
end
it 'validate withdrawal_enabled param' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.first.id, withdrawal_enabled: '123' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_boolean_withdrawal_enabled')
end
it 'validates negative precision' do
expect {
api_post '/api/v2/admin/currencies/update', params: { code: Currency.first.id, precision: -1 }, token: token
}.not_to change { Currency.first }
expect(response).not_to be_successful
expect(response.status).to eq 422
end
it 'validate options param' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.first.id, options: 'test' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.non_json_options')
end
it 'checked required params' do
api_post '/api/v2/admin/currencies/update', params: { }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.currency.missing_code')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/currencies/update', params: { code: Currency.first.id }, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
end

View File

@@ -0,0 +1,293 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Deposits, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
let!(:fiat_deposits) do
[
create(:deposit_usd, amount: 10.0),
create(:deposit_usd, amount: 9.0),
create(:deposit_usd, amount: 100.0, member: level_3_member),
]
end
let!(:coin_deposits) do
[
create(:deposit_btc, amount: 102.0),
create(:deposit_btc, amount: 11.0, member: level_3_member),
create(:deposit_btc, amount: 12.0, member: level_3_member),
]
end
describe 'GET /api/v2/admin/deposits' do
let(:url) { '/api/v2/admin/deposits' }
it 'get all deposits' do
api_get url, token: token
actual = JSON.parse(response.body)
expected = coin_deposits + fiat_deposits
expect(actual.length).to eq expected.length
expect(actual.map { |a| a['state'] }).to match_array expected.map(&:aasm_state)
expect(actual.map { |a| a['id'] }).to match_array expected.map(&:id)
expect(actual.map { |a| a['currency'] }).to match_array expected.map(&:currency_id)
expect(actual.map { |a| a['member'] }).to match_array expected.map(&:member_id)
expect(actual.map { |a| a['type'] }).to match_array(expected.map { |d| d.currency.coin? ? 'coin' : 'fiat' })
expect(actual.map { |a| a['uid'] }).to match_array(expected.map { |d| d.member.uid })
expect(actual.map { |a| a['email'] }).to match_array(expected.map { |d| d.member.email })
end
context 'ordering' do
it 'ascending by id' do
api_get url, token: token, params: { order_by: 'id', ordering: 'asc' }
actual = JSON.parse(response.body)
expected = (coin_deposits + fiat_deposits).sort { |a, b| a.id <=> b.id }
expect(actual.map { |a| a['id'] }).to eq expected.map(&:id)
end
it 'descending by amount' do
api_get url, token: token, params: { order_by: 'amount', ordering: 'desc' }
actual = JSON.parse(response.body)
expected = (coin_deposits + fiat_deposits).sort { |a, b| b.amount <=> a.amount }
expect(actual.map { |a| a['id'] }).to eq expected.map(&:id)
end
it 'ordering by unexisting field' do
api_get url, token: token, params: { order_by: 'cutiness', ordering: 'desc' }
actual = JSON.parse(response.body)
expected = coin_deposits + fiat_deposits
expect(actual.map { |a| a['id'] }).to match_array expected.map(&:id)
end
end
context 'filtering' do
it 'by member' do
api_get url, token: token, params: { uid: level_3_member.uid }
actual = JSON.parse(response.body)
expected = (coin_deposits + fiat_deposits).select { |d| d.member_id == level_3_member.id }
expect(actual.length).to eq expected.length
expect(actual.map { |a| a['state'] }).to match_array expected.map(&:aasm_state)
expect(actual.map { |a| a['id'] }).to match_array expected.map(&:id)
expect(actual.map { |a| a['currency'] }).to match_array expected.map(&:currency_id)
expect(actual.map { |a| a['member'] }).to all eq level_3_member.id
expect(actual.map { |a| a['type'] }).to match_array(expected.map { |d| d.currency.coin? ? 'coin' : 'fiat' })
expect(actual.map { |a| a['uid'] }).to match_array(expected.map { |d| d.member.uid })
expect(actual.map { |a| a['email'] }).to match_array(expected.map { |d| d.member.email })
end
it 'by type' do
api_get url, token: token, params: { type: 'coin' }
actual = JSON.parse(response.body)
expected = coin_deposits
expect(actual.length).to eq expected.length
expect(actual.map { |a| a['state'] }).to match_array expected.map(&:aasm_state)
expect(actual.map { |a| a['id'] }).to match_array expected.map(&:id)
expect(actual.map { |a| a['currency'] }).to match_array expected.map(&:currency_id)
expect(actual.map { |a| a['member'] }).to match_array expected.map(&:member_id)
expect(actual.map { |a| a['type'] }).to all eq 'coin'
end
it 'by email' do
api_get url, token: token, params: { email: level_3_member.email }
expected = (coin_deposits + fiat_deposits).select { |d| d.member.email == level_3_member.email }
expect(response_body.length).to eq expected.length
expect(response_body.map { |a| a['state'] }).to match_array expected.map(&:aasm_state)
expect(response_body.map { |a| a['id'] }).to match_array expected.map(&:id)
expect(response_body.map { |a| a['currency'] }).to match_array expected.map(&:currency_id)
expect(response_body.map { |a| a['member'] }).to all eq level_3_member.id
expect(response_body.map { |a| a['type'] }).to match_array(expected.map { |d| d.currency.coin? ? 'coin' : 'fiat' })
expect(response_body.map { |a| a['uid'] }).to match_array(expected.map { |d| d.member.uid })
expect(response_body.map { |a| a['email'] }).to match_array(expected.map { |d| d.member.email })
end
end
end
describe 'POST /api/v2/admin/deposits/actions' do
let(:url) { '/api/v2/admin/deposits/actions' }
let(:fiat) { fiat_deposits.first }
let!(:coin) { create(:deposit, :deposit_trst, aasm_state: :accepted) }
context 'validates params' do
it 'does not pass unsupported action' do
api_post url, token: token, params: { action: 'illegal', id: fiat.id }
expect(response.status).to eq 422
expect(response).to include_api_error('admin.deposit.invalid_action')
end
it 'passes supported action for fiat' do
api_post url, token: token, params: { action: 'reject', id: fiat.id }
expect(response).not_to include_api_error('admin.deposit.invalid_action')
end
it 'does not pass coin action for fiat' do
api_post url, token: token, params: { action: 'collect', id: fiat.id }
expect(response.status).to eq 422
expect(response).to include_api_error('admin.deposit.invalid_action')
end
end
context 'updates deposit' do
let!(:coin) { create(:deposit, :deposit_trst) }
it 'accept fiat' do
api_post url, token: token, params: { action: 'accept', id: fiat.id }
expect(fiat.reload.aasm_state).to eq('accepted')
expect(response).to be_successful
end
it 'accept coin' do
api_post url, token: token, params: { action: 'accept', id: coin.id }
expect(coin.reload.aasm_state).to eq('accepted')
expect(response).to be_successful
end
it 'reject fiat' do
api_post url, token: token, params: { action: 'reject', id: fiat.id }
expect(response).to be_successful
expect(fiat.reload.aasm_state).to eq('rejected')
end
end
context 'action :process' do
it 'sends event to deposit_collection daemon' do
api_post url, token: token, params: { action: 'process', id: coin.id }
expect(response).to be_successful
expect(Deposit.find(response_body['id']).processing?).to be_truthy
end
it 'sends event to deposit_collection daemon' do
api_post url, token: token, params: { action: 'fee_process', fees: true, id: coin.id }
expect(response).to be_successful
expect(Deposit.find(response_body['id']).fee_processing?).to be_truthy
end
end
end
describe 'POST /api/v2/admin/deposits/new' do
let(:url) { '/api/v2/admin/deposits/new' }
let(:fiat) { Currency.find(:usd) }
let(:coin) { Currency.find(:btc) }
context 'validates params' do
it 'returns error when user doesnt exist' do
api_post url, token: token, params: { uid: SecureRandom.uuid, currency: fiat.code, amount: 12.2 }
expect(response.status).to eq 422
expect(response).to include_api_error('admin.deposit.user_doesnt_exist')
end
it 'returns error when currency doesnt exist' do
api_post url, token: token, params: { uid: admin.uid, currency: coin.code, amount: 12.2 }
expect(response.status).to eq 422
expect(response).to include_api_error('admin.deposit.currency_doesnt_exist')
end
it 'returns error when amount is not decimal' do
api_post url, token: token, params: { uid: admin.uid, currency: fiat.code, amount: 'amount' }
expect(response.status).to eq 422
expect(response).to include_api_error('admin.deposit.non_decimal_amount')
end
end
it 'creates fiat deposit' do
api_post url, token: token, params: { uid: admin.uid, currency: fiat.code, amount: '13.4' }
result = JSON.parse(response.body)
expect(response.status).to eq 201
expect(result['currency']).to eq fiat.id
expect(result['member']).to eq admin.id
expect(result['uid']).to eq admin.uid
expect(result['email']).to eq admin.email
expect(result['amount']).to eq '13.4'
expect(result['type']).to eq 'fiat'
expect(result['state']).to eq 'submitted'
expect(result['transfer_type']).to eq 'fiat'
end
it 'return error in case of not permitted ability' do
api_post url, token: level_3_member_token, params: { uid: admin.uid, currency: fiat.code, amount: 12.1 }
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/deposit_address' do
let(:url) { '/api/v2/admin/deposit_address' }
context 'failed' do
let(:currency) { :eth }
it 'validates currency with address_format param' do
api_post url, params: { currency: 'abc', uid: '' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.deposit.user_doesnt_exist')
end
it 'validates currency' do
api_post url, params: { currency: 'dildocoin', uid: level_3_member.uid }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.deposit.currency_doesnt_exist')
end
it 'validates currency address format' do
api_post url, params: { currency: 'eth', uid: level_3_member.uid, address_format: 'cash' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.deposit.doesnt_support_cash_address_format')
end
end
context 'successful' do
context 'eth address' do
let(:currency) { :eth }
let(:wallet) { Wallet.deposit_wallet(currency) }
before { level_3_member.payment_address(wallet.id).update!(address: '2N2wNXrdo4oEngp498XGnGCbru29MycHogR') }
it 'expose data about eth address' do
api_post url, params: { currency: currency, uid: level_3_member.uid}, token: token
expect(response.body).to eq '{"currencies":["eth"],"address":"2n2wnxrdo4oengp498xgngcbru29mychogr","state":"active"}'
end
it 'pending user address state' do
level_3_member.payment_address(wallet.id).update!(address: nil)
api_post url, params: { currency: currency, uid: level_3_member.uid}, token: token
expect(response.body).to eq '{"currencies":["eth"],"address":null,"state":"pending"}'
end
end
end
context 'disabled deposit for currency' do
let(:currency) { :btc }
before { Currency.find(currency).update!(deposit_enabled: false) }
it 'returns error' do
api_post url, params: { currency: currency, uid: level_3_member.uid}, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.deposit.deposit_disabled')
end
end
end
end

View File

@@ -0,0 +1,150 @@
# frozen_string_literal: true
describe API::V2::Admin::Engines, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
describe 'GET /api/v2/admin/engines/:id' do
let(:engine) { Engine.first }
it 'returns information about specified engine' do
api_get "/api/v2/admin/engines/#{engine.id}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.fetch('id')).to eq engine.id
expect(result.fetch('name')).to eq engine.name
expect(result.fetch('driver')).to eq engine.driver
expect(result.fetch('state')).to eq engine.state
end
it 'returns error in case of invalid id' do
api_get "/api/v2/admin/engines/#{Engine.last.id + 1}", token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'return error in case of not permitted ability' do
api_get "/api/v2/admin/engines/#{engine.id}", token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'GET /api/v2/admin/engines' do
it 'lists of engines' do
api_get '/api/v2/admin/engines', token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq 2
end
it 'returns engines by ascending order' do
api_get '/api/v2/admin/engines', params: { ordering: 'asc' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.first['id']).to eq Engine.first.id
end
it 'returns paginated engines' do
api_get '/api/v2/admin/engines', params: { limit: 1, page: 1 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '2'
expect(result.size).to eq 1
api_get '/api/v2/admin/engines', params: { limit: 1, page: 2 }, token: token
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
it 'return error in case of not permitted ability' do
api_get '/api/v2/admin/engines', token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/engines/new' do
let(:engine) { create(:engine) }
let(:valid_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
api_post '/api/v2/admin/engines/new', token: token, params: valid_params
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['name']).to eq 'new-engine'
expect(result['data'].blank?).to eq true
api_post '/api/v2/admin/engines/new', token: token, params: valid_params
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.engine.duplicate_name')
end
it 'checked required params' do
api_post '/api/v2/admin/engines/new', params: {}, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.engine.missing_name')
expect(response).to include_api_error('admin.engine.missing_driver')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/engines/new', params: valid_params, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/engines/update' do
it 'updates attributes' do
api_post '/api/v2/admin/engines/update', params: { id: Engine.first.id, name: 'Second Engine', driver: 'second_driver' }, token: token
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
it 'updates secret' do
api_post '/api/v2/admin/engines/update', params: { id: Engine.first.id, secret: 'my_secret' }, token: token
expect(response).to be_successful
expect(Engine.first.secret).to eq('my_secret')
end
it 'checkes required params' do
api_post '/api/v2/admin/engines/update', params: {}, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.engine.missing_id')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/engines/update', params: { id: Engine.first.id, name: :new }, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
end

View File

@@ -0,0 +1,101 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::InternalTransfers, type: :request do
let(:endpoint) { '/api/v2/admin/internal_transfers' }
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0', username: 'test') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3, uid: 'ID84BF61C8H0', username: 'member') }
let(:level_3_member_token) { jwt_for(level_3_member) }
describe 'GET /api/v2/admin/internal_transfer' do
let!(:internal_transfer_btc) { create_list(:internal_transfer_btc, 4, :with_deposit_liability, sender: admin, receiver: level_3_member) }
let!(:internal_transfer_usd) { create_list(:internal_transfer_usd, 3, :with_deposit_liability, sender: level_3_member) }
it 'lists of internal transfers' do
api_get endpoint, token: token
expect(response).to be_successful
expect(response_body.size).to eq 7
end
it 'returns paginated internal transfers' do
api_get endpoint, params: { limit: 1, page: 1 }, token: token
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '7'
expect(response_body.size).to eq 1
api_get endpoint, params: { limit: 1, page: 2 }, token: token
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '7'
expect(response_body.size).to eq 1
end
it 'returns internal transfers by desc order' do
api_get endpoint, params: { ordering: 'desc' }, token: token
expect(response).to be_successful
expect(response_body.first['id']).to eq InternalTransfer.last.id
end
it 'returns internal transfers by ascending order' do
api_get endpoint, params: { ordering: 'asc' }, token: token
expect(response).to be_successful
expect(response_body.first['id']).to eq InternalTransfer.first.id
end
it 'return error in case of not permitted ability' do
api_get endpoint, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
context 'filtering' do
it 'by currency' do
api_get endpoint, token: token, params: { currency: 'btc' }
expect(response_body.count).to eq(InternalTransfer.where(currency_id: 'btc').count)
end
it 'returns orders for specific sender by uid' do
api_get endpoint, params: { sender: admin.uid }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['sender_uid']}.size).to eq 4
expect(result.map{|r| r['sender_uid']}).to all eq admin.uid
end
it 'returns orders for specific sender by username' do
api_get endpoint, params: { sender: admin.username }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['sender_username']}.size).to eq 4
expect(result.map{|r| r['sender_username']}).to all eq admin.username
end
it 'returns orders for specific receiver by uid' do
api_get endpoint, params: { receiver: level_3_member.uid }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['receiver_uid']}.size).to eq 4
expect(result.map{|r| r['receiver_uid']}).to all eq level_3_member.uid
end
it 'returns orders for specific receiver by username' do
api_get endpoint, params: { receiver: level_3_member.username }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['receiver_username']}.size).to eq 4
expect(result.map{|r| r['receiver_username']}).to all eq level_3_member.username
end
end
end
end

View File

@@ -0,0 +1,241 @@
# frozen_string_literal: true
describe API::V2::Admin::Markets, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
describe 'GET /api/v2/admin/markets/:id' do
let(:market) { Market.find_by(id: 'btcusd') }
it 'returns information about specified market' do
api_get "/api/v2/admin/markets/#{market.id}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.fetch('id')).to eq market.id
expect(result.fetch('base_unit')).to eq market.base_currency
expect(result.fetch('quote_unit')).to eq market.quote_currency
expect(result.fetch('data')).to eq market.data
end
it 'returns ordered by position currencies' do
api_get "/api/v2/admin/markets/", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('position')).to eq Market.ordered.pluck(:position)
end
context 'market name with dot' do
let!(:currency) { create(:currency, :xagm_cx) }
let!(:market) { create(:market, :xagm_cxusd) }
it 'returns information about specified market' do
api_get "/api/v2/admin/markets/#{market.id}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.fetch('id')).to eq market.id
expect(result.fetch('base_unit')).to eq market.base_currency
expect(result.fetch('quote_unit')).to eq market.quote_currency
expect(result.fetch('data')).to eq market.data
end
end
it 'returns error in case of invalid id' do
api_get '/api/v2/admin/markets/120', token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'return error in case of not permitted ability' do
api_get "/api/v2/admin/markets/#{market.id}", token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'GET /api/v2/admin/markets' do
it 'lists of markets' do
api_get '/api/v2/admin/markets', token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq 2
end
it 'returns markets by ascending order' do
api_get '/api/v2/admin/markets', params: { ordering: 'asc', order_by: 'quote_currency' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.first['quote_unit']).to eq 'eth'
end
it 'returns paginated markets' do
api_get '/api/v2/admin/markets', params: { limit: 1, page: 1 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '2'
expect(result.size).to eq 1
expect(result.first['id']).to eq 'btcusd'
api_get '/api/v2/admin/markets', params: { limit: 1, page: 2 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '2'
expect(result.size).to eq 1
expect(result.first['id']).to eq 'btceth'
end
it 'return error in case of not permitted ability' do
api_get '/api/v2/admin/markets', token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/markets/new' do
let(:engine) { create(:engine) }
let(:valid_params) do
{
base_currency: 'trst',
quote_currency: 'btc',
engine_id: engine.id,
price_precision: 2,
amount_precision: 2,
min_price: 0.01,
min_amount: 0.01,
data: {
upstream: {
driver: :opendax
}
}
}
end
it 'creates new market' do
api_post '/api/v2/admin/markets/new', token: token, params: valid_params
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['id']).to eq 'trstbtc'
expect(result['engine_id']).to eq Market.last.engine_id
expect(result['data']).to eq({ 'upstream' => { 'driver' => 'opendax' } })
end
it 'create new market with engine name param' do
api_post '/api/v2/admin/markets/new', token: token, params: valid_params.except(:engine_id).merge(engine_name: engine.name)
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['id']).to eq 'trstbtc'
expect(result['engine_id']).to eq Market.last.engine_id
expect(result['data']).to eq({ 'upstream' => { 'driver' => 'opendax' } })
end
it 'validate base_currency param' do
api_post '/api/v2/admin/markets/new', token: token, params: valid_params.merge(base_currency: 'test')
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.market.currency_doesnt_exist')
end
it 'validate quote_currency param' do
api_post '/api/v2/admin/markets/new', token: token, params: valid_params.merge(quote_currency: 'test')
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.market.currency_doesnt_exist')
end
it 'validate enabled param' do
api_post '/api/v2/admin/markets/new', token: token, params: valid_params.merge(state: '123')
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.market.invalid_state')
end
it 'validate engine name param' do
api_post '/api/v2/admin/markets/new', token: token, params: valid_params.except(:engine_id).merge(engine_name: 'test')
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.market.engine_doesnt_exist')
end
it 'checked exactly_one_ofr params' do
api_post '/api/v2/admin/markets/new', token: token, params: valid_params.merge(engine_name: 'test')
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.market.one_of_engine_id_engine_name_fields')
end
it 'checked required params' do
api_post '/api/v2/admin/markets/new', params: {}, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.market.missing_base_currency')
expect(response).to include_api_error('admin.market.missing_quote_currency')
expect(response).to include_api_error('admin.market.one_of_engine_id_engine_name_fields')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/markets/new', params: valid_params, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/markets/update' do
it 'updates attributes' do
api_post '/api/v2/admin/markets/update', params: { id: Market.first.id, amount_precision: 3, price_precision: 5, min_amount: 0.1, min_price: 0.1 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['amount_precision']).to eq 3
expect(result['price_precision']).to eq 5
expect(result['min_amount']).to eq '0.1'
expect(result['min_price']).to eq '0.1'
end
it 'updates data' do
api_post '/api/v2/admin/markets/update', params: { id: Market.first.id, data: { 'upstream' => { 'driver' => 'opendax' } } }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['data']).to eq({ 'upstream' => { 'driver' => 'opendax' } })
end
it 'validates data field' do
api_post '/api/v2/admin/markets/update', params: { id: Market.first.id, data: 'data' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.market.invalid_data')
end
it 'validates position' do
api_post '/api/v2/admin/markets/update', params: { id: Market.first.id, position: 0 }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.market.invalid_position')
end
it 'checkes required params' do
api_post '/api/v2/admin/markets/update', params: {}, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.market.missing_id')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/markets/update', params: { id: Market.first.id, state: :disabled }, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
end

View File

@@ -0,0 +1,195 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Members, type: :request do
let(:uid) { 'ID00FEE1DEAD' }
let(:email) { 'someone@mailbox.com' }
let(:admin) { create(:member, :admin, :level_3, email: email, uid: uid) }
let(:token) { jwt_for(admin) }
let(:member) { create(:member, :level_3) }
let(:member_token) { jwt_for(member) }
let!(:members) do
[
create(:member, role: 'admin', state: 'pending'),
create(:member, role: 'admin', state: 'active'),
create(:member, group: 'any'),
]
end
describe 'GET' do
context 'authentication' do
it 'requires token' do
get '/api/v2/admin/members'
expect(response.code).to eq '401'
end
it 'validates permissions' do
api_get'/api/v2/admin/members', token: member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'authenticate admin' do
api_get'/api/v2/admin/members', token: token
expect(response).to be_successful
end
end
context 'filtering' do
it 'returns all members' do
api_get'/api/v2/admin/members', token: token
result = JSON.parse(response.body)
expect(result.length).to eq(Member.count)
end
it 'filters by role & state' do
api_get'/api/v2/admin/members', token: token, params: { role: 'admin', state: 'active' }
result = JSON.parse(response.body)
expected = Member.where(role: 'admin', state: 'active').pluck(:id)
expect(result.map { |r| r['id'] }).to match_array expected
end
it 'filters by group' do
api_get'/api/v2/admin/members', token: token, params: { group: 'any' }
result = JSON.parse(response.body)
expected = Member.where(group: 'any').pluck(:id)
expect(result.map { |r| r['id'] }).to match_array expected
end
it 'filters by uid' do
api_get'/api/v2/admin/members', token: token, params: { uid: uid }
result = JSON.parse(response.body)
expect(result.length).to eq 1
expect(result.first['id']).to eq admin.id
end
end
context 'accounts' do
before { admin.touch_accounts }
it 'returns accounts for all currencies' do
api_get'/api/v2/admin/members', token: token, params: { uid: uid }
result = JSON.parse(response.body)
expect(result.first['accounts'].count).to eq(Currency.count)
end
end
end
describe 'Get by uid' do
context 'authentication' do
it 'requires token' do
get "/api/v2/admin/members/#{member.uid}"
expect(response.code).to eq '401'
end
it 'validates permissions' do
api_get "/api/v2/admin/members/#{member.uid}", token: member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'authenticate admin' do
api_get "/api/v2/admin/members/#{member.uid}", token: token
expect(response).to be_successful
end
end
context 'get user by uid' do
let!(:account) { member.touch_accounts }
let(:address) { Faker::Blockchain::Ethereum.address }
let(:coin) { Currency.find(:btc) }
let!(:beneficiary) { create(:beneficiary,
member: member,
currency: coin,
state: :active,
data: generate(:coin_beneficiary_data).merge(address: address)) }
it 'returns user entities' do
api_get "/api/v2/admin/members/UID1234", token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'returns user entities' do
api_get "/api/v2/admin/members/#{member.uid}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['uid']).to eq(member.uid)
expect(result['email']).to eq(member.email)
expect(result['uid']).to eq(member.uid)
expect(result['group']).to eq(member.group)
expect(result['accounts'][0]['currency']).to eq(member.accounts[0].currency.id)
expect(result['accounts'][0]['balance']).to eq(member.accounts[0].balance.to_s)
expect(result['accounts'][0]['locked']).to eq(member.accounts[0].locked.to_s)
expect(result['beneficiaries'][0]['currency']).to eq(member.beneficiaries[0].currency_id)
expect(result['beneficiaries'][0]['data']['address']).to eq(member.beneficiaries[0].data['address'])
end
context 'fiat beneficiary' do
let(:fiat) { Currency.find(:usd) }
let!(:beneficiary) { create(:beneficiary,
member: member,
currency: fiat,
state: :active,
data: generate(:fiat_beneficiary_data)) }
it 'returns user entities' do
api_get "/api/v2/admin/members/#{member.uid}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['uid']).to eq(member.uid)
expect(result['email']).to eq(member.email)
expect(result['uid']).to eq(member.uid)
expect(result['group']).to eq(member.group)
expect(result['accounts'][0]['currency']).to eq(member.accounts[0].currency.id)
expect(result['accounts'][0]['balance']).to eq(member.accounts[0].balance.to_s)
expect(result['accounts'][0]['locked']).to eq(member.accounts[0].locked.to_s)
expect(result['beneficiaries'][0]['currency']).to eq(member.beneficiaries[0].currency_id)
expect(result['beneficiaries'][0]['data']['address']).to eq(member.beneficiaries[0].data['address'])
expect(result['beneficiaries'][0]['data']['account_number']).to eq(member.beneficiaries[0].data['account_number'])
end
end
end
end
describe 'GET /api/v2/admin/members/groups' do
it 'get list of all existing groups' do
api_get '/api/v2/admin/members/groups', token: token
expect(JSON.parse(response.body)).to match_array Member.groups.map &:to_s
end
end
describe 'POST /api/v2/admin/members/groups' do
it 'returns user with updated role' do
api_put "/api/v2/admin/members/#{member.uid}", token: token, params: { group: 'vip-2' }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['group']).to eq('vip-2')
end
it 'returns user with updated group' do
api_put "/api/v2/admin/members/#{member.uid}", token: token, params: { group: ' Vip-2 ' }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['group']).to eq('vip-2')
end
it 'returns status 404 and error' do
api_put "/api/v2/admin/members/U1234", token: token, params: { group: 'vip-2' }
expect(response).to have_http_status(404)
expect(response).to include_api_error('record.not_found')
end
it 'validate params' do
api_put "/api/v2/admin/members/#{member.uid}", token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.member.missing_group')
end
end
end

View File

@@ -0,0 +1,175 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Operations, type: :request do
let(:uid) { 'ID00FEE1DEAD' }
let(:email) { 'someone@mailbox.com' }
let(:admin) { create(:member, :admin, :level_3, email: email, uid: uid) }
let(:token) { jwt_for(admin) }
let(:member) { create(:member, :level_3) }
let(:member_token) { jwt_for(member) }
describe 'GET' do
context 'authentication' do
it 'requires token' do
get '/api/v2/admin/assets'
expect(response.code).to eq '401'
end
it 'validates permissions' do
api_get'/api/v2/admin/expenses', token: member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'authenticate admin' do
api_get'/api/v2/admin/liabilities', token: token
expect(response).to be_successful
end
end
context 'assets' do
let!(:assets) do
[
create(:asset, currency: Currency.find(:btc), debit: 80.0),
create(:asset, currency: Currency.find(:btc), debit: 120.0),
create(:asset, currency: Currency.find(:usd), debit: 220.0),
]
end
it 'entity present valid fields' do
api_get '/api/v2/admin/assets', token: token
result = JSON.parse(response.body)
expected = %w[id rid currency reference_type credit debit created_at code account_kind]
expect(result.first.keys).to match_array expected
end
it 'filters by currency' do
api_get '/api/v2/admin/assets', token: token, params: { currency: 'usd' }
result = JSON.parse(response.body)
expected = assets.select { |a| a.currency_id == 'usd' }
expect(result.map { |a| a['id'] }).to match_array expected.map { |e| e.id }
end
it 'orders by debit ascending' do
api_get '/api/v2/admin/assets', token: token, params: { order_by: 'debit', ordering: 'asc' }
result = JSON.parse(response.body)
expected = assets.sort { |a, b| a.debit <=> b.debit }
expect(result.map { |a| a['id'] }).to match_array expected.map { |e| e.id }
end
end
context 'expenses' do
let!(:expenses) do
[
create(:expense, created_at: 5.days.ago, reference_type: 'Deposit'),
create(:expense, created_at: 2.days.ago, reference_type: 'Trade'),
create(:expense, created_at: 2.days.ago, reference_type: 'Deposit'),
]
end
it 'entity presents valid fields' do
api_get '/api/v2/admin/expenses', token: token
result = JSON.parse(response.body)
expected = %w[id rid currency reference_type credit debit created_at code account_kind]
expect(result.first.keys).to match_array expected
end
it 'filters by reference type' do
api_get '/api/v2/admin/expenses', token: token, params: { reference_type: 'Deposit' }
result = JSON.parse(response.body)
expected = expenses.select { |a| a.reference_type == 'Deposit' }
expect(result.map { |e| e['id'] }).to match_array expected.map { |e| e.id }
end
it 'fileters by created_at_to' do
api_get '/api/v2/admin/expenses', token: token, params: { range: 'created', to: 3.days.ago }
result = JSON.parse(response.body)
expected = expenses.select { |l| l.created_at < 3.days.ago }
expect(result.map { |a| a['id'] }).to match_array expected.map { |e| e.id }
end
it 'filters by created_at_from' do
api_get '/api/v2/admin/expenses', token: token, params: { range: 'created', from: 3.days.ago }
result = JSON.parse(response.body)
expected = expenses.select { |l| l.created_at >= 3.days.ago }
expect(result.map { |a| a['id'] }).to match_array expected.map { |e| e.id }
end
end
context 'revenues' do
let!(:revenues) do
[
create(:revenue, code: 301, currency: Currency.find(:usd), reference_id: 1),
create(:revenue, code: 302, currency: Currency.find(:btc), reference_id: 1),
create(:revenue, code: 302, currency: Currency.find(:btc), reference_id: 2),
]
end
it 'entity presents valid fields' do
api_get '/api/v2/admin/revenues', token: token
result = JSON.parse(response.body)
expected = %w[id rid currency reference_type credit debit created_at code account_kind]
expect(result.first.keys).to match_array expected
end
it 'filters by code' do
api_get '/api/v2/admin/revenues', token: token, params: { code: 302 }
result = JSON.parse(response.body)
expected = revenues.select { |a| a.code == 302 }
expect(result.map { |r| r['id'] }).to match_array expected.map { |e| e.id }
end
it 'filters by reference id' do
api_get '/api/v2/admin/revenues', token: token, params: { rid: 1 }
result = JSON.parse(response.body)
expected = revenues.select { |a| a.reference_id == 1 }
expect(result.map { |r| r['id'] }).to match_array expected.map { |e| e.id }
end
end
context 'liabilities' do
let!(:liabilities) do
[
create(:liability, member: member, credit: 110.0),
create(:liability, member: member, credit: 190.0),
create(:liability, member: admin, credit: 80.0),
]
end
it 'entity presents valid fields' do
api_get '/api/v2/admin/liabilities', token: token
result = JSON.parse(response.body)
expected = %w[id rid currency reference_type credit debit created_at code uid account_kind]
expect(result.first.keys).to match_array expected
end
it 'filters by member uid' do
api_get '/api/v2/admin/liabilities', token: token, params: { uid: member.uid }
result = JSON.parse(response.body)
expected = liabilities.select { |l| l.member.uid == member.uid }
expect(result.map { |a| a['id'] }).to match_array expected.map { |e| e.id }
end
it 'orders by credit descending' do
api_get '/api/v2/admin/liabilities', token: token, params: { order_by: 'credit', ordering: 'asc' }
result = JSON.parse(response.body)
expected = liabilities.sort { |a, b| b.credit <=> a.credit }
expect(result.map { |a| a['id'] }).to match_array expected.map { |e| e.id }
end
end
end
end

View File

@@ -0,0 +1,262 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Orders, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
describe 'GET /api/v2/admin/orders' do
before do
# NOTE: We specify updated_at attribute for testing order of Order.
create(:order_bid, :btcusd, price: '11'.to_d, origin_volume: '123.12', member: admin, updated_at: Time.at(1548224524), created_at: Time.at(1548234524))
create(:order_bid, :btceth, price: '11'.to_d, origin_volume: '123.12', member: admin, updated_at: Time.at(1548234524), created_at: Time.at(1548254524))
create(:order_bid, :btcusd, price: '12'.to_d, origin_volume: '123.12', member: admin, state: Order::CANCEL, updated_at: Time.at(1548244524), created_at: Time.at(1548254524))
create(:order_ask, :btcusd, price: '13'.to_d, origin_volume: '123.12', member: admin, state: Order::WAIT, updated_at: Time.at(1548254524), created_at: Time.at(1548254524))
create(:order_ask, :btcusd, price: '14'.to_d, origin_volume: '123.12', member: admin, state: Order::DONE, created_at: Time.at(1548254524))
end
it 'csv export' do
api_get'/api/v2/admin/orders', token: token, params: { format: :csv }
expect(response).to be_successful
end
it 'requires authentication' do
get '/api/v2/admin/orders', params: { market: 'btcusd' }
expect(response.code).to eq '401'
end
it 'validates market param' do
api_get '/api/v2/admin/orders', params: { market: 'usdusd' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.market.doesnt_exist')
end
it 'validates limit param' do
api_get '/api/v2/admin/orders', params: { market: 'btcusd', limit: -1 }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.pagination.invalid_limit')
end
it 'validates price param' do
api_get '/api/v2/admin/orders', params: { market: 'btcusd', price: -1 }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.order.non_positive_price')
end
it 'validates origin_volume param' do
api_get '/api/v2/admin/orders', params: { market: 'btcusd', origin_volume: -1 }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.order.non_positive_origin_volume')
end
it 'validates page param' do
api_get '/api/v2/admin/orders', params: { market: 'btcusd', limit: 2, page: "page 2" }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.pagination.non_integer_page')
end
it 'validates ord_type param' do
api_get '/api/v2/admin/orders', params: { ord_type: 'test' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.order.invalid_ord_type')
end
it 'returns orders with state done' do
api_get '/api/v2/admin/orders', params: { market: 'btcusd', state: Order::DONE }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 1
expect(result.first['state']).to eq Order::DONE
end
it 'returns all my orders for btcusd market' do
api_get '/api/v2/admin/orders', params: { market: 'btcusd' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 4
end
it 'returns orders with ord_type limit' do
api_get '/api/v2/admin/orders', params: { ord_type: 'limit' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['ord_type']}).to all eq 'limit'
end
it 'returns orders with type sell' do
api_get '/api/v2/admin/orders', params: { type: 'sell' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['side']}).to all eq 'sell'
end
it 'returns orders for specific price' do
api_get '/api/v2/admin/orders', params: { price: '11'.to_d }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['price']}.size).to eq 2
expect(result.map{|r| r['price']}).to all eq '11.0'
end
it 'returns orders for specific origin_volume' do
api_get '/api/v2/admin/orders', params: { origin_volume: '123.12' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['origin_volume']}.size).to eq 5
expect(result.map{|r| r['origin_volume']}).to all eq '123.12'
end
it 'returns orders for specific user by email' do
api_get '/api/v2/admin/orders', params: { email: 'example@gmail.com' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['email']}.size).to eq 5
expect(result.map{|r| r['email']}).to all eq 'example@gmail.com'
end
it 'returns orders for specific user by uid' do
api_get '/api/v2/admin/orders', params: { uid: 'ID73BF61C8H0' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['uid']}.size).to eq 5
expect(result.map{|r| r['uid']}).to all eq 'ID73BF61C8H0'
end
it 'returns paginated orders' do
api_get '/api/v2/admin/orders', params: { market: 'btcusd', limit: 1, page: 1 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 1
expect(result.first['price']).to eq '11.0'
api_get '/api/v2/admin/orders', params: { market: 'btcusd', limit: 1, page: 2 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 1
expect(result.first['price']).to eq '12.0'
end
it 'returns orders by ascending order' do
api_get '/api/v2/admin/orders', params: { market: 'btcusd', ordering: 'asc', order_by: 'updated_at'}, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.first['price']).to eq '11.0'
end
it 'returns orders for updated time range' do
api_get '/api/v2/admin/orders', params: { range: 'updated', from: Time.at(1548224524).iso8601, to: Time.at(1548244524).iso8601 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 3
end
it 'return error in case of not permitted ability' do
api_get'/api/v2/admin/orders', token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/orders/:id/cancel' do
let!(:order) { create(:order_bid, :btcusd, price: '12.32'.to_d, volume: '3.14', origin_volume: '12.13', locked: '20.1082', origin_locked: '38.0882', member: level_3_member) }
before do
level_3_member.get_account(:usd).update_attributes(locked: order.price * order.volume)
end
it 'should cancel specified order' do
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: order.to_matching_attributes)
expect do
api_post "/api/v2/admin/orders/#{order.id}/cancel", token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['id']).to eq order.id
end.not_to change(Order, :count)
end
it 'return error in case of non existent order' do
api_post '/api/v2/admin/orders/0/cancel', token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/orders/0/cancel', token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/orders/cancel' do
before do
create(:order_ask, :btcusd, price: '12.32', volume: '3.14', origin_volume: '12.13', member: level_3_member)
create(:order_bid, :btcusd, price: '12.32', volume: '3.14', origin_volume: '12.13', member: level_3_member)
create(:order_bid, :btceth, price: '12.32', volume: '3.14', origin_volume: '12.13', member: level_3_member)
level_3_member.get_account(:btc).update_attributes(locked: '5')
level_3_member.get_account(:usd).update_attributes(locked: '50')
end
it 'should cancel all my orders for specific market' do
level_3_member.orders.where(market: 'btceth').each do |o|
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: o.to_matching_attributes)
end
expect do
api_post '/api/v2/admin/orders/cancel', token: token, params: { market: 'btceth' }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 1
end.not_to change(Order, :count)
end
it 'should cancel all asks for specific market' do
level_3_member.orders.where(type: 'OrderAsk', market_id: 'btcusd').each do |o|
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: o.to_matching_attributes)
end
expect do
api_post '/api/v2/admin/orders/cancel', token: token, params: { market: 'btcusd', side: 'sell' }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 1
expect(result.first['id']).to eq level_3_member.orders.where(type: 'OrderAsk').first.id
end.not_to change(Order, :count)
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/orders/cancel', token: level_3_member_token, params: { market: 'btceth' }
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'return error in case of invalid order type' do
api_post '/api/v2/admin/orders/cancel', token: token, params: { market: 'btceth', side: 'ask' }
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.order.invalid_side')
end
it 'return error in case of invalid market' do
api_post '/api/v2/admin/orders/cancel', token: token, params: { market: 'testusd' }
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.order.market_doesnt_exist')
end
end
end

View File

@@ -0,0 +1,230 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Trades, type: :request do
let(:uid) { 'ID00FEE1DEAD' }
let(:email) { 'someone@mailbox.com' }
let(:admin) { create(:member, :admin, :level_3, email: email, uid: uid) }
let(:token) { jwt_for(admin) }
let(:member) { create(:member, :level_3) }
let(:member_token) { jwt_for(member) }
describe 'GET /api/v2/admin/trades' do
let!(:trades) do
[
create(:trade, :btcusd, price: 12.0, amount: 2.0, created_at: 3.days.ago),
create(:trade, :btcusd, price: 3.0, amount: 13.0, created_at: 5.days.ago),
create(:trade, :btcusd, price: 25.0, amount: 5.0, created_at: 1.days.ago, maker: member),
create(:trade, :btcusd, price: 6.0, amount: 5.0, created_at: 5.days.ago, taker: member),
create(:trade, :btcusd, price: 5.0, amount: 6.0, created_at: 5.days.ago, taker: member),
]
end
it 'entity provides correct fields' do
api_get'/api/v2/admin/trades', token: token, params: { limit: 5 }
result = JSON.parse(response.body).first
keys = %w[id amount price total maker_order_email taker_order_email created_at maker_uid taker_uid
taker_type market maker_fee_currency maker_fee_amount taker_fee_currency taker_fee_amount]
expect(result.keys).to match_array keys
expect(result.values).not_to include nil
end
it 'csv export' do
api_get'/api/v2/admin/trades', token: token, params: { format: :csv }
expect(response).to be_successful
end
context 'authentication' do
it 'requires token' do
get '/api/v2/admin/trades'
expect(response.code).to eq '401'
end
it 'validates permissions' do
api_get'/api/v2/admin/trades', token: member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'authenticate admin' do
api_get'/api/v2/admin/trades', token: token
expect(response).to be_successful
end
end
context 'pagination' do
it 'with default values' do
api_get'/api/v2/admin/trades', token: token
result = JSON.parse(response.body)
expect(result.length).to eq trades.length
end
it 'validates limit' do
api_get'/api/v2/admin/trades', token: token, params: { limit: 'meow' }
expect(response).to include_api_error 'admin.pagination.non_integer_limit'
end
it 'validates page' do
api_get'/api/v2/admin/trades', token: token, params: { page: 'meow' }
expect(response).to include_api_error 'admin.pagination.non_integer_page'
end
it 'first 5 trades ordered by id' do
api_get'/api/v2/admin/trades', token: token, params: { limit: 5 }
result = JSON.parse(response.body)
expected = trades[0...5]
expect(result.map { |t| t['id'] }).to match_array expected.map(&:id)
end
it 'second 5 trades ordered by id' do
api_get'/api/v2/admin/trades', token: token, params: { limit: 5, page: 2 }
result = JSON.parse(response.body)
expected = trades[5...10]
expect(result.map { |t| t['id'] }).to match_array expected.map(&:id)
end
end
context 'ordering' do
it 'validates ordering' do
api_get'/api/v2/admin/trades', token: token, params: { ordering: 'straight' }
expect(response).not_to be_successful
end
it 'orders by price ascending' do
api_get'/api/v2/admin/trades', token: token, params: { order_by: 'price', ordering: 'asc' }
result = JSON.parse(response.body)
expected = trades.sort { |a, b| a.price <=> b.price }
expect(result.map { |t| t['id'] }).to match_array expected.map(&:id)
end
it 'orders by amount descending' do
api_get'/api/v2/admin/trades', token: token, params: { order_by: 'amount', ordering: 'asc' }
result = JSON.parse(response.body)
expected = trades.sort { |a, b| b.amount <=> a.amount }
expect(result.map { |t| t['id'] }).to match_array expected.map(&:id)
end
end
context 'filtering' do
context 'with market' do
it 'validates market param' do
api_get'/api/v2/admin/trades', token: token, params: { market: 'btcbtc' }
expect(response).to include_api_error "admin.market.doesnt_exist"
end
it 'filters by market' do
api_get'/api/v2/admin/trades', token: token, params: { market: 'btcusd' }
result = JSON.parse(response.body)
expected = trades.select { |t| t.market_id == 'btcusd' }
expect(result.map { |t| t['id'] }).to match_array expected.map(&:id)
end
end
context 'with uid' do
it 'returns orders for specific user (both maker and taker sides)' do
api_get'/api/v2/admin/trades', token: token, params: { uid: member.uid }
result = JSON.parse(response.body)
expected = member.trades
expect(result.map { |t| t['id'] }).to match_array expected.map(&:id)
end
it 'return error when user does not exist' do
api_get'/api/v2/admin/trades', token: token, params: { uid: 'ID00DEADBEEF' }
expect(response).to include_api_error 'admin.user.doesnt_exist'
end
it 'empty collection when user has no trades' do
api_get'/api/v2/admin/trades', token: token, params: { uid: admin.uid }
expect(JSON.parse(response.body)).to be_empty
end
end
context 'with timestamps' do
it 'validates created_at_from' do
api_get'/api/v2/admin/trades', token: token, params: { from: 'yesterday' }
expect(response).to include_api_error 'admin.filter.range_from_invalid'
end
it 'validates created_at_to' do
api_get'/api/v2/admin/trades', token: token, params: { to: 'today' }
expect(response).to include_api_error 'admin.filter.range_to_invalid'
end
it 'returns trades created after specidfied date' do
api_get'/api/v2/admin/trades', token: token, params: { from: 4.days.ago }
result = JSON.parse(response.body)
expected = trades.select { |t| t.created_at >= 4.days.ago }
expect(result.map { |t| t['id'] }).to match_array expected.map(&:id)
end
it 'return trades created before specidfied date' do
api_get'/api/v2/admin/trades', token: token, params: { to: 2.days.ago }
result = JSON.parse(response.body)
expected = trades.select { |t| t.created_at < 2.days.ago }
expect(result.map { |t| t['id'] }).to match_array expected.map(&:id)
end
it 'returns trades created after and before specidfied dates' do
api_get'/api/v2/admin/trades', token: token, params: { from: 4.days.ago, to: 2.days.ago }
result = JSON.parse(response.body)
expected = trades.select { |t| t.created_at >= 4.days.ago && t.created_at < 2.days.ago }
expect(result.map { |t| t['id'] }).to match_array expected.map(&:id)
end
end
end
end
describe 'GET /api/v2/admin/trades/:id' do
let(:maker) { create(:order_ask, :btcusd) }
let(:taker) { create(:order_bid, :btcusd) }
let(:trade) { create(:trade, :btcusd, price: 12.0, amount: 2.0, maker_order: maker, taker_order: taker) }
it 'entity provides correct fields' do
api_get "/api/v2/admin/trades/#{trade.id}", token: token
result = JSON.parse(response.body)
keys = %w[id amount price total maker_order_email taker_order_email created_at maker_uid taker_uid taker_type
market maker_fee_currency maker_fee maker_fee_amount taker_fee_currency taker_fee taker_fee_amount maker_order taker_order]
expect(result.keys).to match_array keys
expect(result.values).not_to include nil
end
it 'exposes correct orders' do
api_get "/api/v2/admin/trades/#{trade.id}", token: token
result = JSON.parse(response.body)
expect(result['maker_order']['id']).to eq maker.id
expect(result['taker_order']['id']).to eq taker.id
end
it 'fee calculation' do
api_get "/api/v2/admin/trades/#{trade.id}", token: token
result = JSON.parse(response.body)
expect(result['maker_fee_amount']).to eq((trade.total * maker.maker_fee).to_s)
expect(result['taker_fee_amount']).to eq((trade.amount * taker.taker_fee).to_s)
end
it 'fee currency' do
api_get "/api/v2/admin/trades/#{trade.id}", token: token
result = JSON.parse(response.body)
expect(result['maker_fee_currency']).to eq 'usd'
expect(result['taker_fee_currency']).to eq 'btc'
end
end
end

View File

@@ -0,0 +1,266 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::TradingFees, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H1') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
describe 'GET /trading_fees' do
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' do
api_get '/api/v2/admin/trading_fees', token: token
expect(response.status).to eq 200
expect(JSON.parse(response.body).length).to eq TradingFee.count
end
it 'pagination' do
api_get '/api/v2/admin/trading_fees', token: token, params: { limit: 1 }
expect(JSON.parse(response.body).length).to eq 1
end
it 'filters by market_id' do
api_get '/api/v2/admin/trading_fees', token: token, params: { market_id: 'btcusd' }
result = JSON.parse(response.body)
expect(result.map { |r| r['market_id'] }).to all eq 'btcusd'
expect(result.length).to eq TradingFee.where(market_id: 'btcusd').count
end
it 'filters by group' do
api_get '/api/v2/admin/trading_fees', token: token, params: { group: 'vip-0' }
result = JSON.parse(response.body)
expect(result.map { |r| r['group'] }).to all eq 'vip-0'
expect(result.length).to eq TradingFee.where(group: 'vip-0').count
end
it 'capitalized fee group' do
api_get '/api/v2/admin/trading_fees', token: token, params: { group: 'Vip-0' }
result = JSON.parse(response.body)
expect(result.map { |r| r['group'] }).to all eq 'vip-0'
expect(result.length).to eq TradingFee.where(group: 'vip-0').count
end
end
describe 'POST /trading_fees/new' do
it 'creates a table with default group' do
api_post '/api/v2/admin/trading_fees/new', token: token, params: { maker: 0.001, taker: 0.0015, market_id: 'btcusd' }
expect(response).to be_successful
expect(JSON.parse(response.body)['maker']).to eq('0.001')
expect(JSON.parse(response.body)['taker']).to eq('0.0015')
expect(JSON.parse(response.body)['group']).to eq('any')
expect(JSON.parse(response.body)['market_id']).to eq('btcusd')
end
it 'creates a table with default market' do
api_post '/api/v2/admin/trading_fees/new', token: token, params: { group: 'vip-1', maker: 0.001, taker: 0.0015 }
expect(response).to be_successful
expect(JSON.parse(response.body)['maker']).to eq('0.001')
expect(JSON.parse(response.body)['taker']).to eq('0.0015')
expect(JSON.parse(response.body)['group']).to eq('vip-1')
expect(JSON.parse(response.body)['market_id']).to eq('any')
end
it 'returns created trading fee table' do
api_post '/api/v2/admin/trading_fees/new', token: token, params: { group: 'vip-1', market_id: 'btcusd', maker: 0.001, taker: 0.0015 }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['maker']).to eq('0.001')
expect(JSON.parse(response.body)['taker']).to eq('0.0015')
expect(JSON.parse(response.body)['group']).to eq('vip-1')
expect(JSON.parse(response.body)['market_id']).to eq('btcusd')
end
context 'returns created trading fee table without group' do
it 'returns created trading fee table' do
api_post '/api/v2/admin/trading_fees/new', token: token, params: { market_id: 'btcusd', maker: 0.001, taker: 0.0015 }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['maker']).to eq('0.001')
expect(JSON.parse(response.body)['taker']).to eq('0.0015')
expect(JSON.parse(response.body)['group']).to eq('any')
expect(JSON.parse(response.body)['market_id']).to eq('btcusd')
end
end
context 'returns created trading fee table without market_id' do
it 'returns created trading fee table' do
api_post '/api/v2/admin/trading_fees/new', token: token, params: { maker: 0.001, taker: 0.0015, group: 'vip-1' }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['maker']).to eq('0.001')
expect(JSON.parse(response.body)['taker']).to eq('0.0015')
expect(JSON.parse(response.body)['group']).to eq('vip-1')
expect(JSON.parse(response.body)['market_id']).to eq('any')
end
end
context 'invalid market_id' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/trading_fees/new', token: token, params: { maker: 0.001, taker: 0.0015, market_id: 'uahusd' }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.trading_fee.market_doesnt_exist')
end
end
context 'empty maker field' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/trading_fees/new', token: token, params: { taker: 0.0015, group: 'vip-1', market_id: 'btcusd' }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.trading_fee.invalid_maker')
end
end
context 'empty taker field' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/trading_fees/new', token: token, params: { maker: 0.0015, group: 'vip-1', market_id: 'btcusd' }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.trading_fee.invalid_taker')
end
end
context 'invalid maker/taker type' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/trading_fees/new', token: token, params: { taker: -0.1, maker: -0.15, group: 'vip-1', market_id: 'btcusd' }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.trading_fee.invalid_maker')
expect(response).to include_api_error('admin.trading_fee.invalid_taker')
end
end
context 'invalid maker/taker fee' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/trading_fees/new', token: token, params: { taker: 1, maker: 1, group: 'vip-1', market_id: 'btcusd' }
expect(response).to have_http_status(422)
expect(response).to include_api_error('Maker must be less than or equal to 0.5')
expect(response).to include_api_error('Taker must be less than or equal to 0.5')
end
end
end
describe 'POST /trading_fees/update' do
it 'returns updated trading fee table with new group' do
api_post '/api/v2/admin/trading_fees/update', token: token, params: { group: 'vip-1', id: TradingFee.first.id }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['maker']).to eq('0.0015')
expect(JSON.parse(response.body)['taker']).to eq('0.0015')
expect(JSON.parse(response.body)['group']).to eq('vip-1')
expect(JSON.parse(response.body)['market_id']).to eq('any')
end
it 'returns updated trading fee table with new group with capitalized letter' do
api_post '/api/v2/admin/trading_fees/update', token: token, params: { group: 'Vip-1 ', id: TradingFee.first.id }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['maker']).to eq('0.0015')
expect(JSON.parse(response.body)['taker']).to eq('0.0015')
expect(JSON.parse(response.body)['group']).to eq('vip-1')
expect(JSON.parse(response.body)['market_id']).to eq('any')
end
it 'returns updated trading fee table with new maker' do
api_post '/api/v2/admin/trading_fees/update', token: token, params: { market_id: 'btcusd', id: TradingFee.first.id }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['maker']).to eq('0.0015')
expect(JSON.parse(response.body)['taker']).to eq('0.0015')
expect(JSON.parse(response.body)['group']).to eq('any')
expect(JSON.parse(response.body)['market_id']).to eq('btcusd')
end
it 'returns updated trading fee table with new maker, taker fields' do
api_post '/api/v2/admin/trading_fees/update', token: token, params: { maker: 0.1, taker: 0.1, id: TradingFee.first.id }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['maker']).to eq('0.1')
expect(JSON.parse(response.body)['taker']).to eq('0.1')
expect(JSON.parse(response.body)['group']).to eq('any')
expect(JSON.parse(response.body)['market_id']).to eq('any')
end
context 'not found trading_fee table' do
it 'returns status 404 and error' do
api_post '/api/v2/admin/trading_fees/update', token: token, params: { id: TradingFee.last.id + 1 }
expect(response).to have_http_status(404)
expect(response).to include_api_error('record.not_found')
end
end
context 'empty maker type' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/trading_fees/update', token: token, params: { maker: -1, id: TradingFee.first.id }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.trading_fee.invalid_maker')
end
end
context 'empty taker type' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/trading_fees/update', token: token, params: { taker: -1, id: TradingFee.first.id }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.trading_fee.invalid_taker')
end
end
context 'invalid maker/taker type' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/trading_fees/update', token: token, params: { market_id: 'uahusd', id: TradingFee.first.id }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.trading_fee.market_doesnt_exist')
end
end
end
describe 'POST /trading_fees/delete' do
let!(:trading_fee) { create(:trading_fee) }
it 'requires id' do
api_post '/api/v2/admin/trading_fees/delete', token: token
expect(response).to include_api_error 'admin.tradingfee.missing_id'
end
it 'deletes trading fee table' do
expect {
api_post '/api/v2/admin/trading_fees/delete', token: token, params: { id: trading_fee.id }
}.to change { TradingFee.count }.by(-1)
expect(response).to have_http_status(201)
end
it 'returns deleted trading fee table' do
api_post '/api/v2/admin/trading_fees/delete', token: token, params: { id: trading_fee.id }
expect(JSON.parse(response.body)['id']).to eq trading_fee.id
end
it 'retuns 404 if record does not exist' do
expect {
api_post '/api/v2/admin/trading_fees/delete', token: token, params: { id: TradingFee.last.id + 42 }
}.not_to change { TradingFee.count }
expect(response.status).to eq 404
end
end
end

View File

@@ -0,0 +1,358 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Wallets, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
describe 'GET /api/v2/admin/wallets/:id' do
let(:wallet) { Wallet.find_by(blockchain_key: 'eth-rinkeby') }
it 'returns information about specified wallet' do
api_get "/api/v2/admin/wallets/#{wallet.id}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.fetch('id')).to eq wallet.id
expect(result.fetch('currencies')).to eq wallet.currency_ids
expect(result.fetch('address')).to eq wallet.address
end
it 'returns error in case of invalid id' do
api_get '/api/v2/admin/wallets/120', token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'return error in case of not permitted ability' do
api_get "/api/v2/admin/wallets/#{wallet.id}", token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'returns information about specified wallet' do
api_get "/api/v2/admin/wallets/#{wallet.id}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result).not_to include('settings')
end
it 'returns NA balance if node not accessible' do
wallet.update(balance: wallet.current_balance)
api_get "/api/v2/admin/wallets/#{wallet.id}", token: token
expect(response).to be_successful
expect(response_body['balance']).to eq(wallet.current_balance)
end
it 'returns wallet balance if node accessible' do
wallet.update(balance: { 'eth' => '1'})
api_get "/api/v2/admin/wallets/#{wallet.id}", token: token
expect(response).to be_successful
expect(response_body['balance']).to eq({ 'eth' => '1' })
end
end
describe 'GET /api/v2/admin/wallets' do
it 'lists of wallets' do
api_get '/api/v2/admin/wallets', token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Wallet.count
end
it 'returns paginated wallets' do
api_get '/api/v2/admin/wallets', params: { limit: 6, page: 1 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq Wallet.count.to_s
expect(result.size).to eq 6
expect(result.first['name']).to eq 'Ethereum Deposit Wallet'
api_get '/api/v2/admin/wallets', params: { limit: 6, page: 2 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq Wallet.count.to_s
expect(result.size).to eq 2
expect(result.first['name']).to eq 'Bitcoin Hot Wallet'
end
it 'return error in case of not permitted ability' do
api_get "/api/v2/admin/wallets", token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
context 'filtering' do
it 'filters by blockchain key' do
api_get "/api/v2/admin/wallets", token: token, params: { blockchain_key: "eth-rinkeby" }
result = JSON.parse(response.body)
expect(result.length).not_to eq 0
expect(result.map { |r| r["blockchain_key"]}).to all eq "eth-rinkeby"
end
it 'filters by kind'do
api_get "/api/v2/admin/wallets", token: token, params: { kind: "deposit" }
result = JSON.parse(response.body)
expect(result.length).not_to eq 0
expect(result.map { |r| r["kind"]}).to all eq "deposit"
end
context do
let(:hot_wallet) { Wallet.joins(:currencies).find_by(blockchain_key: 'eth-rinkeby', kind: :hot, currencies: { id: :eth }) }
before do
hot_wallet.currencies << Currency.find(:trst)
end
it 'filters by currency' do
api_get '/api/v2/admin/wallets', token: token, params: { currencies: 'eth' }
expect(response_body.length).not_to eq 0
expect(response_body.pluck('currencies').map { |a| a.include?('eth') }.all?).to eq(true)
count = Wallet.joins(:currencies).where(currencies: { id: :eth }).count
expect(response_body.find { |c| c['id'] == hot_wallet.id }['currencies'].sort).to eq(%w[eth trst])
expect(response_body.count).to eq(count)
end
it 'filters by currency' do
api_get '/api/v2/admin/wallets', token: token, params: { currencies: %w[eth trst] }
expect(response_body.length).not_to eq 0
count = Wallet.joins(:currencies).where(currencies: { id: %i[eth trst] }).distinct.count
expect(response_body.find { |c| c['id'] == hot_wallet.id }['currencies'].sort).to eq(%w[eth trst])
expect(response_body.count).to eq(count)
end
end
end
end
describe 'GET /api/v2/admin/wallets/kinds' do
it 'list kinds' do
api_get '/api/v2/admin/wallets/kinds', token: token
expect(response).to be_successful
end
end
describe 'GET /api/v2/admin/wallets/gateways' do
it 'list gateways' do
api_get '/api/v2/admin/wallets/gateways', token: token
expect(response).to be_successful
end
end
describe 'POST /api/v2/admin/wallets/new' do
it 'create wallet' do
api_post '/api/v2/admin/wallets/new', params: { name: 'Test', kind: 'deposit', currencies: 'eth', address: 'blank', blockchain_key: 'btc-testnet', gateway: 'geth', settings: { uri: 'http://127.0.0.1:18332'}}, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['name']).to eq 'Test'
end
it 'create wallet' do
api_post '/api/v2/admin/wallets/new', params: { name: 'Test', kind: 'deposit', currencies: ['eth','trst'], address: 'blank', blockchain_key: 'btc-testnet', gateway: 'geth', settings: { uri: 'http://127.0.0.1:18332'}}, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['currencies']).to eq(['eth', 'trst'])
expect(result['name']).to eq 'Test'
end
it 'checked required params' do
api_post '/api/v2/admin/wallets/new', params: { }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.wallet.missing_name')
expect(response).to include_api_error('admin.wallet.missing_kind')
expect(response).to include_api_error('admin.wallet.currencies_field_is_missing')
expect(response).to include_api_error('admin.wallet.missing_blockchain_key')
expect(response).to include_api_error('admin.wallet.missing_gateway')
end
it 'validate status' do
api_post '/api/v2/admin/wallets/new', params: { name: 'Test', kind: 'deposit', currencies: 'eth', address: 'blank', blockchain_key: 'btc-testnet', gateway: 'geth', settings: { uri: 'http://127.0.0.1:18332'}, status: 'disable' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.invalid_status')
end
it 'validate gateway' do
api_post '/api/v2/admin/wallets/update', params: { name: 'Test', kind: 'deposit', currencies: 'eth', address: 'blank', blockchain_key: 'btc-testnet', settings: { uri: 'http://127.0.0.1:18332'}, gateway: 'test' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.gateway_doesnt_exist')
end
it 'validate kind' do
api_post '/api/v2/admin/wallets/update', params: { name: 'Test', kind: 'test', currencies: 'eth', address: 'blank', blockchain_key: 'btc-testnet', settings: { uri: 'http://127.0.0.1:18332'}, gateway: 'geth' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.invalid_kind')
end
it 'validate currency_id' do
api_post '/api/v2/admin/wallets/update', params: { id: 1, name: 'Test', kind: 'deposit', address: 'blank', blockchain_key: 'btc-testnet', gateway: 'geth', settings: { uri: 'http://127.0.0.1:18332'}, currencies: 'test' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.currency_doesnt_exist')
end
it 'validate uri' do
api_post '/api/v2/admin/wallets/new', params: { name: 'Test', kind: 'hot', currencies: 'eth', address: 'blank', blockchain_key: 'btc-testnet', settings: { uri: 'invalid_uri'}, gateway: 'geth' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.invalid_uri_setting')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/wallets/new', params: { name: 'Test', kind: 'deposit', currencies: 'eth', address: 'blank', blockchain_key: 'btc-testnet', gateway: 'geth', settings: { uri: 'http://127.0.0.1:18332'}}, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/wallets/update' do
it 'update wallet' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, gateway: 'geth' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['gateway']).to eq 'geth'
end
it 'update currency' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, currencies: 'btc' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['currencies']).to eq ['btc']
end
it 'update wallet with new secret' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, currencies: 'btc', settings: { secret: 'new secret'} }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['currencies']).to eq ['btc']
expect(Wallet.first.settings['uri']).to eq nil
expect(Wallet.first.settings['secret']).to eq 'new secret'
end
it 'update wallet with settings' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, currencies: 'btc', settings: { secret: 'new secret', access_token: 'new token'} }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['currencies']).to eq ['btc']
expect(Wallet.first.settings['uri']).to eq nil
expect(Wallet.first.settings['access_token']).to eq 'new token'
expect(Wallet.first.settings['secret']).to eq 'new secret'
end
it 'validate blockchain_key' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, blockchain_key: 'test' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.blockchain_key_doesnt_exist')
end
it 'validate status' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, status: 'disable' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.invalid_status')
end
it 'validate gateway' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, gateway: 'test' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.gateway_doesnt_exist')
end
it 'validate kind' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, kind: 'test' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.invalid_kind')
end
it 'validate currency_id' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, currencies: 'test ' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.currency_doesnt_exist')
end
it 'checked required params' do
api_post '/api/v2/admin/wallets/update', params: { }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.wallet.missing_id')
end
it 'validate uri' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, name: 'Test', kind: 'hot', currencies: 'eth', address: 'blank', blockchain_key: 'btc-testnet', settings: { uri: 'invalid_uri'}, gateway: 'geth' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.wallet.invalid_uri_setting')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/wallets/update', params: { id: Wallet.first.id, status: 'disabled' }, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'POST /api/v2/admin/wallets/currencies' do
let(:wallet) { Wallet.joins(:currencies).find_by(currencies: { id: 'eth' }) }
it do
api_post '/api/v2/admin/wallets/currencies', params: { id: wallet.id, currencies: 'trst' }, token: token
expect(response).to be_successful
expect(response_body['currencies'].include?('trst')).to be_truthy
end
it do
api_post '/api/v2/admin/wallets/currencies', params: { id: wallet.id, currencies: 'eth' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('Currency has already been taken')
end
end
describe 'POST /api/v2/admin/wallets/currencies' do
let(:wallet) { Wallet.joins(:currencies).find_by(currencies: { id: 'eth' }) }
it do
api_delete '/api/v2/admin/wallets/currencies', params: { id: wallet.id, currencies: 'eth' }, token: token
expect(response).to be_successful
expect(response_body['currencies'].include?('eth')).to be_falsey
end
it do
api_delete '/api/v2/admin/wallets/currencies', params: { id: wallet.id, currencies: 'trst' }, token: token
expect(response).to have_http_status 404
end
end
end

View File

@@ -0,0 +1,201 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::WhitelistedSmartContracts, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example15@gmail.com', uid: 'ID73BF61C8H1') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
let(:test_file) { Rack::Test::UploadedFile.new(Rails.root.join('spec', 'resources', 'whitelisted_addresses', file_name), 'text/csv') }
let(:file_name) { '1.csv' }
describe 'GET /api/v2/admin/whitelisted_smart_contract/:id' do
let!(:addresses_1) { create(:whitelisted_smart_contract, :address_1) }
let!(:addresses_2) { create(:whitelisted_smart_contract, :address_2) }
let!(:addresses_3) { create(:whitelisted_smart_contract, :address_3) }
let!(:addresses_4) { create(:whitelisted_smart_contract, :address_4) }
let!(:addresses_5) { create(:whitelisted_smart_contract, :address_5) }
let(:whitelisted_address) { WhitelistedSmartContract.find(1) }
it 'returns information about specified WhitelistedSmartContract' do
api_get "/api/v2/admin/whitelisted_smart_contract/#{whitelisted_address.id}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.fetch('id')).to eq whitelisted_address.id
expect(result.fetch('address')).to eq whitelisted_address.address
end
it 'returns error in case of invalid id' do
api_get '/api/v2/admin/whitelisted_smart_contract/120', token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'return error in case of not permitted ability' do
api_get "/api/v2/admin/whitelisted_smart_contract/#{whitelisted_address.id}", token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'GET /api/v2/admin/whitelisted_smart_contracts' do
let!(:addresses_1) { create(:whitelisted_smart_contract, :address_1) }
let!(:addresses_2) { create(:whitelisted_smart_contract, :address_2) }
let!(:addresses_3) { create(:whitelisted_smart_contract, :address_3) }
let!(:addresses_4) { create(:whitelisted_smart_contract, :address_4) }
let!(:addresses_5) { create(:whitelisted_smart_contract, :address_5) }
it 'lists of whitelisted_smart_contracts' do
api_get '/api/v2/admin/whitelisted_smart_contracts', token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq WhitelistedSmartContract.count
end
it 'returns paginated whitelisted_smart_contracts' do
api_get '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { limit: 4, page: 1 }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq WhitelistedSmartContract.count.to_s
expect(result.size).to eq 4
end
it 'return error in case of not permitted ability' do
api_get "/api/v2/admin/whitelisted_smart_contracts", token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
context 'filtering' do
it 'filters by blockchain key' do
api_get "/api/v2/admin/whitelisted_smart_contracts", token: token, params: { blockchain_key: "eth-rinkeby" }
result = JSON.parse(response.body)
expect(result.length).not_to eq 0
expect(result.map { |r| r["blockchain_key"]}).to all eq "eth-rinkeby"
end
end
end
describe 'POST /api/v2/admin/whitelisted_smart_contracts/csv' do
it 'create whitelisted_smart_contracts from csv' do
api_post '/api/v2/admin/whitelisted_smart_contracts/csv', token: token, params: { file: test_file }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 3
end
end
describe 'POST /api/v2/admin/whitelisted_smart_contracts' do
it 'create whitelisted_smart_contracts' do
api_post '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { description: 'Test', address: 'blank', blockchain_key: 'eth-rinkeby' }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['description']).to eq 'Test'
end
it 'checked required params' do
api_post '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { }
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.whitelistedsmartcontract.missing_address')
expect(response).to include_api_error('admin.whitelistedsmartcontract.missing_blockchain_key')
end
it 'validate state' do
api_post '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { description: 'Test', address: 'blank', blockchain_key: 'eth-rinkeby', state: 'invalid' }
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.whitelistedsmartcontract.invalid_state')
end
it 'return error in case of not permitted ability' do
api_post '/api/v2/admin/whitelisted_smart_contracts', params: { description: 'Test', address: 'blank', blockchain_key: 'eth-rinkeby'}, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
end
describe 'PUT /api/v2/admin/whitelisted_smart_contracts' do
let!(:addresses_1) { create(:whitelisted_smart_contract, :address_1) }
it 'update WhitelistedSmartContract' do
api_put '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { id: WhitelistedSmartContract.first.id, address: 'test' }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['address']).to eq 'test'
end
it 'update WhitelistedSmartContract with new description' do
api_put '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { id: WhitelistedSmartContract.first.id, description: 'test'}
result = JSON.parse(response.body)
expect(response).to be_successful
expect(WhitelistedSmartContract.first.description).to eq 'test'
end
it 'update WhitelistedSmartContract with new state' do
api_put '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { id: WhitelistedSmartContract.first.id, state: 'disabled' }
expect(response).to be_successful
expect(WhitelistedSmartContract.first.state).to eq 'disabled'
end
it 'validate blockchain_key' do
api_put '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { id: WhitelistedSmartContract.first.id, blockchain_key: 'test' }
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.whitelistedsmartcontract.blockchain_key_doesnt_exist')
end
it 'validate status' do
api_put '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { id: WhitelistedSmartContract.first.id, state: 'disable' }
expect(response.code).to eq '422'
expect(response).to include_api_error('admin.whitelistedsmartcontract.invalid_state')
end
it 'checked required params' do
api_put '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { }
expect(response).to have_http_status 422
expect(response).to include_api_error('admin.whitelistedsmartcontract.missing_id')
end
it 'return error in case of not permitted ability' do
api_put '/api/v2/admin/whitelisted_smart_contracts', params: { id: WhitelistedSmartContract.first.id, status: 'disabled' }, token: level_3_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('admin.ability.not_permitted')
end
it 'return error in case of not permitted ability' do
api_put '/api/v2/admin/whitelisted_smart_contracts', params: { id: WhitelistedSmartContract.last.id + 1, status: 'disabled' }, token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
context 'rails validations' do
let!(:addresses_1) { create(:whitelisted_smart_contract, :address_1) }
let!(:addresses_2) { create(:whitelisted_smart_contract, :address_2) }
it 'returns error' do
api_put '/api/v2/admin/whitelisted_smart_contracts', token: token, params: { id: WhitelistedSmartContract.first.id, address: addresses_2.address }
expect(response).to include_api_error 'Address has already been taken'
end
end
end
end

View File

@@ -0,0 +1,248 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::WithdrawLimits, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
describe 'GET /withdraw_limits' do
before do
create(:withdraw_limit, limit_24_hour: 100, limit_1_month: 1000, kyc_level: 2, group: 'vip-0')
create(:withdraw_limit, limit_24_hour: 50, limit_1_month: 500, kyc_level: 1, group: 'vip-0')
create(:withdraw_limit, limit_24_hour: 50, limit_1_month: 500, kyc_level: 1, group: :any)
end
it 'returns all withdraw limits' do
api_get '/api/v2/admin/withdraw_limits', token: token
expect(response.status).to eq 200
expect(JSON.parse(response.body).length).to eq WithdrawLimit.count
end
it 'pagination' do
api_get '/api/v2/admin/withdraw_limits', token: token, params: { limit: 1 }
expect(JSON.parse(response.body).length).to eq 1
end
it 'filters by group' do
api_get '/api/v2/admin/withdraw_limits', token: token, params: { group: 'vip-0' }
result = JSON.parse(response.body)
expect(result.map { |r| r['group'] }).to all eq 'vip-0'
expect(result.length).to eq WithdrawLimit.where(group: 'vip-0').count
end
it 'filters by kyc_level' do
api_get '/api/v2/admin/withdraw_limits', token: token, params: { kyc_level: '1' }
result = JSON.parse(response.body)
expect(result.map { |r| r['kyc_level'] }).to all eq '1'
expect(result.length).to eq WithdrawLimit.where(kyc_level: '1').count
end
it 'capitalized group' do
api_get '/api/v2/admin/withdraw_limits', token: token, params: { group: 'Vip-0' }
result = JSON.parse(response.body)
expect(result.map { |r| r['group'] }).to all eq 'vip-0'
expect(result.length).to eq WithdrawLimit.where(group: 'vip-0').count
end
end
describe 'POST /withdraw_limits' do
it 'creates a table with default group' do
api_post '/api/v2/admin/withdraw_limits', token: token, params: { kyc_level: 1, limit_24_hour: 100, limit_1_month: 1000 }
expect(response).to be_successful
expect(JSON.parse(response.body)['limit_24_hour']).to eq('100.0')
expect(JSON.parse(response.body)['limit_1_month']).to eq('1000.0')
expect(JSON.parse(response.body)['group']).to eq('any')
expect(JSON.parse(response.body)['kyc_level']).to eq('1')
end
it 'creates a table with default kyc_level' do
api_post '/api/v2/admin/withdraw_limits', token: token, params: { group: 'vip-1', limit_24_hour: 100, limit_1_month: 1000 }
expect(response).to be_successful
expect(JSON.parse(response.body)['limit_24_hour']).to eq('100.0')
expect(JSON.parse(response.body)['limit_1_month']).to eq('1000.0')
expect(JSON.parse(response.body)['group']).to eq('vip-1')
expect(JSON.parse(response.body)['kyc_level']).to eq('any')
end
it 'returns created withdraw limit table' do
api_post '/api/v2/admin/withdraw_limits', token: token, params: { kyc_level: 4, group: 'vip-1', limit_24_hour: 100, limit_1_month: 1000 }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['limit_24_hour']).to eq('100.0')
expect(JSON.parse(response.body)['limit_1_month']).to eq('1000.0')
expect(JSON.parse(response.body)['group']).to eq('vip-1')
expect(JSON.parse(response.body)['kyc_level']).to eq('4')
end
context 'returns created withdraw limit table without group' do
it 'returns created withdraw limit table' do
api_post '/api/v2/admin/withdraw_limits', token: token, params: { kyc_level: 1, limit_24_hour: 100, limit_1_month: 1000 }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['limit_24_hour']).to eq('100.0')
expect(JSON.parse(response.body)['limit_1_month']).to eq('1000.0')
expect(JSON.parse(response.body)['group']).to eq('any')
expect(JSON.parse(response.body)['kyc_level']).to eq('1')
end
end
context 'returns created withdraw limit table without kyc_level' do
it 'returns created withdraw limit table' do
api_post '/api/v2/admin/withdraw_limits', token: token, params: { limit_24_hour: 100, limit_1_month: 1000, group: 'vip-1' }
expect(response).to have_http_status(201)
expect(JSON.parse(response.body)['limit_24_hour']).to eq('100.0')
expect(JSON.parse(response.body)['limit_1_month']).to eq('1000.0')
expect(JSON.parse(response.body)['group']).to eq('vip-1')
expect(JSON.parse(response.body)['kyc_level']).to eq('any')
end
end
context 'empty limit_24_hour field' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/withdraw_limits', token: token, params: { limit_1_month: 1000, group: 'vip-1' }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.withdraw_limit.invalid_limit_24_hour')
end
end
context 'empty limit_1_month field' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/withdraw_limits', token: token, params: { limit_24_hour: 1000, group: 'vip-1' }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.withdraw_limit.invalid_limit_1_month')
end
end
context 'invalid limit_24_hour/limit_1_month value' do
it 'returns status 422 and error' do
api_post '/api/v2/admin/withdraw_limits', token: token, params: { limit_1_month: -1, limit_24_hour: -15, group: 'vip-1' }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.withdraw_limit.invalid_limit_24_hour')
expect(response).to include_api_error('admin.withdraw_limit.invalid_limit_1_month')
end
end
end
describe 'PUT /withdraw_limits' do
it 'returns updated withdraw limit table with new kyc_level' do
api_put '/api/v2/admin/withdraw_limits', token: token, params: { kyc_level: '3', id: WithdrawLimit.first.id }
expect(response).to have_http_status(200)
expect(JSON.parse(response.body)['limit_24_hour']).to eq('9999.0')
expect(JSON.parse(response.body)['limit_1_month']).to eq('999999.0')
expect(JSON.parse(response.body)['group']).to eq('any')
expect(JSON.parse(response.body)['kyc_level']).to eq('3')
end
it 'returns updated withdraw limit table with new group' do
api_put '/api/v2/admin/withdraw_limits', token: token, params: { group: 'vip-1', id: WithdrawLimit.first.id }
expect(response).to have_http_status(200)
expect(JSON.parse(response.body)['limit_24_hour']).to eq('9999.0')
expect(JSON.parse(response.body)['limit_1_month']).to eq('999999.0')
expect(JSON.parse(response.body)['group']).to eq('vip-1')
expect(JSON.parse(response.body)['kyc_level']).to eq('any')
end
it 'returns updated withdraw limit table with new group with capitalized letter' do
api_put '/api/v2/admin/withdraw_limits', token: token, params: { group: 'Vip-1 ', id: WithdrawLimit.first.id }
expect(response).to have_http_status(200)
expect(JSON.parse(response.body)['limit_24_hour']).to eq('9999.0')
expect(JSON.parse(response.body)['limit_1_month']).to eq('999999.0')
expect(JSON.parse(response.body)['group']).to eq('vip-1')
expect(JSON.parse(response.body)['kyc_level']).to eq('any')
end
it 'returns updated withdraw limit table with new limit_24_hour' do
api_put '/api/v2/admin/withdraw_limits', token: token, params: { id: WithdrawLimit.first.id }
expect(response).to have_http_status(200)
expect(JSON.parse(response.body)['limit_24_hour']).to eq('9999.0')
expect(JSON.parse(response.body)['limit_1_month']).to eq('999999.0')
expect(JSON.parse(response.body)['group']).to eq('any')
expect(JSON.parse(response.body)['kyc_level']).to eq('any')
end
it 'returns updated withdraw limit table with new limit_24_hour, limit_1_month fields' do
api_put '/api/v2/admin/withdraw_limits', token: token, params: { limit_24_hour: 10, limit_1_month: 100, id: WithdrawLimit.first.id }
expect(response).to have_http_status(200)
expect(JSON.parse(response.body)['limit_24_hour']).to eq('10.0')
expect(JSON.parse(response.body)['limit_1_month']).to eq('100.0')
expect(JSON.parse(response.body)['group']).to eq('any')
expect(JSON.parse(response.body)['kyc_level']).to eq('any')
end
context 'not found withdraw_limit table' do
it 'returns status 404 and error' do
api_put '/api/v2/admin/withdraw_limits', token: token, params: { id: WithdrawLimit.last.id + 1 }
expect(response).to have_http_status(404)
expect(response).to include_api_error('record.not_found')
end
end
context 'empty limit_24_hour type' do
it 'returns status 422 and error' do
api_put '/api/v2/admin/withdraw_limits', token: token, params: { limit_24_hour: -1, id: WithdrawLimit.first.id }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.withdraw_limit.invalid_limit_24_hour')
end
end
context 'empty limit_1_month type' do
it 'returns status 422 and error' do
api_put '/api/v2/admin/withdraw_limits', token: token, params: { limit_1_month: -1, id: WithdrawLimit.first.id }
expect(response).to have_http_status(422)
expect(response).to include_api_error('admin.withdraw_limit.invalid_limit_1_month')
end
end
end
describe 'DELETE /withdraw_limits' do
let!(:withdraw_limit) { create(:withdraw_limit, kyc_level: 1) }
it 'id has invalid type' do
api_delete '/api/v2/admin/withdraw_limits/id', token: token
expect(response).to include_api_error 'admin.withdraw_limit.non_integer_id'
end
it 'deletes withdraw limit table' do
expect {
api_delete "/api/v2/admin/withdraw_limits/#{withdraw_limit.id}", token: token
}.to change { WithdrawLimit.count }.by(-1)
expect(response).to have_http_status(200)
end
it 'returns deleted withdraw limit table' do
api_delete "/api/v2/admin/withdraw_limits/#{withdraw_limit.id}", token: token
expect(JSON.parse(response.body)['id']).to eq withdraw_limit.id
end
it 'retuns 404 if record does not exist' do
expect {
api_delete "/api/v2/admin/withdraw_limits/#{WithdrawLimit.last.id + 42}", token: token
}.not_to change { WithdrawLimit.count }
expect(response.status).to eq 404
end
end
end

View File

@@ -0,0 +1,323 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Admin::Withdraws, type: :request do
let(:admin) { create(:member, :admin, :level_3, email: 'example@gmail.com', uid: 'ID73BF61C8H0') }
let(:token) { jwt_for(admin) }
let(:level_3_member) { create(:member, :level_3) }
let(:level_3_member_token) { jwt_for(level_3_member) }
before do
[admin, level_3_member].each do |member|
member.touch_accounts
member.accounts.map { |a| a.update(balance: 500) }
end
create(:usd_withdraw, amount: 10.0, sum: 10.0, member: admin)
create(:usd_withdraw, amount: 9.0, sum: 9.0, member: admin)
create(:usd_withdraw, amount: 100.0, sum: 100.0, member: level_3_member)
create(:btc_withdraw, amount: 42.0, sum: 42.0, txid: 'special_txid', member: admin)
create(:btc_withdraw, amount: 42.0, sum: 42.0, member: admin, aasm_state: :accepted)
create(:btc_withdraw, amount: 11.0, sum: 11.0, member: level_3_member, aasm_state: :skipped)
create(:btc_withdraw, amount: 12.0, sum: 12.0, member: level_3_member, aasm_state: :errored)
end
describe 'GET /api/v2/admin/withdraws' do
let(:url) { '/api/v2/admin/withdraws' }
it 'get all withdraws' do
api_get url, token: token
actual = JSON.parse(response.body)
expected = Withdraw.all
expect(actual.length).to eq expected.length
expect(actual.map { |a| a['state'] }).to match_array expected.map(&:aasm_state)
expect(actual.map { |a| a['id'] }).to match_array expected.map(&:id)
expect(actual.map { |a| a['currency'] }).to match_array expected.map(&:currency_id)
expect(actual.map { |a| a['member'] }).to match_array expected.map(&:member_id)
expect(actual.map { |a| a['type'] }).to match_array(expected.map { |d| d.currency.coin? ? 'coin' : 'fiat' })
expect(actual.map { |a| a['uid'] }).to match_array(expected.map { |d| d.member.uid })
expect(actual.map { |a| a['email'] }).to match_array(expected.map { |d| d.member.email })
end
context 'ordering' do
it 'ascending by id' do
api_get url, token: token, params: { order_by: 'id', ordering: 'asc' }
actual = JSON.parse(response.body)
expected = Withdraw.order(id: 'asc')
expect(actual.map { |a| a['id'] }).to eq expected.map(&:id)
end
it 'descending by sum' do
api_get url, token: token, params: { order_by: 'sum', ordering: 'desc' }
actual = JSON.parse(response.body)
expected = Withdraw.order(sum: 'desc')
expect(actual.map { |a| a['id'] }).to eq expected.map(&:id)
end
end
context 'filtering' do
it 'by member' do
api_get url, token: token, params: { uid: level_3_member.uid }
actual = JSON.parse(response.body)
expected = Withdraw.where(member_id: level_3_member.id)
expect(actual.length).to eq expected.length
expect(actual.map { |a| a['state'] }).to match_array expected.map(&:aasm_state)
expect(actual.map { |a| a['id'] }).to match_array expected.map(&:id)
expect(actual.map { |a| a['currency'] }).to match_array expected.map(&:currency_id)
expect(actual.map { |a| a['member'] }).to all eq level_3_member.id
expect(actual.map { |a| a['type'] }).to match_array(expected.map { |d| d.currency.coin? ? 'coin' : 'fiat' })
expect(actual.map { |a| a['uid'] }).to match_array(expected.map { |d| d.member.uid })
expect(actual.map { |a| a['email'] }).to match_array(expected.map { |d| d.member.email })
end
it 'by state' do
api_get url, token: token, params: { state: :skipped }
actual = JSON.parse(response.body)
expected = Withdraw.where(aasm_state: :skipped)
expect(actual.map { |a| a['state'] }).to all eq 'skipped'
expect(actual.length).to eq expected.count
expect(actual.map { |a| a['id'] }).to match_array expected.map(&:id)
expect(actual.map { |a| a['uid'] }).to match_array(expected.map { |d| d.member.uid })
end
it 'by multiple states' do
api_get url, token: token, params: { state: [:skipped, :accepted] }
actual = JSON.parse(response.body)
expected = Withdraw.where(aasm_state: [:skipped, :accepted])
expect(actual.map { |a| a['state'] }.uniq).to match_array %w[skipped accepted]
expect(actual.length).to eq expected.count
expect(actual.map { |a| a['id'] }).to match_array expected.map(&:id)
expect(actual.map { |a| a['uid'] }).to match_array(expected.map { |d| d.member.uid })
end
it 'by type' do
api_get url, token: token, params: { type: 'coin' }
actual = JSON.parse(response.body)
expected = Withdraw.where(type: 'Withdraws::Coin')
expect(actual.length).to eq expected.length
expect(actual.map { |a| a['state'] }).to match_array expected.map(&:aasm_state)
expect(actual.map { |a| a['id'] }).to match_array expected.map(&:id)
expect(actual.map { |a| a['currency'] }).to match_array expected.map(&:currency_id)
expect(actual.map { |a| a['member'] }).to match_array expected.map(&:member_id)
expect(actual.map { |a| a['type'] }).to all eq 'coin'
end
it 'by txid' do
api_get url, token: token, params: { txid: Withdraw.where(type: 'Withdraws::Coin').first.txid }
actual = JSON.parse(response.body)
expected = Withdraw.where(type: 'Withdraws::Coin').first
expect(actual.length).to eq 1
expect(actual.first['state']).to eq expected.aasm_state
expect(actual.first['id']).to eq expected.id
expect(actual.first['currency']).to eq expected.currency_id
expect(actual.first['member']).to eq expected.member_id
expect(actual.first['type']).to eq 'coin'
end
it 'by wallet_type' do
wallet_type = Wallet.joins(:currencies)
.find_by(currencies: { id: Withdraw.find_by(type: 'Withdraws::Coin').currency_id })
.gateway
api_get url, token: token, params: { wallet_type: wallet_type }
actual = JSON.parse(response.body)
expect(actual.length).to eq Withdraw.where(currency: Currency.joins(:wallets)
.where(wallets: { id: Wallet.where(gateway: wallet_type) })).count
end
end
end
describe 'GET /api/v2/admin/withdraws/:id' do
context 'invalid params' do
context 'non-integer id' do
it do
api_get '/api/v2/admin/withdraws/id', token: token
expect(response).to include_api_error('admin.withdraw.non_integer_id')
end
end
context 'withdraw does not exist' do
it do
api_get "/api/v2/admin/withdraws/#{Withdraw.last.id + 1}", token: token
expect(response).to include_api_error('record.not_found')
end
end
end
context 'with beneficiary' do
context 'has beneficiary' do
let!(:withdraw) { create(:usd_withdraw, :with_beneficiary, :with_deposit_liability) }
it 'includes beneficiary in withdrawal payload' do
beneficiary_json = API::V2::Entities::Beneficiary
.represent(withdraw.beneficiary)
.as_json
.deep_stringify_keys
api_get "/api/v2/admin/withdraws/#{withdraw.id}", token: token
expect(response_body['beneficiary']).to_not be_nil
expect(response_body['beneficiary']).to eq(beneficiary_json)
end
end
context 'does not have beneficiary' do
let!(:withdraw) { create(:usd_withdraw, :with_deposit_liability) }
it 'includes beneficiary in withdrawal payload' do
api_get "/api/v2/admin/withdraws/#{withdraw.id}", token: token
expect(response_body['beneficiary']).to be_nil
end
end
end
context 'with error message' do
context 'does not have error message' do
let!(:withdraw) { create(:usd_withdraw, :with_beneficiary, :with_deposit_liability, aasm_state: :succeed) }
it 'without error message in withdrawal payload' do
api_get "/api/v2/admin/withdraws/#{withdraw.id}", token: token
expect(response_body.include?('error')).to be_falsey
end
end
context 'includes error message' do
let!(:withdraw) { create(:usd_withdraw, :with_beneficiary, :with_deposit_liability, aasm_state: :skipped) }
it 'includes error message in withdrawal payload' do
api_get "/api/v2/admin/withdraws/#{withdraw.id}", token: token
expect(response_body.include?('error')).to be_truthy
end
end
end
end
describe 'PUT /api/v2/admin/withdraws' do
let(:url) { '/api/v2/admin/withdraws' }
let(:fiat) { Withdraw.where(type: 'Withdraws::Fiat').first }
let(:coin) { Withdraw.where(type: 'Withdraws::Coin').first }
context 'updates withdraw' do
it 'updates empty metadata' do
api_put url, token: token, params: { metadata: { info: :some }, id: coin.id }
expect(response_body['metadata']).to eq({'info' => 'some'})
end
it 'updates existing metadata' do
coin.update!(metadata: { data: :data })
api_put url, token: token, params: { metadata: { info: :some }, id: coin.id }
expect(response_body['metadata']).to eq({'data' => 'data', 'info' => 'some'})
end
it 'updates existing metadata' do
coin.update!(metadata: { info: :some })
api_put url, token: token, params: { metadata: { info: :some1 }, id: coin.id }
expect(response_body['metadata']).to eq({ 'info' => 'some1' })
end
end
end
describe 'POST /api/v2/admin/withdraws/actions' do
let(:url) { '/api/v2/admin/withdraws/actions' }
let(:fiat) { Withdraw.where(type: 'Withdraws::Fiat').first }
let(:coin) { Withdraw.where(type: 'Withdraws::Coin').first }
context 'validates params' do
it 'does not pass unsupported action' do
api_post url, token: token, params: { action: 'illegal', id: fiat.id }
expect(response.status).to eq 422
expect(response).to include_api_error('admin.withdraw.invalid_action')
end
it 'passes supported action for coin' do
api_post url, token: token, params: { action: 'process', id: coin.id }
expect(response).not_to include_api_error('admin.withdraw.invalid_action')
end
it 'passes supported action for fiat' do
api_post url, token: token, params: { action: 'reject', id: fiat.id }
expect(response).not_to include_api_error('admin.withdraw.invalid_action')
end
it 'does not pass coin action for fiat' do
api_post url, token: token, params: { action: 'load', id: fiat.id }
expect(response.status).to eq 422
expect(response).to include_api_error('admin.withdraw.cannot_load')
end
end
context 'updates withdraw' do
before { [coin, fiat].map(&:accept!) }
it 'process coin' do
api_post url, token: token, params: { action: 'process', id: coin.id }
expect(coin.reload.aasm_state).to eq('processing')
end
it 'reject fiat' do
api_post url, token: token, params: { action: 'reject', id: fiat.id }
expect(fiat.reload.aasm_state).to eq('rejected')
expect(response).to be_successful
end
it 'fail coin' do
coin.accept!
coin.process!
api_post url, token: token, params: { action: 'fail', id: coin.id }
expect(coin.reload.aasm_state).to eq('failed')
expect(response).to be_successful
end
it 'load coin with txid' do
BlockchainService.any_instance.expects(:fetch_transaction).once.returns(Peatio::Transaction.new)
coin.accept!
api_post url, token: token, params: { action: 'load', id: coin.id, txid: 'new_txid' }
expect(coin.reload.txid).to eq('new_txid')
expect(coin.aasm_state).to eq('confirming')
expect(response).to be_successful
end
it 'load fiat with txid' do
fiat.accept!
expect {
api_post url, token: token, params: { action: 'load', id: fiat.id, txid: 'new_txid' }
}.not_to change { fiat }
expect(response).to include_api_error('admin.withdraw.redundant_txid')
end
it 'load coin without txid with txid as param' do
BlockchainService.any_instance.expects(:fetch_transaction).once.returns(Peatio::Transaction.new)
coin.update(txid: nil)
coin.accept!
api_post url, token: token, params: { action: 'load', id: coin.id, txid: 'new_txid' }
expect(coin.reload.txid).to eq('new_txid')
expect(coin.aasm_state).to eq('confirming')
expect(response).to be_successful
end
it 'load coin without txid' do
coin.update(txid: nil)
coin.accept!
expect {
api_post url, token: token, params: { action: 'load', id: coin.id }
}.not_to change { coin }
expect(response).to include_api_error('admin.withdraw.cannot_load')
end
end
end
end

View File

@@ -0,0 +1,247 @@
# frozen_string_literal: true
describe API::V2::CoinGecko::HistoricalTrades, type: :request do
describe 'GET /api/v2/coingecko/historical_trades' do
before(:each) { delete_measurments('trades') }
after(:each) { delete_measurments('trades') }
context 'there is no market pair' do
it 'should return error' do
get '/api/v2/coingecko/historical_trades', params: { ticker_id: 'TEST_TEST' }
expect(response).to have_http_status 404
expect(response).to include_api_error('record.not_found')
end
end
context 'there is no trades in influx' do
let(:expected_response) do
{
'buy' => [],
'sell' => []
}
end
it 'should return recent trades' do
get '/api/v2/coingecko/historical_trades', params: { ticker_id: 'BTC_USD' }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result).to eq expected_response
end
end
context 'there are trades in influx' do
let!(:trade1) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d, created_at: Time.now) }
let!(:trade2) { create(:trade, :btcusd, price: '6.0'.to_d, amount: '0.9'.to_d, total: '5.4'.to_d, created_at: Time.now + 1.month) }
before do
trade1.write_to_influx
trade2.write_to_influx
end
context 'return all trades' do
let(:expected_response) do
{
'buy' => [
{'base_volume' => 0.9,
'price' => 6,
'target_volume' => 5.4,
'trade_id' => trade2.id,
'trade_timestamp' => trade2.created_at.to_i * 1000,
'type' => 'buy'},
{'base_volume' => 1.1,
'price' => 5,
'target_volume' => 5.5,
'trade_id' => trade1.id,
'trade_timestamp' => trade1.created_at.to_i * 1000,
'type' => 'buy'}
],
'sell' => []
}
end
it do
get '/api/v2/coingecko/historical_trades', params: { ticker_id: 'BTC_USD'}
expect(response).to be_successful
expect(response_body).to eq expected_response
end
end
context 'return filtered trades' do
let(:expected_response) do
{
'buy' => [
{'base_volume' => 0.9,
'price' => 6,
'target_volume' => 5.4,
'trade_id' => trade2.id,
'trade_timestamp' => trade2.created_at.to_i * 1000,
'type' => 'buy'},
{'base_volume' => 1.1,
'price' => 5,
'target_volume' => 5.5,
'trade_id' => trade1.id,
'trade_timestamp' => trade1.created_at.to_i * 1000,
'type' => 'buy'}
],
'sell' => []
}
end
context 'by taker_type' do
it 'buy' do
get '/api/v2/coingecko/historical_trades', params: { ticker_id: 'BTC_USD', type: 'buy'}
expect(response).to be_successful
expect(response_body).to eq expected_response
end
it 'sell' do
get '/api/v2/coingecko/historical_trades', params: { ticker_id: 'BTC_USD', type: 'sell'}
expect(response).to be_successful
expect(response_body).to eq({'buy'=> [], 'sell' => []})
end
end
context 'by start time' do
let(:expected_response) do
{
'buy' => [
{'base_volume' => 0.9,
'price' => 6,
'target_volume' => 5.4,
'trade_id' => trade2.id,
'trade_timestamp' => trade2.created_at.to_i * 1000,
'type' => 'buy'}
],
'sell' => []
}
end
it do
get '/api/v2/coingecko/historical_trades', params: { ticker_id: 'BTC_USD', start_time: Time.now + 15.days}
expect(response).to be_successful
expect(response_body).to eq expected_response
end
end
context 'by end time' do
let(:expected_response) do
{
'buy' => [
{'base_volume' => 1.1,
'price' => 5,
'target_volume' => 5.5,
'trade_id' => trade1.id,
'trade_timestamp' => trade1.created_at.to_i * 1000,
'type' => 'buy'}
],
'sell' => []
}
end
it do
get '/api/v2/coingecko/historical_trades', params: { ticker_id: 'BTC_USD', end_time: Time.now + 15.days}
expect(response).to be_successful
expect(response_body).to eq expected_response
end
end
context 'by limit' do
context 'without specified limit' do
let(:expected_response) do
{
'buy' => [
{'base_volume' => 0.9,
'price' => 6,
'target_volume' => 5.4,
'trade_id' => trade2.id,
'trade_timestamp' => trade2.created_at.to_i * 1000,
'type' => 'buy'},
{'base_volume' => 1.1,
'price' => 5,
'target_volume' => 5.5,
'trade_id' => trade1.id,
'trade_timestamp' => trade1.created_at.to_i * 1000,
'type' => 'buy'}
],
'sell' => []
}
end
it 'returns all trades' do
get '/api/v2/coingecko/historical_trades', params: { ticker_id: 'BTC_USD'}
expect(response).to be_successful
expect(response_body).to eq expected_response
end
end
context 'with specified limit' do
let(:expected_response) do
{
'buy' => [
{'base_volume' => 0.9,
'price' => 6,
'target_volume' => 5.4,
'trade_id' => trade2.id,
'trade_timestamp' => trade2.created_at.to_i * 1000,
'type' => 'buy'}
],
'sell' => []
}
end
it 'returns specific number of trades' do
get '/api/v2/coingecko/historical_trades', params: { ticker_id: 'BTC_USD', limit: 1}
expect(response).to be_successful
expect(response_body).to eq expected_response
end
end
context 'with limit==0' do
let(:expected_response) do
{
'buy' => [
{'base_volume' => 0.9,
'price' => 6,
'target_volume' => 5.4,
'trade_id' => trade2.id,
'trade_timestamp' => trade2.created_at.to_i * 1000,
'type' => 'buy'},
{'base_volume' => 1.1,
'price' => 5,
'target_volume' => 5.5,
'trade_id' => trade1.id,
'trade_timestamp' => trade1.created_at.to_i * 1000,
'type' => 'buy'}
],
'sell' => []
}
end
it 'returns all trades' do
get '/api/v2/coingecko/historical_trades', params: { ticker_id: 'BTC_USD', limit: 0}
expect(response).to be_successful
expect(response_body).to eq expected_response
end
end
end
end
end
end
end

View File

@@ -0,0 +1,98 @@
# frozen_string_literal: true
describe API::V2::CoinGecko::Orderbook, type: :request do
describe 'GET /api/v2/coingecko/orderbook' do
before do
create_list(:order_bid, 5, :btcusd)
create_list(:order_bid, 5, :btcusd, price: 2)
create_list(:order_ask, 5, :btcusd)
create_list(:order_ask, 5, :btcusd, price: 3)
end
let(:asks) { [["1.0", "5.0"], ["3.0", "5.0"]] }
let(:bids) { [["2.0", "5.0"], ["1.0", "5.0"]] }
context 'valid market param' do
it 'sorts asks and bids from highest to lowest' do
get "/api/v2/coingecko/orderbook", params: { ticker_id: "BTC_USD"}
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 2
expect(result['bids'].size).to eq 2
expect(result['asks']).to eq asks
expect(result['bids']).to eq bids
end
context 'with depth param' do
before do
create_list(:order_bid, 5, :btcusd)
create_list(:order_bid, 5, :btcusd, price: 4.1)
create_list(:order_ask, 5, :btcusd)
create_list(:order_ask, 5, :btcusd, price: 12.2)
end
it 'get asks and bids with depth param' do
get '/api/v2/coingecko/orderbook', params: { ticker_id: "BTC_USD", depth: 2 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 1
expect(result['bids'].size).to eq 1
end
it 'get asks and bids with depth param' do
get '/api/v2/coingecko/orderbook', params: { ticker_id: "BTC_USD", depth: 4 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 2
expect(result['bids'].size).to eq 2
end
it 'get asks and bids with depth param' do
get '/api/v2/coingecko/orderbook', params: { ticker_id: "BTC_USD", depth: 1 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 0
expect(result['bids'].size).to eq 0
end
it 'get asks and bids with depth param' do
get '/api/v2/coingecko/orderbook', params: { ticker_id: "BTC_USD", depth: 3 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 1
expect(result['bids'].size).to eq 1
end
it 'get asks and bids with depth param for all orderbook' do
get '/api/v2/coingecko/orderbook', params: { ticker_id: "BTC_USD", depth: 0 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 3
expect(result['bids'].size).to eq 3
end
context 'invalid depth params' do
it 'shoud return error' do
get '/api/v2/coingecko/orderbook', params: { ticker_id: "BTC_USD", depth: 'test' }
expect(response).to have_http_status 422
expect(response).to include_api_error('coingecko.market_depth.non_integer_depth')
end
it 'shoud return error' do
get '/api/v2/coingecko/orderbook', params: { ticker_id: "BTC_USD", depth: 2000 }
expect(response).to have_http_status 422
expect(response).to include_api_error('coingecko.market_depth.invalid_depth')
end
end
end
end
context 'invalid market param' do
it 'validates market param' do
get '/api/v2/coingecko/orderbook', params: { ticker_id: "usdusd" }
expect(response).to have_http_status 404
expect(response).to include_api_error('record.not_found')
end
end
end
end

View File

@@ -0,0 +1,42 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::CoinGecko::Pairs, type: :request do
describe 'GET /api/v2/coingecko/pairs' do
before(:each) { clear_redis }
let!(:market) do
::Market.enabled.ordered.sample
end
let!(:expected_response) {
{
"ticker_id" => market.underscore_name,
"base" => market[:base_unit].upcase,
"target" => market[:quote_unit].upcase
}
}
it 'lists visible currencies' do
get '/api/v2/coingecko/pairs'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Market.enabled.size
expect(result).to include(expected_response)
end
end
context 'There is no markets' do
before { DatabaseCleaner.clean }
it 'should return summary' do
get '/api/v2/coingecko/pairs'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result).to eq []
end
end
end

View File

@@ -0,0 +1,107 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::CoinGecko::Tickers, type: :request do
describe 'GET /api/v2/coingecko/tickers' do
before(:each) { delete_measurments('trades') }
after(:each) { delete_measurments('trades') }
before do
create_list(:order_bid, 5, :btcusd)
create_list(:order_ask, 5, :btcusd)
end
context 'no trades executed yet' do
let(:expected_btcusd_ticker) do
{
'ticker_id' => 'BTC_USD',
'base_currency' => 'BTC',
'target_currency' => 'USD',
'last_price' => '0.0',
'target_volume' => '0.0', 'base_volume' => '0.0',
'bid' => '1.0', 'ask' => '1.0',
'high' => '0.0', 'low' => '0.0'
}
end
let(:expected_btceth_ticker) do
{
'ticker_id' => 'BTC_ETH',
'base_currency' => 'BTC',
'target_currency' => 'ETH',
'last_price' => '0.0',
'target_volume' => '0.0', 'base_volume' => '0.0',
'bid' => '0.0', 'ask' => '0.0',
'high' => '0.0', 'low' => '0.0'
}
end
it 'returns tickers of all markets' do
get '/api/v2/coingecko/tickers'
expect(response).to be_successful
btc_usd_ticker = response_body.find {|ticker| ticker['ticker_id'] == 'BTC_USD'}
btc_eth_ticker = response_body.find {|ticker| ticker['ticker_id'] == 'BTC_ETH'}
expect(btc_usd_ticker).to eq expected_btcusd_ticker
expect(btc_eth_ticker).to eq expected_btceth_ticker
end
end
context 'single trade was executed' do
let!(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
let(:expected_ticker) do
{
'ticker_id' => 'BTC_USD',
'base_currency' => 'BTC',
'target_currency' => 'USD',
'last_price' => '5.0',
'target_volume' => '5.5', 'base_volume' => '1.1',
'bid' => '1.0', 'ask' => '1.0',
'high' => '5.0', 'low' => '5.0'
}
end
before do
trade.write_to_influx
end
it 'returns tickers of all markets' do
get '/api/v2/coingecko/tickers'
expect(response).to be_successful
btc_usd_ticker = response_body.find {|ticker| ticker['ticker_id'] == 'BTC_USD'}
expect(btc_usd_ticker).to eq expected_ticker
end
end
context 'multiple trades were executed' do
let!(:trade1) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
let!(:trade2) { create(:trade, :btcusd, price: '6.0'.to_d, amount: '0.9'.to_d, total: '5.4'.to_d)}
let(:expected_ticker) do
{
'ticker_id' => 'BTC_USD',
'base_currency' => 'BTC',
'target_currency' => 'USD',
'last_price' => '6.0',
'target_volume' => '10.9', 'base_volume' => '2.0',
'bid' => '1.0', 'ask' => '1.0',
'high' => '6.0', 'low' => '5.0'
}
end
before do
trade1.write_to_influx
trade2.write_to_influx
end
it 'returns tickers of all markets' do
get '/api/v2/coingecko/tickers'
expect(response).to be_successful
btc_usd_ticker = response_body.find {|ticker| ticker['ticker_id'] == 'BTC_USD'}
expect(btc_usd_ticker).to eq expected_ticker
end
end
end
end

View File

@@ -0,0 +1,113 @@
# frozen_string_literal: true
describe API::V2::CoinMarketCap::Assets, type: :request do
describe 'GET /api/v2/coinmarketcap/assets' do
before(:each) { clear_redis }
context 'There are currencies' do
context 'with unified id' do
before do
Currency.coins.each do |currency|
stub_request(:get, "https://pro-api.coinmarketcap.com/v1/cryptocurrency/map?CMC_PRO_API_KEY=UNIFIED-CRYPTOASSET-INDEX&"\
"listing_status=active&"\
"symbol=#{currency.id}")
.to_return(body:
{
'status'=>
{
'error_code'=>0,
'error_message'=>nil,
'elapsed'=>12,
'credit_count'=>1,
'notice'=>nil
},
'data'=>
[
{
'id'=>1
}
]
}.to_json)
end
end
it 'should return crypto assets' do
get '/api/v2/coinmarketcap/assets'
expect(response).to be_successful
expect(response_body['BTC'].keys).to match_array %w[name unified_cryptoasset_id can_withdraw can_deposit min_withdraw]
expect(response_body['BTC']['name']).to eq 'Bitcoin'
expect(response_body['BTC']['unified_cryptoasset_id']).to eq 1
expect(response_body['BTC']['can_withdraw']).to eq true
expect(response_body['BTC']['can_deposit']).to eq true
expect(response_body['BTC']['min_withdraw']).to eq '0.0'
end
end
context 'without unified id' do
before do
Currency.coins.each do |currency|
stub_request(:get, "https://pro-api.coinmarketcap.com/v1/cryptocurrency/map?CMC_PRO_API_KEY=UNIFIED-CRYPTOASSET-INDEX&"\
"listing_status=active&"\
"symbol=#{currency.id}")
.to_return(status: 400, body:
{
'status'=>
{
'timestamp'=>'2020-09-25T08:43:56.778Z',
'error_code'=>400,
'error_message'=>'Invalid value for \'symbol\': \'TESTTEST\'',
'elapsed'=>0,
'credit_count'=>0,
'notice'=>nil
}
}.to_json)
end
end
it 'should return crypto assets' do
get '/api/v2/coinmarketcap/assets'
expect(response).to be_successful
expect(response_body['BTC'].keys).to match_array %w[name can_withdraw can_deposit min_withdraw]
expect(response_body['BTC']['name']).to eq 'Bitcoin'
expect(response_body['BTC']['can_withdraw']).to eq true
expect(response_body['BTC']['can_deposit']).to eq true
expect(response_body['BTC']['min_withdraw']).to eq '0.0'
end
context 'with 500 error from Faraday' do
before do
Currency.coins.each do |currency|
stub_request(:get, "https://pro-api.coinmarketcap.com/v1/cryptocurrency/map?CMC_PRO_API_KEY=UNIFIED-CRYPTOASSET-INDEX&"\
"listing_status=active&"\
"symbol=#{currency.id}")
.to_raise(Faraday::Error)
end
end
it 'should return crypto assets' do
get '/api/v2/coinmarketcap/assets'
expect(response).to be_successful
expect(response_body['BTC'].keys).to match_array %w[name can_withdraw can_deposit min_withdraw]
expect(response_body['BTC']['name']).to eq 'Bitcoin'
expect(response_body['BTC']['can_withdraw']).to eq true
expect(response_body['BTC']['can_deposit']).to eq true
expect(response_body['BTC']['min_withdraw']).to eq '0.0'
end
end
end
end
context 'There is no currencies' do
before { DatabaseCleaner.clean }
it 'should return assets' do
get '/api/v2/coinmarketcap/assets'
expect(response).to be_successful
expect(response_body).to eq({})
end
end
end
end

View File

@@ -0,0 +1,98 @@
# frozen_string_literal: true
describe API::V2::CoinMarketCap::Orderbook, type: :request do
describe 'GET /api/v2/coinmarketcap/orderbook/:market_pair' do
before do
create_list(:order_bid, 5, :btcusd)
create_list(:order_bid, 5, :btcusd, price: 2)
create_list(:order_ask, 5, :btcusd)
create_list(:order_ask, 5, :btcusd, price: 3)
end
let(:asks) { [["1.0", "5.0"], ["3.0", "5.0"]] }
let(:bids) { [["2.0", "5.0"], ["1.0", "5.0"]] }
context 'valid market param' do
it 'sorts asks and bids from highest to lowest' do
get "/api/v2/coinmarketcap/orderbook/BTC_USD"
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 2
expect(result['bids'].size).to eq 2
expect(result['asks']).to eq asks
expect(result['bids']).to eq bids
end
context 'with depth param' do
before do
create_list(:order_bid, 5, :btcusd)
create_list(:order_bid, 5, :btcusd, price: 4.1)
create_list(:order_ask, 5, :btcusd)
create_list(:order_ask, 5, :btcusd, price: 12.2)
end
it 'get asks and bids with depth param' do
get '/api/v2/coinmarketcap/orderbook/BTC_USD', params: { depth: 2 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 1
expect(result['bids'].size).to eq 1
end
it 'get asks and bids with depth param' do
get '/api/v2/coinmarketcap/orderbook/BTC_USD', params: { depth: 4 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 2
expect(result['bids'].size).to eq 2
end
it 'get asks and bids with depth param' do
get '/api/v2/coinmarketcap/orderbook/BTC_USD', params: { depth: 1 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 0
expect(result['bids'].size).to eq 0
end
it 'get asks and bids with depth param' do
get '/api/v2/coinmarketcap/orderbook/BTC_USD', params: { depth: 3 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 1
expect(result['bids'].size).to eq 1
end
it 'get asks and bids with depth param for all orderbook' do
get '/api/v2/coinmarketcap/orderbook/BTC_USD', params: { depth: 0 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 3
expect(result['bids'].size).to eq 3
end
context 'invalid depth params' do
it 'shoud return error' do
get '/api/v2/coinmarketcap/orderbook/BTC_USD', params: { depth: 'test' }
expect(response).to have_http_status 422
expect(response).to include_api_error('coinmarketcap.market_depth.non_integer_depth')
end
it 'shoud return error' do
get '/api/v2/coinmarketcap/orderbook/BTC_USD', params: { depth: 2000 }
expect(response).to have_http_status 422
expect(response).to include_api_error('coinmarketcap.market_depth.invalid_depth')
end
end
end
end
context 'invalid market param' do
it 'validates market param' do
api_get "/api/v2/coinmarketcap/orderbook/usdusd"
expect(response).to have_http_status 404
expect(response).to include_api_error('record.not_found')
end
end
end
end

View File

@@ -0,0 +1,70 @@
# frozen_string_literal: true
describe API::V2::CoinMarketCap::Summary, type: :request do
describe 'GET /api/v2/coinmarketcap/summary' do
before(:each) { clear_redis }
after(:each) { delete_measurments('trades') }
context 'There is no trades in influx' do
it 'should return summary' do
get '/api/v2/coinmarketcap/summary'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.count).to eq Market.all.count
expect(result.first['trading_pairs']).to eq 'BTC_USD'
expect(result.first['base_currency']).to eq 'BTC'
expect(result.first['quote_currency']).to eq 'USD'
expect(result.first['last_price']).to eq '0.0'
expect(result.first['lowest_ask']).to eq '0.0'
expect(result.first['highest_bid']).to eq '0.0'
expect(result.first['base_volume']).to eq '0.0'
expect(result.first['quote_volume']).to eq '0.0'
expect(result.first['price_change_percent_24h']).to eq '0.0'
expect(result.first['highest_price_24h']).to eq '0.0'
expect(result.first['lowest_price_24h']).to eq '0.0'
end
end
context 'There are trades in influx' do
let!(:trade1) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
let!(:trade2) { create(:trade, :btcusd, price: '6.0'.to_d, amount: '0.9'.to_d, total: '5.4'.to_d)}
before do
trade1.write_to_influx
trade2.write_to_influx
end
it 'should return summary' do
get '/api/v2/coinmarketcap/summary'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.count).to eq Market.all.count
expect(result.first['trading_pairs']).to eq 'BTC_USD'
expect(result.first['base_currency']).to eq 'BTC'
expect(result.first['quote_currency']).to eq 'USD'
expect(result.first['last_price']).to eq '6.0'
expect(result.first['lowest_ask']).to eq '1.0'
expect(result.first['highest_bid']).to eq '1.0'
expect(result.first['base_volume']).to eq '2.0'
expect(result.first['quote_volume']).to eq '10.9'
expect(result.first['price_change_percent_24h']).to eq '0.2'
expect(result.first['highest_price_24h']).to eq '6.0'
expect(result.first['lowest_price_24h']).to eq '5.0'
end
end
context 'There is no markets' do
before { DatabaseCleaner.clean }
it 'should return summary' do
get '/api/v2/coinmarketcap/summary'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result).to eq []
end
end
end
end

View File

@@ -0,0 +1,127 @@
# frozen_string_literal: true
describe API::V2::CoinMarketCap::Ticker, type: :request do
describe 'GET /api/v2/coinmarketcap/ticker' do
before do
create_list(:order_bid, 5, :btcusd)
create_list(:order_ask, 5, :btcusd)
end
before(:each) { clear_redis }
context 'with unified id' do
before(:each) { delete_measurments('trades') }
after(:each) { delete_measurments('trades') }
before do
Currency.ordered.coins.each.with_index(1) do |currency, index|
stub_request(:get, "https://pro-api.coinmarketcap.com/v1/cryptocurrency/map?CMC_PRO_API_KEY=UNIFIED-CRYPTOASSET-INDEX&"\
"listing_status=active&"\
"symbol=#{currency.id}")
.to_return(body:
{
'status'=>
{
'error_code'=>0,
'error_message'=>nil,
'elapsed'=>12,
'credit_count'=>1,
'notice'=>nil
},
'data'=>
[
{
'id'=>index
}
]
}.to_json)
end
end
context 'no trades executed yet' do
let(:expected_btc_usd_ticker) do
{
'base_id' => 1, 'last_price' => '0.0',
'quote_volume' => '0.0', 'base_volume' => '0.0',
'isFrozen' => 0 }
end
let(:expected_btc_eth_ticker) do
{
'base_id' => 1, 'quote_id' => 2, 'last_price' => '0.0',
'quote_volume' => '0.0', 'base_volume' => '0.0',
'isFrozen' => 0 }
end
it 'returns ticker of all markets' do
get '/api/v2/coinmarketcap/ticker'
expect(response).to be_successful
# crypto/fiat market
expect(response_body['BTC_USD'].keys).to match_array %w[base_id last_price base_volume quote_volume isFrozen]
expect(response_body['BTC_USD']).to include(expected_btc_usd_ticker)
# crypto/crypto market
expect(response_body['BTC_ETH'].keys).to match_array %w[base_id quote_id last_price base_volume quote_volume isFrozen]
expect(response_body['BTC_ETH']).to include(expected_btc_eth_ticker)
end
end
context 'single trade was executed' do
let!(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
let(:expected_btc_usd_ticker) do
{
'base_id' => 1,
'last_price' => '5.0', 'quote_volume' => '5.5',
'base_volume' => '1.1', 'isFrozen' => 0
}
end
let(:expected_btc_usd_frozen_ticker) do
{
'base_id' => 1, 'last_price' => '5.0',
'quote_volume' => '5.5', 'base_volume' => '1.1', 'isFrozen' => 1
}
end
before do
trade.write_to_influx
end
it 'returns market tickers' do
get '/api/v2/coinmarketcap/ticker'
expect(response).to be_successful
# crypto/fiat market
expect(response_body['BTC_USD'].keys).to match_array %w[base_id last_price base_volume quote_volume isFrozen]
expect(response_body['BTC_USD']).to include(expected_btc_usd_ticker)
end
end
context 'multiple trades were executed' do
let!(:trade1) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
let!(:trade2) { create(:trade, :btcusd, price: '6.0'.to_d, amount: '0.9'.to_d, total: '5.4'.to_d)}
let(:expected_btc_usd_ticker) do
{ 'base_id' => 1, 'last_price' => '6.0',
'quote_volume' => '10.9', 'base_volume' => '2.0',
'isFrozen' => 0 }
end
before do
trade1.write_to_influx
trade2.write_to_influx
end
it 'returns market tickers' do
get '/api/v2/coinmarketcap/ticker'
expect(response).to be_successful
# crypto/fiat market
expect(response_body['BTC_USD'].keys).to match_array %w[base_id last_price base_volume quote_volume isFrozen]
expect(response_body['BTC_USD']).to include(expected_btc_usd_ticker)
end
end
end
end
end

View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true
describe API::V2::CoinMarketCap::Trades, type: :request do
describe 'GET /api/v2/coinmarketcap/trades/:market_pair' do
before(:each) { delete_measurments('trades') }
after(:each) { delete_measurments('trades') }
context 'there is no market pair' do
it 'should return error' do
get '/api/v2/coinmarketcap/trades/TEST_TEST'
expect(response).to have_http_status 404
expect(response).to include_api_error('record.not_found')
end
end
context 'there is no trades in influx' do
it 'should return recent trades' do
get '/api/v2/coinmarketcap/trades/BTC_USD'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result).to eq []
end
end
context 'there are trades in influx' do
let!(:trade1) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
let!(:trade2) { create(:trade, :btcusd, price: '6.0'.to_d, amount: '0.9'.to_d, total: '5.4'.to_d)}
before do
trade1.write_to_influx
trade2.write_to_influx
end
it 'should return recent trades' do
get '/api/v2/coinmarketcap/trades/BTC_USD'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.count).to eq 2
expect(result.first.keys).to match_array %w[trade_id price base_volume quote_volume timestamp type]
expect(result.first['trade_id']).to eq trade2.id
expect(result.first['price']).to eq trade2.price
expect(result.first['base_volume']).to eq trade2.amount
expect(result.first['quote_volume']).to eq trade2.total
expect(result.first['type']).to eq trade2.taker_type
end
end
end
end

View File

@@ -0,0 +1,133 @@
# encoding: UTF-8
# frozen_string_literal: true
require 'rack/cors'
describe Rack::Cors, type: :request do
let(:member) { create(:member, :level_3) }
let(:frontend_url) { 'https://frontend.io' }
let(:local_url) { 'http://localhost:3000' }
let(:token) { jwt_for(member) }
let(:app) {
Rack::Builder.new do
use Rack::Cors do
allow do
origins CORS::Validations.validate_origins(ENV['API_CORS_ORIGINS'])
resource '/api/*',
methods: %i[get post delete put patch options head],
headers: :any,
credentials: ENV.true?('API_CORS_ALLOW_CREDENTIALS'),
max_age: CORS::Validations.validate_max_age(ENV['API_CORS_MAX_AGE'])
end
end
run Peatio::Application
end
}
def check_cors(response, origin, allow_crendentails, max_age = '3600')
expect(response.headers['Access-Control-Allow-Origin']).to eq(origin)
expect(response.headers['Access-Control-Allow-Methods']).to eq('GET, POST, DELETE, PUT, PATCH, OPTIONS, HEAD')
expect(response.headers['Access-Control-Allow-Credentials']).to eq(allow_crendentails)
expect(response.headers['Access-Control-Max-Age']).to eq(max_age)
end
def without_cors(response)
expect(response.headers['Access-Control-Allow-Origin']).to eq(nil)
expect(response.headers['Access-Control-Allow-Methods']).to eq(nil)
expect(response.headers['Access-Control-Allow-Credentials']).to eq(nil)
expect(response.headers['Access-Control-Max-Age']).to eq(nil)
end
context 'set API_CORS_ORIGINS as "*"' do
let(:origin) { '*' }
let(:allow_crendentails) { nil }
let(:max_age) { '3600' }
before do
ENV['API_CORS_ORIGINS'] = origin
ENV['API_CORS_ALLOW_CREDENTIALS'] = allow_crendentails
ENV['API_CORS_MAX_AGE'] = max_age
end
after do
ENV['API_CORS_ORIGINS'] = nil
ENV['API_CORS_ALLOW_CREDENTIALS'] = nil
ENV['API_CORS_MAX_AGE'] = nil
end
it 'sends CORS headers when requesting using GET from frontend url' do
api_get '/api/v2/account/balances', token: token, headers: { 'Origin' => frontend_url }
expect(response).to be_successful
check_cors(response, '*', allow_crendentails, max_age)
end
it 'sends CORS headers when requesting using GET from localhost' do
api_get '/api/v2/account/balances', token: token, headers: { 'Origin' => local_url }
expect(response).to be_successful
check_cors(response, '*', allow_crendentails, max_age)
end
end
context 'set multiple API_CORS_ORIGINS for frontend and localhost' do
let(:allow_crendentails) { 'true' }
let(:max_age) { '6200' }
before do
ENV['API_CORS_ORIGINS'] = "#{frontend_url},#{local_url}"
ENV['API_CORS_ALLOW_CREDENTIALS'] = allow_crendentails
ENV['API_CORS_MAX_AGE'] = max_age
end
after do
ENV['API_CORS_ORIGINS'] = nil
ENV['API_CORS_ALLOW_CREDENTIALS'] = nil
ENV['API_CORS_MAX_AGE'] = nil
end
it 'sends CORS headers when requesting using GET from frontend url' do
api_get '/api/v2/account/balances', token: token, headers: { 'Origin' => frontend_url }
expect(response).to be_successful
check_cors(response, frontend_url, allow_crendentails, max_age)
end
it 'sends CORS headers when requesting using GET from localhost' do
api_get '/api/v2/account/balances', token: token, headers: { 'Origin' => local_url }
expect(response).to be_successful
check_cors(response, local_url, allow_crendentails, max_age)
end
it 'doesn\'t sends CORS headers when requesting using GET from unkown domain' do
api_get '/api/v2/account/balances', token: token, headers: { 'Origin' => 'http://domain.com' }
expect(response).to be_successful
without_cors(response)
end
end
context 'send invalid request' do
let(:allow_crendentails) { 'true' }
before do
ENV['API_CORS_ORIGINS'] = "#{frontend_url},#{local_url}"
ENV['API_CORS_ALLOW_CREDENTIALS'] = allow_crendentails
end
after do
ENV['API_CORS_ORIGINS'] = nil
ENV['API_CORS_ALLOW_CREDENTIALS'] = nil
end
it 'sends CORS headers ever when user is not authenticated' do
api_get '/api/v2/account/balances', headers: { 'Origin' => local_url }
expect(response).to have_http_status 401
check_cors(response, local_url, allow_crendentails)
end
it 'sends CORS headers when invalid parameter supplied' do
api_get '/api/v2/account/balances/somecoin', token: token, headers: { 'Origin' => local_url }
expect(response).to have_http_status 422
check_cors(response, local_url, allow_crendentails)
end
end
end

View File

@@ -0,0 +1,66 @@
# encoding: UTF-8
# frozen_string_literal: true
describe CORS::Validations do
describe 'validate origins' do
subject { CORS::Validations.validate_origins(ENV['API_CORS_ORIGINS']) }
context 'set API_CORS_ORIGINS as "*"' do
before { ENV['API_CORS_ORIGINS'] = '*' }
it { is_expected.to eq('*') }
after { ENV['API_CORS_ORIGINS'] = nil }
end
context 'set mulitple API_CORS_ORIGINS with "*"' do
before { ENV['API_CORS_ORIGINS'] = 'https://localhost,*,https://domain.com' }
it { is_expected.to eq('*') }
after { ENV['API_CORS_ORIGINS'] = nil }
end
context 'set multiple API_CORS_ORIGINS' do
before { ENV['API_CORS_ORIGINS'] = 'https://localhost,https://domain.com' }
it { is_expected.to eq(['https://localhost','https://domain.com']) }
after { ENV['API_CORS_ORIGINS'] = nil }
end
context 'set invalid domain into API_CORS_ORIGINS' do
before { ENV['API_CORS_ORIGINS'] = 'htt:://localhost' }
it { expect { subject }.to raise_error(CORS::Validations::Error) }
after { ENV['API_CORS_MAX_AGE'] = nil }
end
end
describe 'validate max age' do
subject { CORS::Validations.validate_max_age(ENV['API_CORS_MAX_AGE']) }
context 'set API_CORS_MAX_AGE as "6200"' do
before { ENV['API_CORS_MAX_AGE'] = '6200' }
it { is_expected.to eq('6200') }
after { ENV['API_CORS_MAX_AGE'] = nil }
end
context 'set API_CORS_MAX_AGE as "6200.1"' do
before { ENV['API_CORS_MAX_AGE'] = '6200.1' }
it { is_expected.to eq('3600') }
after { ENV['API_CORS_MAX_AGE'] = nil }
end
context 'doesn\'t set API_CORS_MAX_AGE"' do
before { ENV['API_CORS_MAX_AGE'] = nil }
it { is_expected.to eq('3600') }
end
end
end

View File

@@ -0,0 +1,14 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Entities::Account do
let(:account) { create_account(:btc, balance: 100) }
subject { OpenStruct.new API::V2::Entities::Account.represent(account).serializable_hash }
it do
expect(subject.currency).to eq 'btc'
expect(subject.balance).to eq '100.0'
expect(subject.locked).to eq '0.0'
end
end

View File

@@ -0,0 +1,13 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Entities::Member do
let(:member) { create(:member, :level_3) }
subject { OpenStruct.new API::V2::Entities::Member.represent(member).serializable_hash }
it do
expect(subject.uid).to eq member.uid
expect(subject.email).to eq member.email
end
end

View File

@@ -0,0 +1,52 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Entities::Order do
let(:order) do
create(
:order_ask,
:btcusd,
price: '12.32'.to_d,
volume: '3.1418',
origin_volume: '12.13'
)
end
context 'default exposure' do
subject { OpenStruct.new API::V2::Entities::Order.represent(order, {}).serializable_hash }
it do
expect(subject.id).to eq order.id
expect(subject.price).to eq order.price
expect(subject.avg_price).to eq ::Trade::ZERO
expect(subject.origin_volume).to eq order.origin_volume
expect(subject.remaining_volume).to eq order.volume
expect(subject.executed_volume).to eq(order.origin_volume - order.volume)
expect(subject.state).to eq order.state
expect(subject.market).to eq order.market_id
expect(subject.side).to eq 'sell'
expect(subject.maker_fee).to eq order.maker_fee
expect(subject.taker_fee).to eq order.taker_fee
expect(subject.trades).to be_nil
expect(subject.trades_count).to be_zero
expect(subject.created_at).to eq order.created_at.iso8601
end
end
context 'full exposure' do
it 'should expose related trades' do
create(:trade, :btcusd, maker_order: order, amount: '8.0', price: '12')
create(:trade, :btcusd, maker_order: order, amount: '0.99', price: '12.56')
json = API::V2::Entities::Order.represent(order, type: :full).serializable_hash
expect(json[:trades].size).to eq 2
end
end
end

View File

@@ -0,0 +1,38 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::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::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
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::Entities::Trade.represent(trade).serializable_hash }
it { expect(subject.respond_to?(:side)).to be_falsey }
end
end

View File

@@ -0,0 +1,59 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
class AuthTest < Grape::API
get('/auth_test') do
authenticate!
end
end
class Mount
mount AuthTest
end
end
end
describe API::V2::Helpers, type: :request do
context '#authentic?' do
let!(:member) { create(:member, :level_3) }
let!(:token) { jwt_for(member) }
context 'Authenticate using headers' do
it 'should response successfully' do
api_get '/api/v2/auth_test', foo: 'bar', hello: 'world', token: token
expect(response).to be_successful
end
it 'should not return authorization header' do
api_get '/api/v2/auth_test', foo: 'bar', hello: 'world', token: token
expect(response.headers).not_to include('Authorization')
end
it 'should set current user' do
api_get '/api/v2/auth_test', foo: 'bar', hello: 'world', token: token
expect(response.body).to eq member.reload.to_json
end
it 'should fail authorization' do
get '/api/v2/auth_test'
expect(response.code).to eq '401'
expect(response).to include_api_error('jwt.decode_and_verify')
end
end
end
context '#authentic_include_username?' do
let!(:member) { create(:member, username: 'foobar') }
let!(:token) { jwt_for(member, { username: 'foobar' }) }
context 'Authenticate using headers' do
it 'should set current user' do
api_get '/api/v2/auth_test', token: token
expect(response.body).to eq member.reload.to_json
end
end
end
end

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,634 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Market::Orders, type: :request do
let(:member) { create(:member, :level_3) }
let(:level_0_member) { create(:member, :level_0) }
let(:token) { jwt_for(member) }
let(:level_0_member_token) { jwt_for(level_0_member) }
before do
Ability.stubs(:user_permissions).returns({'member'=>{'read'=>['Order'],'create'=>['Order'],'update'=>['Order']}})
end
describe 'GET /api/v2/market/orders' do
before do
# NOTE: We specify updated_at attribute for testing order of Order.
create(:order_bid, :btcusd, price: '11'.to_d, volume: '123.12345678', member: member, created_at: 1.day.ago, updated_at: Time.now + 5)
create(:order_bid, :btceth, price: '11'.to_d, volume: '123.1234', member: member)
create(:order_bid, :btcusd, price: '12'.to_d, volume: '123.12345678', created_at: 1.day.ago, member: member, state: Order::CANCEL)
create(:order_ask, :btcusd, price: '13'.to_d, volume: '123.12345678', created_at: 2.hours.ago, member: member, state: Order::WAIT, updated_at: Time.now + 10)
create(:order_ask, :btcusd, price: '14'.to_d, volume: '123.12345678', created_at: 6.hours.ago, member: member, state: Order::DONE)
end
it 'requires authentication' do
get '/api/v2/market/orders', params: { market: 'btcusd' }
expect(response.code).to eq '401'
end
it 'validates market param' do
api_get '/api/v2/market/orders', params: { market: 'usdusd' }, token: token
expect(response).to have_http_status 422
expect(response).to include_api_error('market.market.doesnt_exist')
end
it 'validates state param' do
api_get '/api/v2/market/orders', params: { market: 'btcusd', state: 'test' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.invalid_state')
end
it 'validates limit param' do
api_get '/api/v2/market/orders', params: { market: 'btcusd', limit: -1 }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.invalid_limit')
end
it 'validates ord_type param' do
api_get '/api/v2/market/orders', params: { ord_type: 'test' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.invalid_ord_type')
end
it 'validates type param' do
api_get '/api/v2/market/orders', params: { type: 'test' }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.invalid_type')
end
it 'returns all order history' do
api_get '/api/v2/market/orders', token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 5
end
it 'returns all my orders for btcusd market' do
api_get '/api/v2/market/orders', params: { market: 'btcusd' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 4
end
it 'returns orders for several markets' do
api_get '/api/v2/market/orders', params: { market: ['btcusd', 'btceth'] }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 5
end
it 'returns orders with state done' do
api_get '/api/v2/market/orders', params: { market: 'btcusd', state: Order::DONE }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 1
expect(result.first['state']).to eq Order::DONE
end
it 'returns orders with state done and wait' do
api_get '/api/v2/market/orders', params: { market: 'btcusd', state: [Order::DONE, Order::WAIT] }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
count = member.orders.where(state: [Order::DONE, Order::WAIT], market_id: 'btcusd').count
expect(result.size).to eq count
end
it 'returns paginated orders' do
api_get '/api/v2/market/orders', params: { market: 'btcusd', limit: 1, page: 1 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.first['price']).to eq '13.0'
api_get '/api/v2/market/orders', params: { market: 'btcusd', limit: 1, page: 2 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.first['price']).to eq '11.0'
end
it 'returns sorted orders' do
api_get '/api/v2/market/orders', params: { market: 'btcusd', order_by: 'asc' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
first_order_updated_at = Time.iso8601(result.first['updated_at'])
second_order_updated_at = Time.iso8601(result.second['updated_at'])
expect(first_order_updated_at).to be <= second_order_updated_at
api_get '/api/v2/market/orders', params: { market: 'btcusd', order_by: 'desc' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
first_order_updated_at = Time.iso8601(result.first['updated_at'])
second_order_updated_at = Time.iso8601(result.second['updated_at'])
expect(first_order_updated_at).to be >= second_order_updated_at
end
it 'returns orders with ord_type limit' do
api_get '/api/v2/market/orders', params: { ord_type: 'limit' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['ord_type']}.uniq.size).to eq 1
expect(result.map{|r| r['ord_type']}.uniq.first).to eq 'limit'
end
it 'returns orders with type sell' do
api_get '/api/v2/market/orders', params: { type: 'sell' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.map{|r| r['side']}.uniq.size).to eq 1
expect(result.map{|r| r['side']}.uniq.first).to eq 'sell'
end
it 'returns orders with base unit btc' do
api_get '/api/v2/market/orders', params: { base_unit: 'btc' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 5
end
it 'returns orders with quote unit eth' do
api_get '/api/v2/market/orders', params: { base_unit: 'btc' }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 5
end
it 'returns orders with timestamp filter' do
api_get '/api/v2/market/orders', params: { time_from: 7.hours.ago.to_i, time_to: 5.hours.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 1
end
it 'returns orders with timestamp filter' do
api_get '/api/v2/market/orders', params: { time_from: 3.hours.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 2
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_get '/api/v2/market/orders', params: { time_from: 3.hours.ago.to_i }, token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
it 'denies access to unverified member' do
api_get '/api/v2/market/orders', token: level_0_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('market.trade.not_permitted')
end
end
describe 'GET /api/v2/market/orders/:id' do
let(:order) { create(:order_bid, :btcusd, price: '12.32'.to_d, volume: '3.14', origin_volume: '12.13', member: member, trades_count: 1) }
let!(:trade) { create(:trade, :btcusd, taker_order: order) }
it 'should get specified order by id' do
api_get "/api/v2/market/orders/#{order.id}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['id']).to eq order.id
expect(result['executed_volume']).to eq '8.99'
end
it 'should get specified order by uuid' do
api_get "/api/v2/market/orders/#{order.uuid}", token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['uuid']).to eq order.uuid
expect(result['executed_volume']).to eq '8.99'
end
it 'should include related trades' do
api_get "/api/v2/market/orders/#{order.id}", token: token
result = JSON.parse(response.body)
expect(result['trades_count']).to eq 1
expect(result['trades'].size).to eq 1
expect(result['trades'].first['id']).to eq trade.id
expect(result['trades'].first['side']).to eq 'buy'
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_get "/api/v2/market/orders/#{order.id}", token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
it 'should get 404 error when order doesn\'t exist' do
api_get '/api/v2/market/orders/1234', token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'should raise error' do
api_get '/api/v2/market/orders/1234asd', token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.invaild_id_or_uuid')
end
end
describe 'POST /api/v2/market/orders' do
it 'creates a sell order on peatio engine' do
member.get_account(:btc).update_attributes(balance: 100)
expect do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '12.13', price: '2014' }
expect(response).to be_successful
expect(JSON.parse(response.body)['id']).to eq OrderAsk.last.id
end.to change(OrderAsk, :count).by(1)
end
it 'submit a sell order on third party engine' do
member.get_account(:btc).update_attributes(balance: 100)
Market.find('btcusd').engine.update(driver: "finex-spot")
AMQP::Queue.expects(:publish)
expect do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '12.13', price: '2014' }
expect(response).to be_successful
expect(response_body['market']).to eq 'btcusd'
end.not_to change(OrderAsk, :count)
end
it 'creates a buy order' do
member.get_account(:usd).update_attributes(balance: 100_000)
AMQP::Queue.expects(:enqueue).with(:order_processor, is_a(Hash), is_a(Hash))
expect do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'buy', volume: '12.13', price: '2014' }
expect(response).to be_successful
expect(JSON.parse(response.body)['id']).to eq OrderBid.last.id
end.to change(OrderBid, :count).by(1)
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'buy', volume: '12.13', price: '2014' }
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
it 'validates missing params' do
member.get_account(:usd).update_attributes(balance: 100_000)
api_post '/api/v2/market/orders', token: token
expect(response).to have_http_status(422)
expect(response).to include_api_error('market.order.missing_market')
expect(response).to include_api_error('market.order.missing_side')
expect(response).to include_api_error('market.order.missing_volume')
expect(response).to include_api_error('market.order.missing_price')
end
it 'validates volume positiveness' do
old_count = OrderAsk.count
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '-1.1', price: '2014' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.non_positive_volume')
expect(OrderAsk.count).to eq old_count
end
it 'validates volume to be a number' do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: 'test', price: '2014' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.non_decimal_volume')
end
it 'validates volume greater than min_amount' do
member.get_account(:btc).update_attributes(balance: 1)
m = Market.find(:btcusd)
m.update(min_amount: 1.0)
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '0.1', price: '2014' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.invalid_volume_or_price')
end
it 'validates price less than max_price' do
member.get_account(:usd).update_attributes(balance: 1)
m = Market.find(:btcusd)
m.update(max_price: 1.0)
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'buy', volume: '0.1', price: '2' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.invalid_volume_or_price')
end
it 'validates volume precision' do
member.get_account(:usd).update_attributes(balance: 1)
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'buy', volume: '0.123456789', price: '0.1' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.invalid_volume_or_price')
end
it 'validates price greater than min_price' do
member.get_account(:usd).update_attributes(balance: 1)
m = Market.find(:btcusd)
m.update(min_price: 1.0)
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'buy', volume: '0.1', price: '0.2' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.invalid_volume_or_price')
end
it 'validates price precision' do
member.get_account(:usd).update_attributes(balance: 1)
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'buy', volume: '0.12', price: '0.123' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.invalid_volume_or_price')
end
it 'validates enough funds' do
old_count = OrderAsk.count
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '12.13', price: '2014' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.account.insufficient_balance')
expect(OrderAsk.count).to eq old_count
end
it 'validates price positiveness' do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '12.13', price: '-1.1' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.non_positive_price')
end
it 'validates price to be a number' do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '12.13', price: 'test' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.non_decimal_price')
end
context 'market order' do
it 'validates that market has sufficient volume' do
member.get_account(:btc).update_attributes(balance: 20)
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '12.13', ord_type: 'market' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.insufficient_market_liquidity')
end
it 'validates that order has no price param' do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '0.5', price: '0.5', ord_type: 'market' }
expect(response.code).to eq '422'
expect(response).to include_api_error('market.order.market_order_price')
end
it 'creates sell order' do
create(:order_bid, :btcusd, price: '10'.to_d, volume: '10', origin_volume: '10', member: member)
member.get_account(:btc).update_attributes(balance: 1)
expect do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '0.5', ord_type: 'market' }
end.to change(OrderAsk, :count).by(1)
expect(response).to be_successful
expect(JSON.parse(response.body)['id']).to eq OrderAsk.last.id
end
context 'submit sell order on third party engine' do
it do
create(:order_bid, :btcusd, price: '10'.to_d, volume: '10', origin_volume: '10', member: member)
member.get_account(:btc).update_attributes(balance: 1)
Market.find('btcusd').engine.update(driver: "finex-spot")
AMQP::Queue.expects(:publish)
expect do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'sell', volume: '0.5', ord_type: 'market' }
end.not_to change(OrderAsk, :count)
expect(response).to be_successful
end
end
it 'creates buy order' do
create(:order_ask, :btcusd, price: '10'.to_d, volume: '10', origin_volume: '10', member: member)
member.get_account(:usd).update_attributes(balance: 10)
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'buy', volume: '0.5', ord_type: 'market' }
expect do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'buy', volume: '0.5', ord_type: 'market' }
end.to change(OrderBid, :count).by(1)
expect(response).to be_successful
expect(JSON.parse(response.body)['id']).to eq OrderBid.last.id
end
context '#compute_locked' do
before do
create(:order_ask, :btcusd, price: '10'.to_d, volume: '10', origin_volume: '10', member: member)
member.get_account(:usd).update_attributes(balance: 10)
end
it 'locks all balance' do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'buy', volume: '1', ord_type: 'market' }
expect(Order.find(response_body['id']).locked).to eq member.get_account(:usd).balance
end
it 'locks with locking_buffer' do
api_post '/api/v2/market/orders', token: token, params: { market: 'btcusd', side: 'buy', volume: '0.5', ord_type: 'market' }
# Price: 10, volume: 0.5, locking_buffer: 1.1
expect(Order.find(response_body['id']).locked).to eq 5.5
end
end
end
end
describe 'POST /api/v2/market/orders/:id/cancel' do
let!(:order) { create(:order_bid, :btcusd, price: '12.32'.to_d, volume: '3.14', origin_volume: '12.13', locked: '20.1082', origin_locked: '38.0882', member: member) }
context 'succesful' do
before do
member.get_account(:usd).update_attributes(locked: order.price * order.volume)
end
it 'should cancel specified order by id' do
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: order.to_matching_attributes)
expect do
api_post "/api/v2/market/orders/#{order.id}/cancel", token: token
expect(response).to be_successful
expect(JSON.parse(response.body)['id']).to eq order.id
end.not_to change(Order, :count)
end
it 'should cancel specified order by uuid' do
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: order.to_matching_attributes)
expect do
api_post "/api/v2/market/orders/#{order.uuid}/cancel", token: token
expect(response).to be_successful
expect(JSON.parse(response.body)['uuid']).to eq order.uuid
end.not_to change(Order, :count)
end
end
context 'third party order' do
before do
order.market.engine.update(driver: "finex-spot")
end
it 'should cancel specified order by uuid' do
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: order.to_matching_attributes).never
AMQP::Queue.expects(:publish).with(order.market.engine.driver, data: order.as_json_for_third_party, type: 3)
expect do
api_post "/api/v2/market/orders/#{order.uuid}/cancel", token: token
expect(response).to be_successful
expect(JSON.parse(response.body)['uuid']).to eq order.uuid
end.not_to change(Order, :count)
end
end
context 'failed' do
it 'should return order not found error' do
api_post '/api/v2/market/orders/0/cancel', token: token
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_post "/api/v2/market/orders/#{order.uuid}/cancel", token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
end
describe 'POST /api/v2/market/orders/cancel' do
before do
create(:order_ask, :btcusd, price: '12.32', volume: '3.14', origin_volume: '12.13', member: member)
create(:order_bid, :btcusd, price: '12.32', volume: '3.14', origin_volume: '12.13', member: member)
create(:order_bid, :btceth, price: '12.32', volume: '3.14', origin_volume: '12.13', member: member)
member.get_account(:btc).update_attributes(locked: '5')
member.get_account(:usd).update_attributes(locked: '50')
end
it 'should cancel all my orders' do
member.orders.each do |o|
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: o.to_matching_attributes)
end
expect do
api_post '/api/v2/market/orders/cancel', token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq 3
end.not_to change(Order, :count)
end
context 'third party order' do
before do
Market.find('btcusd').engine.update(driver: "finex-spot")
Market.find('btceth').engine.update(driver: "finex-spot")
end
it 'should cancel all my orders on market with third party engine' do
AMQP::Queue.expects(:enqueue).never
member.orders.each do |o|
AMQP::Queue.expects(:publish).with(o.market.engine.driver, data: o.as_json_for_third_party, type: 3)
end
expect do
api_post '/api/v2/market/orders/cancel', token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq 3
end.not_to change(Order, :count)
end
end
it 'should cancel all my orders for specific market' do
member.orders.where(market: 'btceth').each do |o|
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: o.to_matching_attributes)
end
expect do
api_post '/api/v2/market/orders/cancel', token: token, params: { market: 'btceth' }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq 1
end.not_to change(Order, :count)
end
it 'should cancel all my asks' do
member.orders.where(type: 'OrderAsk').each do |o|
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: o.to_matching_attributes)
end
expect do
api_post '/api/v2/market/orders/cancel', token: token, params: { side: 'sell' }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq 1
expect(result.first['id']).to eq member.orders.where(type: 'OrderAsk').first.id
end.not_to change(Order, :count)
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_post '/api/v2/market/orders/cancel', token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
end

View File

@@ -0,0 +1,288 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Market::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
before do
Ability.stubs(:user_permissions).returns({'member'=>{'read'=>['Trade']}})
end
let(:token) { jwt_for(member) }
let(:level_0_member) { create(:member, :level_0) }
let(:level_0_member_token) { jwt_for(level_0_member) }
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.32'.to_d,
volume: '123.1234',
member: 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.32'.to_d,
volume: '123.1234',
member: member
)
end
let(:btcusd_bid_maker) do
create(
:order_bid,
:btcusd,
price: '12.32'.to_d,
volume: '123.12345678',
member: member
)
end
let(:btceth_ask_taker) do
create(
:order_ask,
:btceth,
price: '12.32'.to_d,
volume: '123.1234',
member: member
)
end
let(:btceth_bid_taker) do
create(
:order_bid,
:btceth,
price: '12.32'.to_d,
volume: '123.1234',
member: 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, taker: member, created_at: 23.hours.ago) }
describe 'GET /api/v2/market/trades' do
it 'requires authentication' do
get '/api/v2/market/trades', params: { market: 'btcusd' }
expect(response.code).to eq '401'
expect(response).to include_api_error('jwt.decode_and_verify')
end
it 'returns all my recent trades' do
api_get '/api/v2/market/trades', token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq 4
expect(result.find { |t| t['id'] == btcusd_ask_trade.id }['side']).to eq 'sell'
expect(result.find { |t| t['id'] == btcusd_ask_trade.id }['order_id']).to eq btcusd_ask.id
expect(result.find { |t| t['id'] == btceth_ask_trade.id }['side']).to eq 'sell'
expect(result.find { |t| t['id'] == btceth_ask_trade.id }['order_id']).to eq btceth_ask.id
expect(result.find { |t| t['id'] == btcusd_bid_trade.id }['side']).to eq 'buy'
expect(result.find { |t| t['id'] == btcusd_bid_trade.id }['order_id']).to eq btcusd_bid.id
expect(result.find { |t| t['id'] == btceth_bid_trade.id }['side']).to eq 'buy'
expect(result.find { |t| t['id'] == btceth_bid_trade.id }['order_id']).to eq btceth_bid.id
end
it 'returns all my recent trades for btcusd market' do
api_get '/api/v2/market/trades', params: { market: 'btcusd' }, token: token
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq 2
expect(result.find { |t| t['id'] == btcusd_ask_trade.id }['side']).to eq 'sell'
expect(result.find { |t| t['id'] == btcusd_ask_trade.id }['order_id']).to eq btcusd_ask.id
expect(result.find { |t| t['id'] == btcusd_bid_trade.id }['side']).to eq 'buy'
expect(result.find { |t| t['id'] == btcusd_bid_trade.id }['order_id']).to eq btcusd_bid.id
end
it 'returns trades for several markets' do
api_get '/api/v2/market/trades', params: { market: ['btcusd', 'btceth'] }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 4
end
it 'returns 1 trade' do
api_get '/api/v2/market/trades', params: { market: 'btcusd', limit: 1 }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 1
end
it 'returns trades for last 24h' do
create(:trade, :btcusd, maker: member, created_at: 6.hours.ago)
api_get '/api/v2/market/trades', params: { time_from: 1.day.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 3
end
it 'returns trades older than 1 day' do
api_get '/api/v2/market/trades', params: { time_to: 1.day.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 2
end
it 'returns trades for specific hour' do
create(:trade, :btcusd, maker: member, created_at: 6.hours.ago)
api_get '/api/v2/market/trades', params: { time_from: 7.hours.ago.to_i, time_to: 5.hours.ago.to_i }, token: token
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.size).to eq 1
end
it 'returns limit out of range error' do
api_get '/api/v2/market/trades', params: { market: 'btcusd', limit: 1024 }, token: token
expect(response.code).to eq '422'
expect(response).to include_api_error('market.trade.invalid_limit')
end
it 'denies access to unverified member' do
api_get '/api/v2/market/trades', params: { market: 'btcusd' }, token: level_0_member_token
expect(response.code).to eq '403'
expect(response).to include_api_error('market.trade.not_permitted')
end
it 'fee calculation for buy order' do
api_get '/api/v2/market/trades', params: { market: 'btcusd' }, token: token
result = JSON.parse(response.body).find { |t| t['side'] == 'buy' }
expect(result['order_id']).to eq btcusd_bid.id
expect(result['fee_amount']).to eq((btcusd_bid.taker_fee * btcusd_bid_trade.amount).to_s)
expect(result['fee']).to eq btcusd_bid.taker_fee.to_s
end
it 'fee calculation for sell order' do
api_get '/api/v2/market/trades', params: { market: 'btcusd' }, token: token
result = JSON.parse(response.body).find { |t| t['side'] == 'sell' }
expect(result['order_id']).to eq btcusd_ask.id
expect(result['fee_amount']).to eq((btcusd_ask.taker_fee * btcusd_ask_trade.total).to_s)
expect(result['fee']).to eq btcusd_ask.taker_fee.to_s
end
it 'fee currency for buy order' do
api_get '/api/v2/market/trades', params: { market: 'btcusd' }, token: token
result = JSON.parse(response.body).find { |t| t['side'] == 'buy' }
expect(result['order_id']).to eq btcusd_bid.id
expect(result['fee_currency']).to eq 'btc'
end
it 'fee currency for sell order' do
api_get '/api/v2/market/trades', params: { market: 'btcusd' }, token: token
result = JSON.parse(response.body).find { |t| t['side'] == 'sell' }
expect(result['order_id']).to eq btcusd_ask.id
expect(result['fee_currency']).to eq 'usd'
end
context 'type filtering' do
context 'sell orders' do
let!(:btceth_ask_trade_taker) { create(:trade, :btceth, taker_order: btceth_ask_taker, created_at: 2.hours.ago) }
let!(:btcusd_bid_trade_maker) { create(:trade, :btcusd, maker_order: btcusd_bid_maker, created_at: 2.hours.ago) }
it 'with taker_id = user_id and taker_type = sell' do
api_get '/api/v2/market/trades', params: { market: 'btceth', type: 'sell' }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 2
expect(result.find { |t| t['id'] == btceth_ask_trade.id }['side']).to eq 'sell'
expect(result.find { |t| t['id'] == btceth_ask_trade.id }['order_id']).to eq btceth_ask.id
expect(result.find { |t| t['id'] == btceth_ask_trade_taker.id }['side']).to eq 'sell'
expect(result.find { |t| t['id'] == btceth_ask_trade_taker.id }['order_id']).to eq btceth_ask_taker.id
end
it 'with maker_id = user_id and taker_type = buy' do
api_get '/api/v2/market/trades', params: { market: 'btcusd', type: 'sell' }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 2
expect(result.find { |t| t['id'] == btcusd_ask_trade.id }['side']).to eq 'sell'
expect(result.find { |t| t['id'] == btcusd_ask_trade.id }['order_id']).to eq btcusd_ask.id
expect(result.find { |t| t['id'] == btcusd_bid_trade_maker.id }['side']).to eq 'buy'
expect(result.find { |t| t['id'] == btcusd_bid_trade_maker.id }['order_id']).to eq btcusd_bid_maker.id
end
end
context 'buy orders' do
let!(:btceth_bid_trade_taker) { create(:trade, :btceth, taker_order: btceth_bid_taker, created_at: 2.hours.ago) }
it 'with taker_id = user_id and taker_type = buy' do
api_get '/api/v2/market/trades', params: { market: 'btceth', type: 'buy' }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 2
expect(result.find { |t| t['id'] == btceth_bid_trade.id }['side']).to eq 'buy'
expect(result.find { |t| t['id'] == btceth_bid_trade.id }['order_id']).to eq btceth_bid.id
expect(result.find { |t| t['id'] == btceth_bid_trade_taker.id }['side']).to eq 'buy'
expect(result.find { |t| t['id'] == btceth_bid_trade_taker.id }['order_id']).to eq btceth_bid_taker.id
end
it 'with maker_id = user_id and taker_type = sell' do
api_get '/api/v2/market/trades', params: { market: 'btcusd', type: 'buy' }, token: token
result = JSON.parse(response.body)
expect(result.size).to eq 1
expect(result.find { |t| t['id'] == btcusd_bid_trade.id }['side']).to eq 'buy'
expect(result.find { |t| t['id'] == btcusd_bid_trade.id }['order_id']).to eq btcusd_bid.id
end
end
end
context 'unauthorized' do
before do
Ability.stubs(:user_permissions).returns([])
end
it 'renders unauthorized error' do
api_get '/api/v2/market/trades', params: { market: 'btcusd' }, token: token
expect(response).to have_http_status 403
expect(response).to include_api_error('user.ability.not_permitted')
end
end
end
end

50
spec/api/v2/mount_spec.rb Normal file
View File

@@ -0,0 +1,50 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
class Mount
# Use /public namespace for skipping rack-jwt authorization.
namespace :public do
get('/null') { '' }
get('/record-not-found') { raise ActiveRecord::RecordNotFound }
get('/auth-error') { raise Peatio::Auth::Error }
get('/standard-error') { raise StandardError }
end
end
end
end
describe API::V2::Mount, type: :request do
let(:middlewares) { API::V2::Mount.middleware }
it 'should use attack middleware' do
expect(middlewares.drop(1)).to eq [[:use, Rack::Attack]]
end
context 'handle exception on request processing' do
it 'returns array with record.not_found error' do
get '/api/v2/public/record-not-found'
expect(response.code).to eq '404'
expect(response).to include_api_error('record.not_found')
end
it 'returns array with jwt.decode_and_verify error' do
get '/api/v2/public/auth-error'
expect(response.code).to eq '401'
expect(response).to include_api_error('jwt.decode_and_verify')
end
it 'returns array with server.internal_error error' do
get '/api/v2/public/standard-error'
expect(response.code).to eq '500'
expect(response).to include_api_error('server.internal_error')
end
end
context 'handle exception on request routing' do
it 'should render json error message' do
get '/api/v2/public/non/exist'
expect(response.code).to eq '404'
end
end
end

View File

@@ -0,0 +1,140 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Public::Currencies, type: :request do
before(:each) { clear_redis }
describe 'GET /api/v2/public/currencies/:id' do
let(:fiat) { Currency.find(:usd) }
let(:coin) { Currency.find(:btc) }
let(:expected_for_fiat) do
%w[id type deposit_enabled withdrawal_enabled deposit_fee withdraw_fee withdraw_limit_24h withdraw_limit_72h base_factor precision]
end
let(:expected_for_coin) do
expected_for_fiat.concat(%w[explorer_transaction explorer_address])
end
it 'returns information about specified currency' do
get "/api/v2/public/currencies/#{coin.id}"
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.fetch('id')).to eq coin.id
end
context 'currency code with dot' do
let!(:currency) { create(:currency, :xagm_cx) }
it 'returns information about specified currency' do
get "/api/v2/public/currencies/#{currency.id}"
result = JSON.parse(response.body)
expect(result.fetch('id')).to eq currency.id
end
end
it 'returns correct keys for fiat' do
get "/api/v2/public/currencies/#{fiat.id}"
expect(response).to be_successful
result = JSON.parse(response.body)
expected_for_fiat.each { |key| expect(result).to have_key key }
(expected_for_coin - expected_for_fiat).each do |key|
expect(result).not_to have_key key
end
end
it 'returns correct keys for coin' do
get "/api/v2/public/currencies/#{coin.id}"
expect(response).to be_successful
result = JSON.parse(response.body)
expected_for_coin.each { |key| expect(result).to have_key key }
end
it 'returns error in case of invalid id' do
get '/api/v2/public/currencies/invalid'
expect(response).to have_http_status 422
expect(response).to include_api_error('public.currency.doesnt_exist')
end
end
describe 'GET /api/v2/public/currencies' do
it 'lists visible currencies' do
get '/api/v2/public/currencies'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.visible.size
end
it 'lists visible coins' do
get '/api/v2/public/currencies', params: { type: 'coin' }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Currency.coins.visible.size
end
it 'lists visible fiats' do
get '/api/v2/public/currencies', params: { type: 'fiat' }
expect(response).to be_successful
result = JSON.parse(response.body, symbolize_names: true)
expect(result.size).to eq Currency.fiats.visible.size
expect(result.dig(0, :id)).to eq 'usd'
end
it 'returns error in case of invalid type' do
get '/api/v2/public/currencies', params: { type: 'invalid' }
expect(response).to have_http_status 422
end
context 'pagination' do
it 'returns paginated currencies' do
get '/api/v2/public/currencies', params: { limit: 2 }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total').to_i).to eq Currency.visible.count
expect(result.size).to eq(2)
end
end
context 'search' do
it 'searches by code' do
get '/api/v2/public/currencies', params: { search: { code: 't' } }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('id')).to contain_exactly('eth', 'btc', 'trst')
end
it 'searches by name' do
get '/api/v2/public/currencies', params: { search: { name: 'e' } }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('name')).to contain_exactly('Ethereum', 'Evolution Land Global Token', 'WeTrust')
end
it 'searches by code or name' do
get '/api/v2/public/currencies', params: { search: { name: 'us', code: 'us' } }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('id')).to contain_exactly('usd', 'trst')
expect(result.pluck('name')).to contain_exactly('US Dollar', 'WeTrust')
end
end
end
end

View File

@@ -0,0 +1,760 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Public::Markets, type: :request do
before(:each) { clear_redis }
describe 'GET /api/v2/markets' do
before { create(:market, :ethusd) }
let(:expected_keys) do
%w[id name base_unit quote_unit min_price max_price
min_amount amount_precision price_precision state]
end
it 'lists enabled markets' do
get '/api/v2/public/markets'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Market.enabled.size
result.each do |market|
expect(market.keys).to contain_exactly(*expected_keys)
end
end
context 'api will return hidden markets' do
before { create(:market, :btceur, state: :hidden) }
it 'returns hidden market' do
get '/api/v2/public/markets'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.find { |currency| currency['id'] == 'btceur' }['state']).to eq('hidden')
end
end
context 'pagination' do
it 'returns paginated markets' do
get '/api/v2/public/markets', params: { limit: 2 }
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total').to_i).to eq Market.enabled.size
expect(result.size).to eq(2)
end
end
context 'filters' do
context 'base_unit & quote_unit' do
it 'filters by base_unit' do
get '/api/v2/public/markets', params: { base_unit: :btc }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Market.enabled.where(base_unit: :btc).size
result.each do |market|
expect(market['base_unit']).to eq 'btc'
end
end
it 'filters by quote_unit' do
get '/api/v2/public/markets', params: { quote_unit: :usd }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Market.enabled.where(quote_unit: :usd).size
result.each do |market|
expect(market['quote_unit']).to eq 'usd'
end
end
it 'does not filter' do
get '/api/v2/public/markets'
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq Market.enabled.size
end
end
context 'base_code & quote_code' do
it 'filters by base_code' do
get '/api/v2/public/markets', params: { search: { base_code: "bt" } }
# Since we have next markets list:
# btcusd, btceth, ethusd
# Since 2 of them has 'bt' in base_unit (btc).
# We expect them to be returned in API response.
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('id')).to contain_exactly('btceth', 'btcusd')
end
it 'filters by quote_code' do
Currency.find(:eur).update(visible: true)
create(:market, :btceur)
# Since we have next markets list:
# btceur, btcusd, btceth, ethusd
# Since 2 of them has 'e' in quote_unit (eur, eth).
# We expect them to be returned in API response.
get '/api/v2/public/markets', params: { search: { quote_code: "e" } }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('id')).to contain_exactly('btceth', 'btceur')
end
end
context 'quote_name' do
before do
Currency.find(:eur).update(visible: true)
create(:market, :btceur)
create(:market, :btctrst)
end
it 'filters by name 1' do
# Since we have next markets list:
# btceur, btcusd, btceth, btctrst, ethusd
# Since 3 of them has 'E' in quote name (Euro, Ethereum, We Trust).
# We expect them to be returned in API response.
get '/api/v2/public/markets', params: { search: { quote_name: 'E' } }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('id')).to contain_exactly('btceth', 'btceur', 'btctrst')
end
it 'filters by name 2' do
# Since we have next markets list:
# btceur, btcusd, btceth, btctrst, ethusd
# Since 3 of them has 'uS' in quote name (US Dollar, We Trust).
# We expect them to be returned in API response.
get '/api/v2/public/markets', params: { search: { quote_name: 'uS' } }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('id')).to contain_exactly('btcusd', 'btctrst', 'ethusd')
end
end
context 'complex filter' do
before do
Currency.find(:eur).update(visible: true)
create(:market, :btceur)
create(:market, :btctrst)
end
it 'filters by base_unit & quote_name or quote_code' do
# Since we have next markets list:
# btceur, btcusd, btceth, btctrst, ethusd
# 1. Filter by base_unit btc: btceur, btcusd, btceth, btctrst
# 2. Filter by quote_code or quote_name 'et': btceth, btctrst (Ethereum, WeTrust)
# We expect them to be returned in API response.
get '/api/v2/public/markets', params: { base_unit: :btc, search: { quote_name: 'et', quote_code: 'et' } }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('id')).to contain_exactly('btceth', 'btctrst')
end
it 'filters by base_unit & quote_code' do
# Since we have next markets list:
# btceur, btcusd, btceth, btctrst, ethusd
# 1. Filter by base_unit btc: btceur, btcusd, btceth, btctrst
# 2. Filter by quote_code 'et': btceth (eth)
# We expect them to be returned in API response.
get '/api/v2/public/markets', params: { base_unit: :btc, search: { quote_code: 'et' } }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.pluck('id')).to contain_exactly('btceth')
end
end
end
end
describe 'GET /api/v2/public/markets/:market/order_book' do
before do
create_list(:order_bid, 5, :btcusd)
create_list(:order_ask, 5, :btcusd)
end
let(:market) { :btcusd }
it 'returns ask and bid orders on specified market' do
get "/api/v2/public/markets/#{market}/order-book"
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 5
expect(result['bids'].size).to eq 5
end
context 'market name with dot' do
let!(:currency) { create(:currency, :xagm_cx) }
let!(:market) { create(:market, :xagm_cxusd) }
it 'returns information about specified market' do
get "/api/v2/public/markets/#{market.id}/order-book"
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 0
expect(result['bids'].size).to eq 0
end
end
it 'returns limited asks and bids' do
get "/api/v2/public/markets/#{market}/order-book", params: { asks_limit: 1, bids_limit: 1 }
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 1
expect(result['bids'].size).to eq 1
end
it 'validates market param' do
get "/api/v2/public/markets/somecoin/order-book", params: { asks_limit: 1, bids_limit: 1 }
expect(response).to have_http_status 422
expect(response).to include_api_error('public.market.doesnt_exist')
end
it 'validates asks limit' do
get "/api/v2/public/markets/somecoin/order-book", params: { asks_limit: 201, bids_limit: 1 }
expect(response).to have_http_status 422
expect(response).to include_api_error('public.order_book.invalid_ask_limit')
end
it 'validates bids limit' do
get "/api/v2/public/markets/somecoin/order-book", params: { asks_limit: 1, bids_limit: 201 }
expect(response).to have_http_status 422
expect(response).to include_api_error('public.order_book.invalid_bid_limit')
end
end
describe 'GET /api/v2/markets/:market/depth' do
before do
create_list(:order_bid, 5, :btcusd)
create_list(:order_bid, 5, :btcusd, price: 2)
create_list(:order_ask, 5, :btcusd)
create_list(:order_ask, 5, :btcusd, price: 3)
end
let(:asks) { [["1.0", "5.0"], ["3.0", "5.0"]] }
let(:bids) { [["2.0", "5.0"], ["1.0", "5.0"]] }
let(:market) { :btcusd }
context 'valid market param' do
it 'sorts asks and bids from highest to lowest' do
get "/api/v2/public/markets/#{market}/depth"
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks']).to eq asks
expect(result['bids']).to eq bids
end
end
context 'market name with dot' do
let!(:currency) { create(:currency, :xagm_cx) }
let!(:market) { create(:market, :xagm_cxusd) }
it 'returns information about specified market' do
get "/api/v2/public/markets/#{market.id}/depth"
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result['asks'].size).to eq 0
expect(result['bids'].size).to eq 0
end
end
context 'invalid market param' do
it 'validates market param' do
api_get "/api/v2/public/markets/usdusd/depth"
expect(response).to have_http_status 422
expect(response).to include_api_error('public.market.doesnt_exist')
end
end
end
describe 'GET /api/v2/public/markets/market/k-line' do
let(:points) do
# [timestamp, open_price, max_price, min_price, last_price, period_volume]
[[1537370460, 0.7079, 0.2204, 0.9794, 0.5273, 0.0747],
[1537370520, 0.6293, 0.5054, 0.2253, 0.1969, 0.7276],
[1537370580, 0.0939, 0.1949, 0.0032, 0.8328, 0.5895],
[1537370640, 0.6416, 0.0772, 0.7045, 0.7794, 0.6151],
[1537370700, 0.0566, 0.6377, 0.3007, 0.6855, 0.6976],
[1537370760, 0.7868, 0.6465, 0.3207, 0.6428, 0.1771],
[1537370820, 0.3318, 0.2124, 0.3773, 0.4274, 0.3473],
[1537370880, 0.0704, 0.4902, 0.5957, 0.5214, 0.3687],
[1537370940, 0.6629, 0.6585, 0.0756, 0.4559, 0.8554],
[1537371000, 0.6627, 0.6627, 0.2128, 0.0788, 0.2013],
[1537371060, 0.5165, 0.0435, 0.5228, 0.6447, 0.9237],
[1537371120, 0.9311, 0.8886, 0.1605, 0.3223, 0.0211],
[1537371180, 0.0704, 0.0103, 0.0325, 0.3846, 0.8957],
[1537371240, 0.1445, 0.6031, 0.9533, 0.0866, 0.4871],
[1537371300, 0.0974, 0.1344, 0.1533, 0.9029, 0.2009],
[1537371360, 0.2609, 0.9687, 0.0287, 0.4465, 0.7088],
[1537371420, 0.5671, 0.0576, 0.6617, 0.1041, 0.4942],
[1537371480, 0.8355, 0.5336, 0.7419, 0.7062, 0.9562],
[1537371540, 0.1805, 0.3577, 0.2768, 0.3162, 0.0209],
[1537371600, 0.7971, 0.1799, 0.8307, 0.5074, 0.0122],
[1537371660, 0.9491, 0.7448, 0.2019, 0.4662, 0.7035],
[1537371720, 0.8126, 0.3899, 0.8823, 0.8115, 0.6067],
[1537371780, 0.2632, 0.6558, 0.7411, 0.3894, 0.1509],
[1537371840, 0.4274, 0.8187, 0.6661, 0.4331, 0.6335],
[1537371900, 0.1356, 0.1787, 0.3081, 0.9549, 0.0723],
[1537371960, 0.1931, 0.9486, 0.2469, 0.2295, 0.9366],
[1537372020, 0.8323, 0.8168, 0.8453, 0.1278, 0.7975],
[1537372080, 0.5663, 0.1374, 0.0025, 0.0358, 0.6063],
[1537372140, 0.9296, 0.5443, 0.2732, 0.6434, 0.9173],
[1537372200, 0.7292, 0.0367, 0.3569, 0.7876, 0.6626],
[1537372260, 0.9979, 0.2182, 0.5141, 0.8984, 0.4512],
[1537372320, 0.4363, 0.4416, 0.2354, 0.6053, 0.7398],
[1537372380, 0.1815, 0.4969, 0.4091, 0.0798, 0.8797]]
end
let(:point_period) { KLineService::POINT_PERIOD_IN_SECONDS }
let(:points_default_limit) { 30 }
let(:last_point) { points.last }
let(:first_point) { points.first }
before { write_to_influx(points) }
after { delete_measurments("candles_1m") }
def influx_data(point)
{
values:
{
open: point[1],
high: point[2],
low: point[3],
close: point[4],
volume: point[5],
},
tags:
{
market: 'btcusd'
},
timestamp: point[0]
}
end
def write_to_influx(points)
points.each do |point|
Peatio::InfluxDB.client(epoch: 's').write_point('candles_1m', influx_data(point), 's')
end
end
def load_k_line(query = {})
api_get '/api/v2/public/markets/btcusd/k-line?' + query.to_query
expect(response).to have_http_status 200
end
def response_body
JSON.parse(response.body)
end
context 'data exists' do
it 'without time limits' do
load_k_line
expect(JSON.parse(response.body)).to eq points[-points_default_limit..-1]
end
context 'with time_from' do
it 'smaller than first point timestamp' do
load_k_line(time_from: first_point.first - 2 * point_period)
expect(response_body).to eq points[0...points_default_limit]
end
it 'bigger than last point timestamp' do
load_k_line(time_from: last_point.first + 2 * point_period)
expect(response_body).to eq []
end
it 'in range of first and last timestamp' do
time_from = first_point.first + 10 * point_period
load_k_line(time_from: time_from)
expect(response_body).to eq points[10..-1]
# First point timestamp should be eq to time_from.
expect(response_body.first.first).to eq time_from
time_from = first_point.first + 22 * point_period
load_k_line(time_from: time_from)
expect(response_body).to eq points[22..-1]
# First point timestamp should be eq to time_from.
expect(response_body.first.first).to eq time_from
end
end
context 'with time_to' do
it 'smaller than first point timestamp' do
load_k_line(time_to: first_point.first - 2 * point_period)
expect(response_body).to eq []
end
it 'bigger than last point timestamp' do
load_k_line(time_to: last_point.first + 2 * point_period)
# Returns (limit - 2) left points.
expect(response_body).to eq points[-points_default_limit..]
end
it 'in range of first and last timestamp' do
load_k_line(time_to: first_point.first + 1 * point_period)
expect(response_body).to eq points[0..1]
load_k_line(time_to: first_point.first + 20 * point_period)
expect(response_body).to eq points[0..20]
end
end
context 'with time_from and time_to' do
it 'time_to less than time_from' do
time_from = first_point.first + 2 * point_period
time_to = first_point.first - 2 * point_period
load_k_line(time_from: time_from, time_to: time_to)
expect(response_body).to eq []
end
it 'both less than first point timestamp' do
time_from = first_point.first - 10 * point_period
time_to = first_point.first - 4 * point_period
load_k_line(time_from: time_from, time_to: time_to)
expect(response_body).to eq []
end
it 'both bigger than last point timestamp' do
time_from = last_point.first + 2 * point_period
time_to = last_point.first + 12 * point_period
load_k_line(time_from: time_from, time_to: time_to)
expect(response_body).to eq []
end
it 'both in range of first and last timestamp' do
time_from = first_point.first + 10 * point_period
time_to = last_point.first - 10 * point_period
load_k_line(time_from: time_from, time_to: time_to)
# Points timestamps should be in range time_from..time_to (limit is bigger).
expect(response_body).to eq\
points.select { |p| p.first >= time_from && p.first <= time_to }
expect(response_body.first.first).to eq time_from
expect(response_body.last.first).to eq time_to
end
end
context 'with limit' do
it 'returns n last points' do
limit = 5
load_k_line(limit: limit)
expect(response_body).to eq points[-limit..-1]
limit = 10
load_k_line(limit: limit)
expect(response_body).to eq points[-limit..-1]
end
it 'returns all points if limit greater than points number' do
limit = points.length + 1
load_k_line(limit: limit)
expect(response_body).to eq points
end
end
context 'with limits, time_from and time_to' do
it 'ignores limit' do
time_from = first_point.first + 1 * point_period
time_to = last_point.first - 1 * point_period
limit = 5
load_k_line(time_from: time_from, time_to: time_to, limit: limit)
# All point in time_from..time_to including time_to (time_to - time_from) / 60 + 1.
expect(response_body.count).to eq (time_to - time_from) / 60 + 1
# Points timestamps should be in range time_from..time_to.
expect(response_body).to eq\
points.select { |p| p.first >= time_from && p.first <= time_to }
expect(response_body.first.first).to eq time_from
expect(response_body.last.first).to eq time_to
end
end
context 'with limits and time_from' do
it 'returns n right points from time_from (adds limit to time_from)' do
time_from = first_point.first + 5 * point_period
limit = 10
load_k_line(time_from: time_from, limit: limit)
expect(response_body.count).to eq limit
# Points timestamps should be bigger than time_from and we select first 10.
expect(response_body).to eq\
points.select { |p| p.first >= time_from }[0...limit]
expect(response_body.first.first).to eq time_from
end
end
end
context 'data is missing' do
before { delete_measurments("candles_1m") }
it 'without time_from' do
load_k_line
expect(JSON.parse(response.body)).to eq []
end
it 'with time_from' do
load_k_line(time_from: first_point.first)
expect(JSON.parse(response.body)).to eq []
end
it 'with time_from and time_to' do
load_k_line(time_from: first_point.first, time_to: last_point.first)
expect(JSON.parse(response.body)).to eq []
end
end
end
describe 'GET /api/v2/markets/tickers' do
after { delete_measurments("trades") }
context 'no trades executed yet' do
let(:expected_ticker) do
{ 'low' => '0.0', 'high' => '0.0',
'open' => '0.0', 'last' => '0.0',
'volume' => '0.0', 'vol' => '0.0', 'amount' => '0.0',
'avg_price' => '0.0', 'price_change_percent' => '+0.00%' }
end
it 'returns ticker of all markets' do
get '/api/v2/public/markets/tickers'
expect(response).to be_successful
expect(JSON.parse(response.body)['btcusd']['at']).not_to be_nil
expect(JSON.parse(response.body)['btcusd']['ticker']).to include(expected_ticker)
end
end
context 'single trade was executed' do
let!(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
let(:expected_ticker) do
{ 'low' => '5.0', 'high' => '5.0',
'open' => '5.0', 'last' => '5.0',
'volume' => '5.5', 'vol' => '5.5', 'amount' => '1.1',
'avg_price' => '5.0', 'price_change_percent' => '+0.00%' }
end
before do
trade.write_to_influx
end
it 'returns market tickers' do
get '/api/v2/public/markets/tickers'
expect(response).to be_successful
expect(JSON.parse(response.body)['btcusd']['at']).not_to be_nil
expect(JSON.parse(response.body)['btcusd']['ticker']).to include(expected_ticker)
end
end
context 'multiple trades were executed' do
let!(:trade1) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
let!(:trade2) { create(:trade, :btcusd, price: '6.0'.to_d, amount: '0.9'.to_d, total: '5.4'.to_d)}
let(:expected_ticker) do
{ 'low' => '5.0', 'high' => '6.0',
'open' => '5.0', 'last' => '6.0',
'vol' => '10.9', 'volume' => '10.9', 'amount' => '2.0',
'avg_price' => '5.45', 'price_change_percent' => '+20.00%' }
end
before do
trade1.write_to_influx
trade2.write_to_influx
end
it 'returns market tickers' do
get '/api/v2/public/markets/tickers'
expect(response).to be_successful
expect(JSON.parse(response.body)['btcusd']['at']).not_to be_nil
expect(JSON.parse(response.body)['btcusd']['ticker']).to include(expected_ticker)
end
end
end
describe 'GET /api/v2/public/markets/:market/tickers' do
after { delete_measurments("trades") }
context 'no trades executed yet' do
let(:expected_ticker) do
{ 'low' => '0.0', 'high' => '0.0',
'open' => '0.0', 'last' => '0.0',
'volume' => '0.0', 'vol' => '0.0', 'amount' => '0.0',
'avg_price' => '0.0', 'price_change_percent' => '+0.00%' }
end
it 'returns market tickers' do
get '/api/v2/public/markets/btcusd/tickers'
expect(response).to be_successful
expect(JSON.parse(response.body)['ticker']).to include(expected_ticker)
end
context 'market name with dot' do
let!(:currency) { create(:currency, :xagm_cx) }
let!(:market) { create(:market, :xagm_cxusd) }
it 'returns information about specified market' do
get "/api/v2/public/markets/#{market.id}/tickers"
expect(response).to be_successful
expect(JSON.parse(response.body)['ticker']).to include(expected_ticker)
end
end
end
context 'single trade was executed' do
let!(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
let(:expected_ticker) do
{ 'low' => '5.0', 'high' => '5.0',
'open' => '5.0', 'last' => '5.0',
'volume' => '5.5', 'vol' => '5.5', 'amount' => '1.1',
'avg_price' => '5.0', 'price_change_percent' => '+0.00%' }
end
before do
trade.write_to_influx
end
it 'returns market tickers' do
get '/api/v2/public/markets/btcusd/tickers'
expect(response).to be_successful
expect(JSON.parse(response.body)['ticker']).to include(expected_ticker)
end
end
context 'multiple trades were executed' do
let!(:trade1) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
let!(:trade2) { create(:trade, :btcusd, price: '6.0'.to_d, amount: '0.9'.to_d, total: '5.4'.to_d)}
# open = 6.0 because it takes last by default.
# to make it work correctly need to run k-line daemon.
let(:expected_ticker) do
{ 'low' => '5.0', 'high' => '6.0',
'open' => '5.0', 'last' => '6.0',
'vol' => '10.9', 'volume' => '10.9', 'amount' => '2.0',
'avg_price' => '5.45', 'price_change_percent' => '+20.00%' }
end
before do
trade1.write_to_influx
trade2.write_to_influx
end
it 'returns market tickers' do
get '/api/v2/public/markets/btcusd/tickers'
expect(response).to be_successful
expect(JSON.parse(response.body)['ticker']).to include(expected_ticker)
end
end
end
describe 'GET /api/v2/public/markets/#{market}/trades' 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(:ask) do
create(
:order_ask,
:btcusd,
price: '12.32'.to_d,
volume: '123.12345678',
member: member
)
end
let(:bid) do
create(
:order_bid,
:btcusd,
price: '12.32'.to_d,
volume: '123.12345678',
member: member
)
end
let(:market) { :btcusd }
let!(:ask_trade) { create(:trade, :btcusd, maker_order: ask, created_at: 2.days.ago) }
let!(:bid_trade) { create(:trade, :btcusd, taker_order: bid, created_at: 1.day.ago) }
after do
delete_measurments('trades')
end
before do
ask_trade.write_to_influx
bid_trade.write_to_influx
end
it 'returns all recent trades' do
get "/api/v2/public/markets/#{market}/trades"
expect(response).to be_successful
expect(JSON.parse(response.body).size).to eq 2
end
context 'market name with dot' do
let!(:currency) { create(:currency, :xagm_cx) }
let!(:market) { create(:market, :xagm_cxusd) }
it 'returns information about specified market' do
get "/api/v2/public/markets/#{market.id}/trades"
expect(response).to be_successful
expect(JSON.parse(response.body).size).to eq 0
end
end
it 'returns 1 trade' do
get "/api/v2/public/markets/#{market}/trades", params: {limit: 1}
expect(response).to be_successful
expect(JSON.parse(response.body).size).to eq 1
end
it 'sorts trades in reverse creation order' do
get "/api/v2/public/markets/#{market}/trades"
expect(response).to be_successful
expect(JSON.parse(response.body).first['id']).to eq bid_trade.id
end
it 'gets trades by limit' do
trade = create(:trade, :btcusd, taker_order: bid, created_at: 6.hours.ago)
trade.write_to_influx
get "/api/v2/public/markets/#{market}/trades", params: { limit: 2, order_by: 'asc'}
expect(response).to be_successful
expect(JSON.parse(response.body).count).to eq 2
get "/api/v2/public/markets/#{market}/trades", params: { market: 'btcusd', limit: 3, order_by: 'asc' }
expect(response).to be_successful
expect(JSON.parse(response.body).count).to eq 3
end
it 'validates market param' do
api_get "/api/v2/public/markets/usdusd/trades"
expect(response).to have_http_status 422
expect(response).to include_api_error('public.market.doesnt_exist')
end
it 'validates limit param' do
get "/api/v2/public/markets/#{market}/trades", params: { limit: 1001 }
expect(response).to have_http_status 422
expect(response).to include_api_error('public.trade.invalid_limit')
end
end
end

View File

@@ -0,0 +1,12 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Public::MemberLevels, type: :request do
describe 'GET /member_levels' do
it 'responds with 200 and returns correct data' do
api_get '/api/v2/public/member-levels'
expect(response).to be_successful
expect(response.body).to eq '{"deposit":{"minimum_level":3},"withdraw":{"minimum_level":3},"trading":{"minimum_level":3}}'
end
end
end

View File

@@ -0,0 +1,39 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Public::Tools, type: :request do
describe '/timestamp' do
it 'returns current time in seconds' do
now = Time.now
get '/api/v2/public/timestamp'
expect(response).to be_successful
expect(JSON.parse(response.body)).to be_between(now.iso8601, (now + 1).iso8601)
end
end
describe '/health' do
it 'returns successful liveness probe' do
get '/api/v2/public/health/alive'
expect(response).to be_successful
end
it 'returns failed liveness probe' do
Market.stubs(:connected?).returns(false)
get '/api/v2/public/health/alive'
expect(response).to have_http_status(503)
end
it 'returns successful readiness probe' do
get '/api/v2/public/health/ready'
expect(response).to be_successful
end
it 'returns failed readiness probe' do
Bunny.stubs(:run).returns(false)
get '/api/v2/public/health/alive'
expect(response).to have_http_status(503)
end
end
end

View File

@@ -0,0 +1,50 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Public::TradingFees, type: :request do
before(:each) { clear_redis }
describe 'GET /trading_fees' do
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' do
api_get '/api/v2/public/trading_fees'
expect(response.status).to eq 200
expect(JSON.parse(response.body).length).to eq TradingFee.count
end
it 'pagination' do
api_get '/api/v2/public/trading_fees', params: { limit: 1 }
expect(JSON.parse(response.body).length).to eq 1
end
it 'filters by market_id' do
api_get '/api/v2/public/trading_fees', params: { market_id: 'btcusd' }
result = JSON.parse(response.body)
expect(result.map { |r| r['market_id'] }).to all eq 'btcusd'
expect(result.length).to eq TradingFee.where(market_id: 'btcusd').count
end
it 'filters by group' do
api_get '/api/v2/public/trading_fees', params: { group: 'vip-0' }
result = JSON.parse(response.body)
expect(result.map { |r| r['group'] }).to all eq 'vip-0'
expect(result.length).to eq TradingFee.where(group: 'vip-0').count
end
it 'capitalized fee group' do
api_get '/api/v2/public/trading_fees', params: { group: 'Vip-0' }
result = JSON.parse(response.body)
expect(result.map { |r| r['group'] }).to all eq 'vip-0'
expect(result.length).to eq TradingFee.where(group: 'vip-0').count
end
end
end

View File

@@ -0,0 +1,193 @@
# frozen_string_literal: true
describe API::V2::Public::Webhooks, type: :request do
describe 'GET /webhooks/:event' do
let(:member) { create(:member) }
let(:transaction) do
Peatio::Transaction.new(
currency_id: :eth,
hash: '0xa049b0202ba078caa723c6b59594247b0c9f33e24878950f8537cedff9ea20ac',
amount: 0.5,
to_address: '0x1ef338196bd0207ba4852ba7a6847eed59331b84',
block_number: 16880960,
txout: 0,
status: :success
)
end
let(:invlaid_transaction) do
Peatio::Transaction.new(
currency_id: :eth,
hash: '0xa049b0202ba078caa723c6b59594247b0c9f33e24878950f8537cedff9ea20ac',
amount: 0.5,
to_address: '0x1ef338196bd0207ba4852ba7a6847eed59331b85',
block_number: 16880960,
txout: 0
)
end
let!(:wallet) { create(:wallet, :eth_deposit, name: 'Bitgo Deposit',
gateway: :bitgo, settings:
{ uri: 'http://localhost',
secret: 'changeme',
wallet_id: '5e4d43680f39a6710435b74edba4e2c2',
access_token: 'changeme',
testnet: false }) }
let(:request_body) {
{ 'event' => 'deposit',
'id' => '5e539be5e6715b2006c7bfa6278aa3f4',
'type' => 'transfer',
'wallet' => '5e4d43680f39a6710435b74edba4e2c2',
'url' => 'http://localhost.com',
'hash' => '0xa049b0202ba078caa723c6b59594247b0c9f33e24878950f8537cedff9ea20ac',
'coin' => 'eth',
'transfer' => '5e4e824894d4902c060f20c28b161fa8',
'state' => 'new',
'simulation' => 'true',
'retries' => '0',
'webhook' => '5e5399ddda65833f06cd53429bcbca83',
'updatedAt' => '2020-02-24T09:48:21.795Z',
'version' => '2' }
}
context 'nonexistent wallet' do
let(:request_body) {
{ 'event' => 'deposit',
'id' => '5e539be5e6715b2006c7bfa6278aa3f4',
'type' => 'transfer',
'wallet' => 'changeme',
'url' => 'http://localhost.com',
'hash' => '0xa049b0202ba078caa723c6b59594247b0c9f33e24878950f8537cedff9ea20ac',
'coin' => 'eth',
'transfer' => '5e4e824894d4902c060f20c28b161fa8',
'state' => 'new',
'simulation' => 'true',
'retries' => '0',
'webhook' => '5e5399ddda65833f06cd53429bcbca83',
'updatedAt' => '2020-02-24T09:48:21.795Z',
'version' => '2' }
}
it 'doesnt create deposit and return 200' do
expect do
api_post '/api/v2/public/webhooks/deposit', params: request_body
expect(response.status).to eq 200
end.not_to change { Deposit.count }
end
end
context 'valid webhook callback' do
before do
member.get_account(:eth).payment_addresses.create(currency_id: :eth, address: '0x1ef338196bd0207ba4852ba7a6847eed59331b84')
WalletService.any_instance.stubs(:trigger_webhook_event).with(request_body).returns({ transfers: [transaction] })
end
it 'creates new deposit' do
api_post '/api/v2/public/webhooks/deposit', params: request_body
expect(response.status).to eq 200
expect(Deposit.last.txid).to eq('0xa049b0202ba078caa723c6b59594247b0c9f33e24878950f8537cedff9ea20ac')
end
context 'process second time' do
it 'doesnt create deposit for same transfer' do
api_post '/api/v2/public/webhooks/deposit', params: request_body
expect do
api_post '/api/v2/public/webhooks/deposit', params: request_body
expect(response.status).to eq 200
expect(Deposit.last.txid).to eq('0xa049b0202ba078caa723c6b59594247b0c9f33e24878950f8537cedff9ea20ac')
end.not_to change { Deposit.count }
end
end
context 'process undefined transfer' do
before do
WalletService.any_instance.stubs(:trigger_webhook_event).with(request_body).returns({ transfers: [invlaid_transaction] })
end
it 'doesnt create deposit and return 200' do
expect do
api_post '/api/v2/public/webhooks/deposit', params: request_body
expect(response.status).to eq 200
end.not_to change { Deposit.count }
end
end
end
context 'adapter raises error invalid' do
before do
member.get_account(:eth).payment_addresses.create(currency_id: :eth, address: '0x1ef338196bd0207ba4852ba7a6847eed59331b84')
WalletService.any_instance.stubs(:trigger_webhook_event).with(request_body).raises(Peatio::Wallet::ClientError.new('something went wrong'))
end
it 'returns error' do
api_post '/api/v2/public/webhooks/deposit', params: request_body
expect(response.status).to eq 422
expect(response).to include_api_error('public.webhook.cannot_perfom_transfer')
end
end
context 'address confirmation event' do
let(:request_body) {
{ 'event' => 'deposit',
'address' => '0xa049b0202ba078caa723c6b59594247b0c9f33e24878950f8537cedff9ea20ac',
'type' => 'address_confirmation',
'walletId' => '5e4e824894d4902c060f20c28b161fa8',
'hash' => '0xa049b0202ba078caa723c6b59594247b0c9f33e24878950f8537cedff9ea20ac',
}
}
context 'valid webhook callback' do
context 'update payment address' do
before do
member.get_account(:eth).payment_addresses.create(currency_id: :eth, address: nil, details: { address_id: 'address_id' })
WalletService.any_instance.stubs(:trigger_webhook_event).with(request_body).returns({ address_id: 'address_id', currency_id: 'eth' })
end
it 'should create address for member' do
api_post '/api/v2/public/webhooks/deposit', params: request_body
expect(response.status).to eq 200
expect(member.get_account(:eth).payment_addresses[0].address).to eq request_body['address']
end
end
context 'skip payment address' do
before do
member.get_account(:eth).payment_addresses.create(currency_id: :eth, address: request_body['address'], details: { address_id: 'address_id' })
WalletService.any_instance.stubs(:trigger_webhook_event).with(request_body).returns({ address_id: 'address_id', currency_id: 'eth' })
end
it 'should not update address if address already exists' do
api_post '/api/v2/public/webhooks/deposit', params: request_body
expect(response.status).to eq 200
expect(member.get_account(:eth).payment_addresses[0].created_at).to eq member.get_account(:eth).payment_addresses[0].updated_at
end
end
end
context 'adapter raises error invalid' do
before do
member.get_account(:eth).payment_addresses.create(currency_id: :eth, address: nil, details: { address_id: 'address_id' })
WalletService.any_instance.stubs(:trigger_webhook_event).with(request_body).raises(Peatio::Wallet::ClientError.new('something went wrong'))
end
it 'returns error' do
api_post '/api/v2/public/webhooks/deposit', params: request_body
expect(response.status).to eq 422
expect(response).to include_api_error('public.webhook.cannot_perfom_address_confirmation')
end
end
end
end
end

14
spec/factories/account.rb Normal file
View File

@@ -0,0 +1,14 @@
# encoding: UTF-8
# frozen_string_literal: true
module AccountFactory
def create_account(*arguments)
currency = Symbol === arguments.first ? arguments.first : :usd
attributes = arguments.extract_options!
attributes.delete(:member) { create(:member) }.get_account(currency).tap do |account|
account.update!(attributes)
end
end
end
RSpec.configure { |config| config.include AccountFactory }

View File

@@ -0,0 +1,15 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :adjustment do
reason { Faker::Coffee.blend_name }
description { Faker::Coffee.notes }
category { 'asset_registration' }
amount { Faker::Number.positive }
currency_id { Currency.ids.sample }
creator { create(:member) }
asset_account_code { 102 }
receiving_account_number { "BTC-#{[402, 302].sample}" }
end
end

View File

@@ -0,0 +1,40 @@
# frozen_string_literal: true
FactoryBot.define do
sequence(:coin_beneficiary_data) do
{ address: Faker::Blockchain::Ethereum.address }
end
sequence(:fiat_beneficiary_data) do
{ full_name: Faker::Name.name_with_middle,
address: Faker::Address.full_address,
country: Faker::Address.country,
account_number: Faker::Bank.account_number,
account_type: %w[saving checking money-market CDs retirement].sample,
bank_name: Faker::Bank.name,
bank_address: Faker::Address.full_address,
bank_country: Faker::Address.country,
bank_swift_code: Faker::Bank.swift_bic,
intermediary_bank_name: Faker::Bank.name,
intermediary_bank_address: Faker::Address.full_address,
intermediary_bank_country: Faker::Address.country,
intermediary_bank_swift_code: Faker::Bank.swift_bic }
end
factory :beneficiary do
member { create(:member) }
currency { Currency.all.sample }
name { Faker::Company.name }
description { Faker::Company.catch_phrase }
state { 'pending' }
data do
# Use save navigation operator for cases when currency is nil.
if currency&.coin?
generate(:coin_beneficiary_data)
elsif currency&.fiat?
generate(:fiat_beneficiary_data).merge(currency: currency.id)
end
end
end
end

View File

@@ -0,0 +1,63 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :blockchain do
trait 'eth-rinkeby' do
key { 'eth-rinkeby' }
name { 'Ethereum Rinkeby' }
client { 'geth' }
server { 'http://127.0.0.1:8545' }
height { 2500000 }
min_confirmations { 6 }
explorer_address { 'https://etherscan.io/address/#{address}' }
explorer_transaction { 'https://etherscan.io/tx/#{txid}' }
status { 'active' }
end
trait 'eth-kovan' do
key { 'eth-kovan' }
name { 'Ethereum Kovan' }
client { 'parity' }
server { 'http://127.0.0.1:8545' }
height { 2500000 }
min_confirmations { 6 }
explorer_address { 'https://kovan.etherscan.io/address/#{address}' }
explorer_transaction { 'https://kovan.etherscan.io/tx/#{txid}' }
status { 'active' }
end
trait 'eth-mainet' do
key { 'eth-mainet' }
name { 'Ethereum Mainet' }
client { 'geth' }
server { 'http://127.0.0.1:8545' }
height { 2500000 }
min_confirmations { 4 }
explorer_address { 'https://etherscan.io/address/#{address}' }
explorer_transaction { 'https://etherscan.io/tx/#{txid}' }
status { 'disabled' }
end
trait 'btc-testnet' do
key { 'btc-testnet' }
name { 'Bitcoin Testnet' }
client { 'bitcoin' }
server { 'http://127.0.0.1:18332' }
height { 1350000 }
min_confirmations { 1 }
explorer_address { 'https://blockchain.info/address/#{address}' }
explorer_transaction { 'https://blockchain.info/tx/#{txid}' }
status { 'active' }
end
trait 'fake-testnet' do
key { 'fake-testnet' }
name { 'Fake Testnet' }
client { 'fake' }
height { 1 }
status { 'active' }
end
end
end

View File

@@ -0,0 +1,139 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :currency do
trait :usd do
code { 'usd' }
name { 'US Dollar' }
type { 'fiat' }
precision { 2 }
withdraw_limit_24h { 100 }
withdraw_limit_72h { 1000 }
withdraw_fee { 0.1 }
position { 1 }
options { {} }
end
trait :eur do
code { 'eur' }
name { 'Euro' }
type { 'fiat' }
precision { 8 }
withdraw_limit_24h { 100 }
withdraw_limit_72h { 1000 }
withdraw_fee { 0.1 }
position { 2 }
visible { false }
options { {} }
end
trait :btc do
blockchain_key { 'btc-testnet' }
code { 'btc' }
name { 'Bitcoin' }
type { 'coin' }
base_factor { 100_000_000 }
withdraw_limit_24h { 0.1 }
withdraw_limit_72h { 1 }
withdraw_fee { 0.01 }
position { 3 }
options { {} }
end
trait :eth do
blockchain_key { 'eth-rinkeby' }
code { 'eth' }
name { 'Ethereum' }
type { 'coin' }
base_factor { 1_000_000_000_000_000_000 }
withdraw_limit_24h { 0.1 }
withdraw_limit_72h { 1 }
withdraw_fee { 0.025 }
position { 4 }
options do
{ gas_limit: 21_000,
gas_price: 1_000_000_000 }
end
end
trait :trst do
blockchain_key { 'eth-rinkeby' }
code { 'trst' }
name { 'WeTrust' }
type { 'coin' }
parent_id { 'eth' }
base_factor { 1_000_000 }
withdraw_limit_24h { 100 }
withdraw_limit_72h { 1000 }
withdraw_fee { 0.025 }
position { 5 }
options do
{ gas_limit: 90_000,
gas_price: 1_000_000_000,
erc20_contract_address: '0x87099adD3bCC0821B5b151307c147215F839a110' }
end
end
trait :tom do
blockchain_key { 'eth-rinkeby' }
code { 'tom' }
name { 'TOM' }
type { 'coin' }
parent_id { 'eth' }
base_factor { 1_000_000 }
withdraw_limit_24h { 100 }
withdraw_limit_72h { 1000 }
withdraw_fee { 0.025 }
position { 5 }
options do
{ gas_limit: 90_000,
gas_price: 1_000_000_000,
erc20_contract_address: '0xf7970499814654cd13cb7b6e7634a12a7a8a9abc' }
end
end
trait :ring do
blockchain_key { 'eth-kovan' }
code { 'ring' }
name { 'Evolution Land Global Token' }
type { 'coin' }
parent_id { 'eth' }
base_factor { 1_000_000 }
withdraw_limit_24h { 100 }
withdraw_limit_72h { 1000 }
withdraw_fee { 0.025 }
position { 6 }
options \
{ { erc20_contract_address: '0xf8720eb6ad4a530cccb696043a0d10831e2ff60e' } }
end
trait :fake do
blockchain_key { 'fake-testnet' }
code { 'fake' }
name { 'Fake Coin' }
type { 'coin' }
base_factor { 1_000_000 }
withdraw_limit_24h { 100 }
withdraw_limit_72h { 1000 }
withdraw_fee { 0.02 }
position { 7 }
options { {} }
end
trait :xagm_cx do
blockchain_key { 'eth-rinkeby' }
code { 'xagm.cx' }
name { 'XAGm.cx' }
type { 'coin' }
parent_id { 'eth' }
base_factor { 1_000_000 }
withdraw_limit_24h { 100 }
withdraw_limit_72h { 1000 }
withdraw_fee { 0.02 }
position { 8 }
visible { true }
options { {} }
end
end
end

View File

View File

@@ -0,0 +1,56 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :deposit do
member { create(:member, :level_3) }
amount { Kernel.rand(100..10_000).to_d }
factory :deposit_btc, class: Deposits::Coin do
currency { Currency.find(:btc) }
address { Faker::Blockchain::Bitcoin.address }
txid { Faker::Lorem.characters(64) }
txout { 0 }
block_number { rand(1..1349999) }
end
factory :deposit_usd, class: Deposits::Fiat do
currency { Currency.find(:usd) }
end
trait :deposit_btc do
type { Deposits::Coin }
currency { Currency.find(:btc) }
address { Faker::Blockchain::Bitcoin.address }
txid { Faker::Lorem.characters(64) }
txout { 0 }
end
trait :deposit_eth do
type { Deposits::Coin }
currency { Currency.find(:eth) }
member { create(:member, :level_3, :barong) }
address { Faker::Blockchain::Bitcoin.address }
txid { Faker::Lorem.characters(64) }
txout { 0 }
end
trait :deposit_trst do
type { Deposits::Coin }
currency { Currency.find(:trst) }
member { create(:member, :level_3, :barong) }
address { Faker::Blockchain::Bitcoin.address }
txid { Faker::Lorem.characters(64) }
txout { 0 }
end
trait :deposit_ring do
type { Deposits::Coin }
currency { Currency.find(:ring) }
member { create(:member, :level_3, :barong) }
address { Faker::Blockchain::Bitcoin.address }
txid { Faker::Lorem.characters(64) }
txout { 0 }
end
end
end

9
spec/factories/engine.rb Normal file
View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :engine do
name { Faker::Company.unique.bs.strip.downcase }
driver { 'peatio' }
state { 'online' }
end
end

View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
FactoryBot.define do
factory :internal_transfer_btc, class: InternalTransfer do
trait :with_deposit_liability do
before(:create) do |internal_tranfer|
deposit = create(:deposit_btc, member: internal_tranfer.sender, amount: internal_tranfer.amount)
deposit.accept!
deposit.process!
deposit.dispatch!
end
end
currency { Currency.find(:btc) }
amount { 10.to_d }
sender { create(:member, :level_3) }
receiver { create(:member, :level_3) }
state { 'completed' }
end
factory :internal_transfer_usd, class: InternalTransfer do
trait :with_deposit_liability do
before(:create) do |internal_tranfer|
create(:deposit_usd, member: internal_tranfer.sender, amount: internal_tranfer.amount)
.accept!
end
end
currency { Currency.find(:usd) }
amount { 1000.to_d }
sender { create(:member, :level_3) }
receiver { create(:member, :level_3) }
state { 'completed' }
end
end

79
spec/factories/market.rb Normal file
View File

@@ -0,0 +1,79 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :market do
engine { create(:engine) }
trait :btcusd do
id { 'btcusd' }
base_currency { 'btc' }
quote_currency { 'usd' }
amount_precision { 8 }
price_precision { 2 }
min_price { 0.01 }
min_amount { 0.00000001 }
position { 1 }
state { :enabled }
end
trait :btceth do
id { 'btceth' }
base_currency { 'btc' }
quote_currency { 'eth' }
amount_precision { 4 }
price_precision { 6 }
min_price { 0.000001 }
min_amount { 0.0001 }
position { 2 }
state { :enabled }
end
trait :btceur do
id { 'btceur' }
base_currency { 'btc' }
quote_currency { 'eur' }
amount_precision { 8 }
price_precision { 2 }
min_price { 0.01 }
min_amount { 0.00000001 }
position { 3 }
state { :enabled }
end
trait :ethusd do
id { 'ethusd' }
base_currency { 'eth' }
quote_currency { 'usd' }
amount_precision { 6 }
price_precision { 4 }
min_price { 0.01 }
min_amount { 0.0001 }
position { 4 }
state { :enabled }
end
trait :btctrst do
id { 'btctrst' }
base_currency { 'btc' }
quote_currency { 'trst' }
amount_precision { 6 }
price_precision { 4 }
min_price { 0.01 }
min_amount { 0.0001 }
position { 5 }
state { :enabled }
end
trait :xagm_cxusd do
id { 'xagm.cxusd' }
base_currency { 'xagm.cx' }
quote_currency { 'usd' }
amount_precision { 6 }
price_precision { 4 }
min_price { 0.01 }
min_amount { 0.0001 }
position { 4 }
state { :enabled }
end
end
end

39
spec/factories/member.rb Normal file
View File

@@ -0,0 +1,39 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :member do
email { Faker::Internet.email }
level { 0 }
uid { "ID#{Faker::Number.unique.hexadecimal(10)}".upcase }
role { "member" }
group { "vip-0" }
state { "active" }
trait :level_3 do
level { 3 }
end
trait :level_2 do
level { 2 }
end
trait :level_1 do
level { 1 }
end
trait :level_0 do
level { 0 }
end
trait :admin do
role { "admin" }
end
trait :barong do
level { 3 }
end
factory :admin_member, traits: %i[admin]
end
end

View File

@@ -0,0 +1,58 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :operation do
currency { Currency.all.sample }
credit { Kernel.rand(10..1000).to_d }
trait :debit do
debit { Kernel.rand(10..1000).to_d }
credit { 0 }
end
reference_type { %w[order deposit trade].sample }
created_at { Faker::Date.between(3.days.ago, Date.today) }
end
factory :asset, class: Operations::Asset, parent: :operation do
code do
Operations::Account.find_by(type: :asset,
currency_type: currency.type).code
end
end
factory :expense, class: Operations::Expense, parent: :operation do
code do
Operations::Account.find_by(type: :expense,
currency_type: currency.type).code
end
end
factory :revenue, class: Operations::Revenue, parent: :operation do
code do
Operations::Account.find_by(type: :revenue,
currency_type: currency.type).code
end
end
factory :liability, class: Operations::Liability, parent: :operation do
code do
Operations::Account.find_by(type: :liability,
currency_type: currency.type,
kind: :main).code
end
trait :with_member do
member { create(:member, :level_3) }
# Update legacy balance.
after(:create) do |liability|
acc = liability.member.get_account(liability.currency)
acc.plus_funds(liability.credit) unless liability.credit.zero?
acc.sub_funds(liability.debit) unless liability.debit.zero?
end
end
end
end

View File

@@ -0,0 +1,97 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
# TODO: Rename this factory to account once we drop legacy accounts.
factory :operations_account, class: Operations::Account do
trait '101' do
code { 101 }
type { :asset }
kind { :main }
currency_type { :fiat }
description { 'Main Fiat Assets Account' }
scope { :platform }
end
trait '102' do
code { 102 }
type { :asset }
kind { :main }
currency_type { :coin }
description { 'Main Crypto Assets Account' }
scope { :platform }
end
trait '201' do
code { 201 }
type { :liability }
kind { :main }
currency_type { :fiat }
description { 'Main Fiat Liabilities Account' }
scope { :member }
end
trait '202' do
code { 202 }
type { :liability }
kind { :main }
currency_type { :coin }
description { 'Main Crypto Liabilities Account' }
scope { :member }
end
trait '211' do
code { 211 }
type { :liability }
kind { :locked }
currency_type { :fiat }
description { 'Locked Fiat Liabilities Account' }
scope { :member }
end
trait '212' do
code { 212 }
type { :liability }
kind { :locked }
currency_type { :coin }
description { 'Locked Crypto Liabilities Account' }
scope { :member }
end
trait '301' do
code { 301 }
type { :revenue }
kind { :main }
currency_type { :fiat }
description { 'Main Fiat Revenues Account' }
scope { :platform }
end
trait '302' do
code { 302 }
type { :revenue }
kind { :main }
currency_type { :coin }
description { 'Main Crypto Revenues Account' }
scope { :platform }
end
trait '401' do
code { 401 }
type { :expense }
kind { :main }
currency_type { :fiat }
description { 'Main Fiat Expenses Account' }
scope { :platform }
end
trait '402' do
code { 402 }
type { :expense }
kind { :main }
currency_type { :coin }
description { 'Main Crypto Expenses Account' }
scope { :platform }
end
end
end

110
spec/factories/orders.rb Normal file
View File

@@ -0,0 +1,110 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :order_bid do
# Create liability history by passing with_deposit_liability trait.
trait :with_deposit_liability do
before(:create) do |order|
deposit = create(:deposit_usd, member: order.member, amount: order.locked)
deposit.accept!
deposit.process!
deposit.dispatch!
end
bid { :usd }
ask { :btc }
market { Market.find(:btcusd) }
state { :wait }
ord_type { 'limit' }
price { '1'.to_d }
volume { '1'.to_d }
origin_volume { volume.to_d }
locked { price.to_d * volume.to_d }
origin_locked { locked.to_d }
member { create(:member) }
end
trait :btcusd do
bid { :usd }
ask { :btc }
market { Market.find(:btcusd) }
state { :wait }
ord_type { 'limit' }
price { '1'.to_d }
volume { '1'.to_d }
origin_volume { volume.to_d }
locked { price.to_d * volume.to_d }
origin_locked { locked.to_d }
member { create(:member) }
end
trait :btceth do
bid { :eth }
ask { :btc }
market { Market.find(:btceth) }
state { :wait }
ord_type { 'limit' }
price { '1'.to_d }
volume { '1'.to_d }
origin_volume { volume.to_d }
locked { price.to_d * volume.to_d }
origin_locked { locked.to_d }
member { create(:member) }
end
end
factory :order_ask do
# Create liability history by passing with_deposit_liability trait.
trait :with_deposit_liability do
before(:create) do |order|
deposit = create(:deposit_btc, member: order.member, amount: order.locked)
deposit.accept!
deposit.process!
deposit.dispatch!
end
bid { :usd }
ask { :btc }
market { Market.find(:btcusd) }
state { :wait }
ord_type { 'limit' }
price { '1'.to_d }
volume { '1'.to_d }
origin_volume { volume.to_d }
locked { volume.to_d }
origin_locked { locked.to_d }
member { create(:member) }
end
trait :btcusd do
bid { :usd }
ask { :btc }
market { Market.find(:btcusd) }
state { :wait }
ord_type { 'limit' }
price { '1'.to_d }
volume { '1'.to_d }
origin_volume { volume.to_d }
locked { volume.to_d }
origin_locked { locked.to_d }
member { create(:member) }
end
trait :btceth do
bid { :eth }
ask { :btc }
market { Market.find(:btceth) }
state { :wait }
ord_type { 'limit' }
price { '1'.to_d }
volume { '1'.to_d }
origin_volume { volume.to_d }
locked { volume.to_d }
origin_locked { locked.to_d }
member { create(:member) }
end
end
end

View File

@@ -0,0 +1,40 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :payment_address do
address { Faker::Blockchain::Bitcoin.address }
member { create(:member, :level_3) }
wallet { Wallet.joins(:currencies).find_by(currencies: { id: 'usd' }) }
trait :btc_address do
member { create(:member, :level_3) }
wallet { Wallet.joins(:currencies).find_by(currencies: { id: 'btc' }) }
end
trait :eth_address do
member { create(:member, :level_3) }
wallet { Wallet.joins(:currencies).find_by(currencies: { id: 'eth' }) }
end
trait :trst_address do
member { create(:member, :level_3) }
wallet { Wallet.joins(:currencies).find_by(currencies: { id: 'trst' }) }
end
trait :ring_address do
member { create(:member, :level_3) }
wallet { Wallet.joins(:currencies).find_by(currencies: { id: 'ring' }) }
end
trait :ltc_address do
member { create(:member, :level_3) }
wallet { Wallet.joins(:currencies).find_by(currencies: { id: 'ltc' }) }
end
factory :btc_payment_address, traits: [:btc_address]
factory :eth_payment_address, traits: [:eth_address]
factory :trst_payment_address, traits: [:trst_address]
factory :ring_payment_address, traits: [:ring_address]
end
end

View File

@@ -0,0 +1,18 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :stats_member_pnl do
member { create(:member, :level_3) }
currency_id { Currency.ids.sample }
pnl_currency_id { Currency.ids.sample }
total_credit { 0 }
total_debit_fees { 0 }
total_credit_fees { 0 }
total_debit { 0 }
total_credit_value { 0 }
total_debit_value { 0 }
total_balance_value { 0 }
average_balance_price { 0 }
end
end

34
spec/factories/trade.rb Normal file
View File

@@ -0,0 +1,34 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :trade do
trait :btcusd do
price { '10.0'.to_d }
amount { '1.0'.to_d }
total { price.to_d * amount.to_d }
market { Market.find(:btcusd) }
maker_order { create(:order_ask, :btcusd) }
taker_order { create(:order_bid, :btcusd) }
maker { maker_order.member }
taker { taker_order.member }
end
trait :btceth do
price { '10.0'.to_d }
amount { '1.0'.to_d }
total { price.to_d * amount.to_d }
market { Market.find(:btceth) }
maker_order { create(:order_ask, :btceth) }
taker_order { create(:order_bid, :btceth) }
maker { maker_order.member }
taker { taker_order.member }
end
# Create liability history for orders by passing with_deposit_liability trait.
trait :with_deposit_liability do
maker_order { create(:order_ask, :with_deposit_liability) }
taker_order { create(:order_bid, :with_deposit_liability) }
end
end
end

View File

@@ -0,0 +1,22 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
sequence(:fee) do
Kernel.rand(TradingFee::MIN_FEE..TradingFee::MAX_FEE).to_d
end
sequence(:group) do |n|
"vip-#{n}"
end
factory :trading_fee do
maker { generate(:fee) }
taker { generate(:fee) }
group { generate(:group) }
trait :with_market do
market { Market.all.sample }
end
end
end

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
FactoryBot.define do
factory :transaction do
currency { Currency.all.sample }
txid { Faker::Lorem.characters(64) }
from_address { Faker::Blockchain::Bitcoin.address }
to_address { Faker::Blockchain::Bitcoin.address }
amount { Kernel.rand(100..10_000).to_d }
end
end

View File

@@ -0,0 +1,51 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
sequence :transfer_key do
"transfer_#{Faker::Number.unique.number(5).to_i}"
end
factory :transfer do
key { generate(:transfer_key) }
category { Transfer::CATEGORIES.sample }
description { "#{category} for #{Time.now.to_date}" }
trait :with_assets do
after(:create) do |t|
assets_number = Faker::Number.between(1, 5).to_i
create_list(:asset, assets_number, reference: t)
end
end
trait :with_expenses do
after(:create) do |t|
expense_number = Faker::Number.between(1, 5).to_i
create_list(:expense, expense_number, reference: t)
end
end
trait :with_liabilities do
after(:create) do |t|
liabilities_number = Faker::Number.between(1, 5).to_i
create_list(:liability, liabilities_number, :with_member, reference: t)
end
end
trait :with_revenues do
after(:create) do |t|
revenues_number = Faker::Number.between(1, 5).to_i
create_list(:revenue, revenues_number, reference: t)
end
end
trait :with_operations do
with_assets
with_expenses
with_liabilities
with_revenues
end
factory :transfer_with_operations, traits: %i[with_operations]
end
end

View File

@@ -0,0 +1,10 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :trigger do
order { create(:order_ask, :with_deposit_liability, :btcusd)}
order_type { Trigger::TYPES.keys.sample }
value { 1.1.to_d }
end
end

213
spec/factories/wallet.rb Normal file
View File

@@ -0,0 +1,213 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :wallet do
trait :eth_deposit do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'eth', wallet_id: w.id)
end
blockchain_key { 'eth-rinkeby' }
name { 'Ethereum Deposit Wallet' }
address { '0x828058628DF254Ebf252e0b1b5393D1DED91E369' }
kind { 'deposit' }
max_balance { 0.0 }
status { 'active' }
gateway { 'geth' }
uri { 'http://127.0.0.1:8545' }
secret { 'changeme' }
end
trait :eth_hot do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'eth', wallet_id: w.id)
end
blockchain_key { 'eth-rinkeby' }
name { 'Ethereum Hot Wallet' }
address { '0xb6a61c43DAe37c0890936D720DC42b5CBda990F9' }
kind { 'hot' }
max_balance { 100.0 }
status { 'active' }
gateway { 'geth' }
uri { 'http://127.0.0.1:8545' }
secret { 'changeme' }
end
trait :eth_warm do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'eth', wallet_id: w.id)
end
blockchain_key { 'eth-rinkeby' }
name { 'Ethereum Warm Wallet' }
address { '0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C' }
kind { 'warm' }
max_balance { 1000.0 }
status { 'active' }
gateway { 'geth' }
uri { 'http://127.0.0.1:8545' }
secret { 'changeme' }
end
trait :eth_cold do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'eth', wallet_id: w.id)
end
blockchain_key { 'eth-rinkeby' }
name { 'Ethereum Cold Wallet' }
address { '0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C' }
kind { 'cold' }
max_balance { 1000.0 }
status { 'active' }
gateway { 'geth' }
uri { 'http://127.0.0.1:8545' }
secret { 'changeme' }
end
trait :eth_fee do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'eth', wallet_id: w.id)
end
blockchain_key { 'eth-rinkeby' }
name { 'Ethereum Fee Wallet' }
address { '0x45a31b15a2ab8a8477375b36b6f5a0c63733dce8' }
kind { 'fee' }
max_balance { 1000.0 }
status { 'active' }
gateway { 'geth' }
uri { 'http://127.0.0.1:8545' }
secret { 'changeme' }
end
trait :trst_deposit do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'trst', wallet_id: w.id)
end
blockchain_key { 'eth-rinkeby' }
name { 'Trust Coin Deposit Wallet' }
address { '0x828058628DF254Ebf252e0b1b5393D1DED91E369' }
kind { 'deposit' }
max_balance { 0.0 }
status { 'active' }
gateway { 'geth' }
uri { 'http://127.0.0.1:8545' }
secret { 'changeme' }
end
trait :trst_hot do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'trst', wallet_id: w.id)
end
blockchain_key { 'eth-rinkeby' }
name { 'Trust Coin Hot Wallet' }
address { '0xb6a61c43DAe37c0890936D720DC42b5CBda990F9' }
kind { 'hot' }
max_balance { 100.0 }
status { 'active' }
gateway { 'geth' }
uri { 'http://127.0.0.1:8545' }
secret { 'changeme' }
end
trait :btc_deposit do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'btc', wallet_id: w.id)
end
blockchain_key { 'btc-testnet' }
name { 'Bitcoin Deposit Wallet' }
address { '3DX3Ak4751ckkoTFbYSY9FEQ6B7mJ4furT' }
kind { 'deposit' }
max_balance { 0.0 }
status { 'active' }
gateway { 'bitcoind' }
uri { 'http://127.0.0.1:18332' }
secret { 'changeme' }
end
trait :btc_hot do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'btc', wallet_id: w.id)
end
blockchain_key { 'btc-testnet' }
name { 'Bitcoin Hot Wallet' }
address { '3NwYr8JxjHG2MBkgdBiHCxStSWDzyjS5U8' }
kind { 'hot' }
max_balance { 500.0 }
status { 'active' }
gateway { 'bitcoind' }
uri { 'http://127.0.0.1:18332' }
secret { 'changeme' }
end
trait :fake_deposit do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'fake', wallet_id: w.id)
end
blockchain_key { 'fake-testnet' }
name { 'Fake Currency Deposit Wallet' }
address { 'fake-deposit' }
kind { 'deposit' }
max_balance { 0.0 }
status { 'active' }
gateway { 'fake' }
uri { 'http://127.0.0.1:18881' }
end
trait :fake_hot do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'fake', wallet_id: w.id)
end
blockchain_key { 'fake-testnet' }
name { 'Fake Currency Hot Wallet' }
address { 'fake-hot' }
kind { 'hot' }
max_balance { 10.0 }
status { 'active' }
gateway { 'fake' }
uri { 'http://127.0.0.1:18881' }
end
trait :fake_warm do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'fake', wallet_id: w.id)
end
blockchain_key { 'fake-testnet' }
name { 'Fake Currency Warm Wallet' }
address { 'fake-warm' }
kind { 'warm' }
max_balance { 100.0 }
status { 'active' }
gateway { 'fake' }
uri { 'http://127.0.0.1:18881' }
end
trait :fake_cold do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'fake', wallet_id: w.id)
end
blockchain_key { 'fake-testnet' }
name { 'Fake Currency Cold Wallet' }
address { 'fake-cold' }
kind { 'cold' }
max_balance { 1000.0 }
status { 'active' }
gateway { 'fake' }
uri { 'http://127.0.0.1:18881' }
end
trait :fake_fee do
after(:create) do |w|
CurrencyWallet.create(currency_id: 'fake', wallet_id: w.id)
end
blockchain_key { 'fake-testnet' }
name { 'Fake Currency Fee Wallet' }
address { 'fake-fee' }
kind { 'fee' }
max_balance { 1000.0 }
status { 'active' }
gateway { 'fake' }
uri { 'http://127.0.0.1:8545' }
secret { 'changeme' }
end
end
end

View File

@@ -0,0 +1,41 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :whitelisted_smart_contract do
trait :address_1 do
id { 1 }
blockchain_key { 'eth-rinkeby' }
address { '0xbbd602bb278edff65cbc967b9b62095ad5be23a3' }
state { 'active' }
end
trait :address_2 do
id { 2 }
blockchain_key { 'eth-rinkeby' }
address { '0xe3cb6897d83691a8eb8458140a1941ce1d6e6dac' }
state { 'active' }
end
trait :address_3 do
id { 3 }
blockchain_key { 'eth-rinkeby' }
address { '0x4b6a630ff1f66604d31952bdce2e4950efc99821' }
state { 'active' }
end
trait :address_4 do
id { 4 }
blockchain_key { 'eth-rinkeby' }
address { '0xc4d276bf32b71cdddb18f3b4d258f057a5ffda03' }
state { 'active' }
end
trait :address_5 do
id { 5 }
blockchain_key { 'eth-rinkeby' }
address { '0x87099add3bcc0821b5b151307c147215f839a110' }
state { 'active' }
end
end
end

View File

@@ -0,0 +1,67 @@
# encoding: UTF-8
# frozen_string_literal: true
# Legacy withdraw factories are deprecated because they update
# account balance in database without creating liability operation.
#
# Use new withdraw factories instead.
# You can create liability history by passing with_deposit_liability trait.
#
# TODO: Add new factories for all currencies.
FactoryBot.define do
factory :btc_withdraw, class: Withdraws::Coin do
# We need to have valid Liability-based balance to spend funds.
trait :with_deposit_liability do
before(:create) do |withdraw|
deposit = create(:deposit_btc, member: withdraw.member, amount: withdraw.sum)
deposit.accept!
deposit.process!
deposit.dispatch!
end
end
trait :with_beneficiary do
beneficiary do
create(:beneficiary,
currency: currency,
member: member,
state: :active)
end
rid { nil }
end
currency { Currency.find(:btc) }
member { create(:member, :level_3) }
rid { Faker::Blockchain::Bitcoin.address }
sum { 10.to_d }
type { 'Withdraws::Coin' }
end
factory :usd_withdraw, class: Withdraws::Fiat do
# We need to have valid Liability-based balance to spend funds.
trait :with_deposit_liability do
before(:create) do |withdraw|
create(:deposit_usd, member: withdraw.member, amount: withdraw.sum)
.accept!
end
end
trait :with_beneficiary do
beneficiary do
create(:beneficiary,
currency: currency,
member: member,
state: :active)
end
rid { nil }
end
member { create(:member, :level_3) }
currency { Currency.find(:usd) }
rid { Faker::Bank.iban }
sum { 1000.to_d }
type { 'Withdraws::Fiat' }
end
end

View File

@@ -0,0 +1,11 @@
# encoding: UTF-8
# frozen_string_literal: true
FactoryBot.define do
factory :withdraw_limit do
group { 'any' }
kyc_level { 'any' }
limit_24_hour { 9999.to_d }
limit_1_month { 999_999.to_d }
end
end

View File

@@ -0,0 +1,851 @@
# frozen_string_literal: true
describe Jobs::Cron::StatsMemberPnl do
let!(:member_platform) { create(:member, :level_3) }
let!(:member) { create(:member, :level_3) }
let!(:maker) { create(:member, role: 'maker') }
let(:maker2) { create(:member, role: 'maker') }
include ::API::V2::Management::Helpers
def create_transfer(transfer_attrs)
Transfer.transaction do
attrs = transfer_attrs.slice(:key, :category, :description)
transfer_attrs[:operations].each do |op_pair|
currency = Currency.find(op_pair[:currency])
debit_op = op_pair[:account_src].merge(debit: op_pair[:amount], credit: 0.0, currency: currency)
credit_op = op_pair[:account_dst].merge(credit: op_pair[:amount], debit: 0.0, currency: currency)
[debit_op, credit_op].each do |op|
klass = ::Operations.klass_for(code: op[:code])
uid = op.delete(:uid)
op.merge!(member: Member.find_by!(uid: uid)) if uid.present?
type = ::Operations::Account.find_by(code: op[:code]).type
type_plural = type.pluralize
if attrs[type_plural].present?
attrs[type_plural].push(klass.new(op))
else
attrs[type_plural] = [klass.new(op)]
end
end
end
Transfer.create!(attrs)
end
end
before(:each) do
Jobs::Cron::StatsMemberPnl.stubs(:exclude_roles).returns(['maker'])
end
context 'conversion_market' do
it 'when there is no market' do
expect do
Jobs::Cron::StatsMemberPnl.conversion_market('test1', 'btc')
end.to raise_error('There is no market test1/btc')
end
it 'when market exists' do
market = Market.first
expect(Jobs::Cron::StatsMemberPnl.conversion_market(market.base_unit, market.quote_unit)).to eq market.id
end
end
context 'price_at' do
after { delete_measurments('trades') }
let!(:coin_deposit) { create(:deposit, :deposit_btc) }
let!(:liability) { create(:liability, member: member, credit: 0.4, reference_type: 'Deposit', reference_id: coin_deposit.id) }
it 'when there is no trades' do
market = Market.find_by(base_unit: 'btc', quote_unit: 'usd')
expect do
Jobs::Cron::StatsMemberPnl.price_at(coin_deposit.currency_id, market.quote_unit, liability.created_at)
end.to raise_error("There is no trades on market #{coin_deposit.currency_id}#{market.quote_unit}")
end
context 'when trade exist' do
let(:trade) { create(:trade, :btceth, price: '5.0'.to_d, amount: '1.9'.to_d, total: '5.5'.to_d) }
before do
trade.write_to_influx
end
it 'return trade price' do
res = Jobs::Cron::StatsMemberPnl.price_at(coin_deposit.currency_id, trade.market.quote_unit, trade.created_at + 3.hours)
expect(res).to eq trade.price
end
it 'when pnl currency id equal to currency id' do
res = Jobs::Cron::StatsMemberPnl.price_at(coin_deposit.currency_id, coin_deposit.currency_id, trade.created_at + 3.hours)
expect(res).to eq 1.0
end
end
end
context 'process currency' do
before(:each) do
StatsMemberPnl.delete_all
end
context 'reference type withdraw' do
before do
[member, maker].each do |m|
m.touch_accounts
m.accounts.map { |a| a.update(balance: 500) }
end
end
context 'creates one pnl' do
let!(:coin_withdraw) { create(:btc_withdraw, sum: 0.3.to_d, amount: 0.2.to_d, aasm_state: 'succeed', member: member) }
let!(:coin_withdraw_maker) { create(:btc_withdraw, sum: 0.3.to_d, amount: 0.2.to_d, aasm_state: 'succeed', member: maker) }
let!(:pnl) { create(:stats_member_pnl) }
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(123)
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
end
it do
expect { Jobs::Cron::StatsMemberPnl.process() }.to change { StatsMemberPnl.count }.by(1)
expect(StatsMemberPnl.last.member_id).to eq coin_withdraw.member_id
expect(StatsMemberPnl.last.currency_id).to eq coin_withdraw.currency_id
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.last.total_credit).to eq 0
expect(StatsMemberPnl.last.total_debit).to eq coin_withdraw.amount
expect(StatsMemberPnl.last.total_debit_value).to eq((coin_withdraw.amount + coin_withdraw.fee) * 123)
expect(StatsMemberPnl.last.total_debit_fees).to eq coin_withdraw.fee
expect(StatsMemberPnl.last.total_credit_fees).to eq 0
expect(StatsMemberPnl.last.total_credit_value).to eq 0
expect(StatsMemberPnl.last.total_balance_value).to eq 0
expect(StatsMemberPnl.last.average_balance_price).to eq 0
end
end
context 'calculation on existing pnl' do
let!(:coin_withdraw) { create(:btc_withdraw, amount: 0.2.to_d, aasm_state: 'succeed', member: member) }
let!(:pnl) do
create(:stats_member_pnl, currency_id: coin_withdraw.currency_id, pnl_currency_id: 'eth', total_debit: 0.1,
total_debit_fees: 0.01, total_debit_value: 0.3,
member_id: coin_withdraw.member_id)
end
let!(:liability) do
create(:liability, id: 2, member_id: coin_withdraw.member_id, currency_id: coin_withdraw.currency_id,
debit: 0.3, reference_type: 'Withdraw', code: 212, reference_id: coin_withdraw.id)
end
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0)
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(0)
expect(StatsMemberPnl.last.member_id).to eq coin_withdraw.member_id
expect(StatsMemberPnl.last.currency_id).to eq coin_withdraw.currency_id
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.last.total_credit).to eq 0
expect(StatsMemberPnl.last.total_debit).to eq coin_withdraw.amount + pnl.total_debit
expect(StatsMemberPnl.last.total_debit_value).to eq (coin_withdraw.amount + coin_withdraw.fee) * 1.0 + pnl.total_debit_value
expect(StatsMemberPnl.last.total_debit_fees).to eq coin_withdraw.fee + pnl.total_debit_fees
expect(StatsMemberPnl.last.total_credit_fees).to eq 0
expect(StatsMemberPnl.last.total_credit_value).to eq 0
expect(StatsMemberPnl.last.total_balance_value).to eq 0
expect(StatsMemberPnl.last.average_balance_price).to eq 0
end
end
end
context 'reference type adjustments' do
context 'creates one pnl with positive adjustment' do
let!(:member) { create(:member) }
let!(:adjustment) { create(:adjustment, currency_id: 'btc', amount: 1.0, receiving_account_number: "btc-202-#{member.uid}") }
let!(:adjustment_maker) { create(:adjustment, currency_id: 'btc', amount: 1.0, receiving_account_number: "btc-202-#{maker.uid}") }
let(:btceth_price) { 100.0 }
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(btceth_price)
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
adjustment.accept!(validator: member)
adjustment_maker.accept!(validator: member)
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(1)
expect(StatsMemberPnl.last.member_id).to eq member.id
expect(StatsMemberPnl.last.currency_id).to eq adjustment.currency_id
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.last.total_credit).to eq adjustment.amount
expect(StatsMemberPnl.last.total_debit).to eq 0
expect(StatsMemberPnl.last.total_debit_value).to eq 0
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
expect(StatsMemberPnl.last.total_credit_fees).to eq 0
expect(StatsMemberPnl.last.total_credit_value).to eq adjustment.amount * btceth_price
expect(StatsMemberPnl.last.total_balance_value).to eq adjustment.amount * btceth_price
expect(StatsMemberPnl.last.average_balance_price).to eq btceth_price
end
end
context 'creates one pnl with positive and negative adjustments' do
let(:member) { create(:member) }
let(:adjustment) { create(:adjustment, currency_id: 'btc', amount: 1.0, receiving_account_number: "btc-202-#{member.uid}") }
let(:btceth_price) { 100.0 }
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(btceth_price)
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
adjustment.accept!(validator: member)
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(1)
expect(StatsMemberPnl.last.member_id).to eq member.id
expect(StatsMemberPnl.last.currency_id).to eq adjustment.currency_id
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.last.total_credit).to eq adjustment.amount
expect(StatsMemberPnl.last.total_debit).to eq 0
expect(StatsMemberPnl.last.total_debit_value).to eq 0
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
expect(StatsMemberPnl.last.total_credit_fees).to eq 0
expect(StatsMemberPnl.last.total_credit_value).to eq adjustment.amount * btceth_price
expect(StatsMemberPnl.last.total_balance_value).to eq adjustment.amount * btceth_price
expect(StatsMemberPnl.last.average_balance_price).to eq btceth_price
half = 1.to_d / 2
adjustment2 = create(:adjustment, currency_id: 'btc', amount: -half, receiving_account_number: "btc-202-#{member.uid}")
adjustment2.accept!(validator: member)
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(0)
expect(StatsMemberPnl.last.member_id).to eq member.id
expect(StatsMemberPnl.last.currency_id).to eq adjustment.currency_id
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.last.total_credit).to eq adjustment.amount
expect(StatsMemberPnl.last.total_debit).to eq 0.5
expect(StatsMemberPnl.last.total_debit_value).to eq 50
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
expect(StatsMemberPnl.last.total_credit_fees).to eq 0
expect(StatsMemberPnl.last.total_credit_value).to eq 1.0 * btceth_price
expect(StatsMemberPnl.last.total_balance_value).to eq half * btceth_price
expect(StatsMemberPnl.last.average_balance_price).to eq btceth_price
end
end
end
context 'reference type deposit' do
context 'creates one pnl' do
let!(:coin_deposit) { create(:deposit, :deposit_btc) }
let!(:coin_deposit_maker) { create(:deposit, :deposit_btc, member: maker) }
let!(:pnl) { create(:stats_member_pnl) }
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0)
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
[coin_deposit, coin_deposit_maker].each do |d|
d.accept!
d.process!
end
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(1)
expect(StatsMemberPnl.last.member_id).to eq coin_deposit.member_id
expect(StatsMemberPnl.last.currency_id).to eq coin_deposit.currency_id
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.last.total_credit).to eq coin_deposit.amount
expect(StatsMemberPnl.last.total_debit).to eq 0
expect(StatsMemberPnl.last.total_debit_value).to eq 0
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
expect(StatsMemberPnl.last.total_credit_fees).to eq coin_deposit.fee
expect(StatsMemberPnl.last.total_credit_value).to eq coin_deposit.amount * 1.0
expect(StatsMemberPnl.last.total_balance_value).to eq coin_deposit.amount * 1.0
expect(StatsMemberPnl.last.average_balance_price).to eq 1.0
end
end
context 'creates several pnls' do
let!(:coin_deposit) { create(:deposit, :deposit_btc) }
let!(:fiat_deposit) { create(:deposit_usd, amount: 190.0) }
let!(:pnl) { create(:stats_member_pnl) }
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0)
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
fiat_deposit.accept!
coin_deposit.accept!
coin_deposit.process!
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(2)
stats_member_pnl_btc = StatsMemberPnl.find_by(currency_id: coin_deposit.currency_id, member: coin_deposit.member)
stats_member_pnl_usd = StatsMemberPnl.find_by(currency_id: fiat_deposit.currency_id, member: fiat_deposit.member)
expect(stats_member_pnl_btc.member_id).to eq coin_deposit.member_id
expect(stats_member_pnl_btc.pnl_currency_id).to eq 'eth'
expect(stats_member_pnl_btc.currency_id).to eq coin_deposit.currency_id
expect(stats_member_pnl_btc.total_credit).to eq coin_deposit.amount
expect(stats_member_pnl_btc.total_debit).to eq 0
expect(stats_member_pnl_btc.total_debit_value).to eq 0
expect(stats_member_pnl_btc.total_debit_fees).to eq 0
expect(stats_member_pnl_btc.total_credit_fees).to eq coin_deposit.fee
expect(stats_member_pnl_btc.total_credit_value).to eq coin_deposit.amount * 1.0
expect(stats_member_pnl_btc.total_balance_value).to eq coin_deposit.amount * 1.0
expect(stats_member_pnl_btc.average_balance_price).to eq 1.0
expect(stats_member_pnl_usd.member_id).to eq fiat_deposit.member_id
expect(stats_member_pnl_usd.pnl_currency_id).to eq 'eth'
expect(stats_member_pnl_usd.currency_id).to eq fiat_deposit.currency_id
expect(stats_member_pnl_usd.total_credit).to eq fiat_deposit.amount
expect(stats_member_pnl_usd.total_debit).to eq 0
expect(stats_member_pnl_usd.total_debit_value).to eq 0
expect(stats_member_pnl_usd.total_debit_fees).to eq 0
expect(stats_member_pnl_usd.total_credit_fees).to eq fiat_deposit.fee
expect(stats_member_pnl_usd.total_credit_value).to eq fiat_deposit.amount * 1.0
expect(stats_member_pnl_usd.total_balance_value).to eq fiat_deposit.amount * 1.0
expect(stats_member_pnl_usd.average_balance_price).to eq 1.0
end
end
context 'calculation on existing pnl' do
let!(:coin_deposit) { create(:deposit, :deposit_btc, amount: '0.1'.to_d) }
let!(:pnl) do
create(:stats_member_pnl, currency_id: coin_deposit.currency_id, pnl_currency_id: 'eth', total_credit: 0.1,
total_credit_fees: '0.01'.to_d, total_credit_value: '0.3'.to_d, total_balance_value: '0.3'.to_d, member_id: coin_deposit.member_id)
end
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0.to_f)
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
coin_deposit.accept!
coin_deposit.process!
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(0)
expect(StatsMemberPnl.last.member_id).to eq coin_deposit.member_id
expect(StatsMemberPnl.last.pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.last.currency_id).to eq coin_deposit.currency_id
expect(StatsMemberPnl.last.total_credit).to eq(coin_deposit.amount + pnl.total_credit)
expect(StatsMemberPnl.last.total_credit_fees).to eq(coin_deposit.fee + pnl.total_credit_fees)
expect(StatsMemberPnl.last.total_credit_value).to eq(pnl.total_credit_value + coin_deposit.amount * 1.0)
expect(StatsMemberPnl.last.total_debit).to eq 0
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
expect(StatsMemberPnl.last.total_debit_value).to eq 0
expect(StatsMemberPnl.last.total_balance_value).to eq(pnl.total_credit_value + coin_deposit.amount * 1.0)
expect(StatsMemberPnl.last.average_balance_price).to eq((pnl.total_balance_value + coin_deposit.amount * 1.0) / (coin_deposit.amount + pnl.total_credit - pnl.total_debit))
end
end
end
context 'reference type trade' do
context 'calculation on existing pnl' do
let!(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d) }
let(:btceth_price) { 123 }
let!(:pnl1) do
create(:stats_member_pnl, pnl_currency_id: 'eth', currency_id: 'btc',
total_credit: 2.0, total_credit_fees: 0.2, total_credit_value: 11.0, total_balance_value: 11.0, total_debit: 0.02,
average_balance_price: 5.5,
total_debit_value: 1.0, member_id: trade.maker_order.member.id)
end
let!(:pnl2) do
create(:stats_member_pnl, pnl_currency_id: 'eth', currency_id: 'usd',
total_credit: 0.1, total_credit_fees: 0.01, total_credit_value: 0.3, total_debit: 0.2,
total_debit_value: 10.0, member_id: trade.maker_order.member.id)
end
let!(:pnl3) do
create(:stats_member_pnl, pnl_currency_id: 'eth', currency_id: 'usd',
total_credit: 0.4, total_credit_fees: 0.01, total_credit_value: 0.3, total_debit: 0.2,
average_balance_price: 0.1,
total_debit_value: 10.0, member_id: trade.taker_order.member.id)
end
let!(:pnl4) do
create(:stats_member_pnl, pnl_currency_id: 'eth', currency_id: 'btc',
total_credit: 0.4, total_credit_fees: 0.01, total_credit_value: 0.3,
total_debit: 0.2, total_debit_value: 10.0, member_id: trade.taker_order.member.id)
end
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(btceth_price)
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(0)
total_fees = trade.total * trade.order_fee(trade.maker_order)
expect(StatsMemberPnl.all[0].member_id).to eq trade.maker_order.member.id
expect(StatsMemberPnl.all[0].pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.all[0].currency_id).to eq pnl1.currency_id
expect(StatsMemberPnl.all[0].total_credit).to eq pnl1.total_credit
expect(StatsMemberPnl.all[0].total_credit_fees).to eq pnl1.total_credit_fees
expect(StatsMemberPnl.all[0].total_debit).to eq trade.amount + pnl1.total_debit
expect(StatsMemberPnl.all[0].total_debit_value).to eq pnl1.total_debit_value + trade.amount * btceth_price
expect(StatsMemberPnl.all[0].total_credit_value).to eq pnl1.total_credit_value
expect(StatsMemberPnl.all[0].total_balance_value).to eq(pnl1.total_balance_value - trade.amount * pnl1.average_balance_price)
expect(StatsMemberPnl.all[1].member_id).to eq trade.maker_order.member.id
expect(StatsMemberPnl.all[1].pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.all[1].currency_id).to eq pnl2.currency_id
expect(StatsMemberPnl.all[1].total_credit).to eq trade.total - total_fees + pnl2.total_credit
expect(StatsMemberPnl.all[1].total_credit_fees).to eq total_fees + + pnl2.total_credit_fees
expect(StatsMemberPnl.all[1].total_debit).to eq pnl2.total_debit
expect(StatsMemberPnl.all[1].total_debit_value).to eq pnl2.total_debit_value
expect(StatsMemberPnl.all[1].total_credit_value).to eq pnl2.total_credit_value + (trade.total - total_fees) * btceth_price
expect(StatsMemberPnl.all[1].total_balance_value).to eq pnl2.total_balance_value + (trade.total - total_fees) * btceth_price
total_fees = trade.amount * trade.order_fee(trade.taker_order)
expect(StatsMemberPnl.all[2].member_id).to eq trade.taker_order.member.id
expect(StatsMemberPnl.all[2].pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.all[2].currency_id).to eq pnl3.currency_id
expect(StatsMemberPnl.all[2].total_credit).to eq pnl3.total_credit
expect(StatsMemberPnl.all[2].total_debit).to eq trade.total + pnl3.total_debit
expect(StatsMemberPnl.all[2].total_debit_value).to eq trade.total * btceth_price + pnl3.total_debit_value
expect(StatsMemberPnl.all[2].total_credit_fees).to eq pnl3.total_credit_fees
expect(StatsMemberPnl.all[2].total_credit_value).to eq pnl3.total_credit_value
expect(StatsMemberPnl.all[2].total_balance_value).to eq(0)
expect(StatsMemberPnl.all[3].member_id).to eq trade.taker_order.member.id
expect(StatsMemberPnl.all[3].pnl_currency_id).to eq 'eth'
expect(StatsMemberPnl.all[3].currency_id).to eq pnl4.currency_id
expect(StatsMemberPnl.all[3].total_credit).to eq trade.amount - total_fees + pnl4.total_credit
expect(StatsMemberPnl.all[3].total_debit).to eq pnl4.total_debit
expect(StatsMemberPnl.all[3].total_debit_value).to eq pnl4.total_debit_value
expect(StatsMemberPnl.all[3].total_credit_fees).to eq total_fees + pnl4.total_credit_fees
expect(StatsMemberPnl.all[3].total_credit_value).to eq pnl4.total_credit_value + (trade.amount - total_fees) * btceth_price
expect(StatsMemberPnl.all[3].total_balance_value).to eq pnl4.total_balance_value + (trade.amount - total_fees) * btceth_price
end
end
context 'creates pnls while executing 1 trade' do
let!(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d) }
let!(:pnl) { create(:stats_member_pnl) }
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0.to_f)
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(4)
total_fees = trade.total * trade.order_fee(trade.maker_order)
pnl1 = StatsMemberPnl.find_by(member_id: trade.maker_order.member.id, currency_id: trade.maker_order.income_currency.id, pnl_currency_id: 'eth')
expect(pnl1.total_credit).to eq trade.total - total_fees
expect(pnl1.total_debit).to eq 0
expect(pnl1.total_debit_value).to eq 0
expect(pnl1.total_credit_fees).to eq total_fees
expect(pnl1.total_credit_value).to eq (trade.total - total_fees) * 1.0
pnl2 = StatsMemberPnl.find_by(member_id: trade.maker_order.member.id, currency_id: trade.maker_order.outcome_currency.id, pnl_currency_id: 'eth')
expect(pnl2.total_credit).to eq 0
expect(pnl2.total_debit).to eq trade.amount
expect(pnl2.total_debit_value).to eq trade.amount * 1.0
expect(pnl2.total_credit_fees).to eq 0
expect(pnl2.total_credit_value).to eq 0
total_fees = trade.amount * trade.order_fee(trade.taker_order)
pnl3 = StatsMemberPnl.find_by(member_id: trade.taker_order.member.id, currency_id: trade.taker_order.income_currency.id, pnl_currency_id: 'eth')
expect(pnl3.total_credit).to eq trade.amount - total_fees
expect(pnl3.total_debit).to eq 0
expect(pnl3.total_debit_value).to eq 0
expect(pnl3.total_credit_fees).to eq total_fees
expect(pnl3.total_credit_value).to eq (trade.amount - total_fees) * 1.0
pnl4 = StatsMemberPnl.find_by(member_id: trade.taker_order.member.id, currency_id: trade.taker_order.outcome_currency.id, pnl_currency_id: 'eth')
expect(pnl4.total_credit).to eq 0
expect(pnl4.total_debit).to eq trade.total
expect(pnl4.total_debit_value).to eq trade.total * 1.0
expect(pnl4.total_credit_fees).to eq 0
expect(pnl4.total_credit_value).to eq 0
end
end
context 'trades of makers should not create pnls' do
let!(:trade) do
create(
:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d,
maker_order: create(:order_bid, :btceth, member: maker),
taker_order: create(:order_ask, :btceth, member: maker)
)
create(
:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d,
maker_order: create(:order_bid, :btceth, member: maker),
taker_order: create(:order_ask, :btceth, member: maker2)
)
end
let!(:pnl) { create(:stats_member_pnl) }
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0.to_f)
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('eth')])
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(0)
end
end
end
end
context 'process' do
before do
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Market.first.quote_unit, Market.second.quote_unit].map{|id| Currency.find(id)})
end
context 'no liabilities' do
it do
Jobs::Cron::StatsMemberPnl.process
expect(StatsMemberPnl.count).to eq 0
end
end
context 'liability for reference type deposit' do
let!(:coin_deposit) { create(:deposit, :deposit_btc) }
let!(:pnl) { create(:stats_member_pnl) }
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0.to_f)
coin_deposit.accept!
coin_deposit.process!
coin_deposit.dispatch!
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(2)
expect(StatsMemberPnl.second.member_id).to eq coin_deposit.member_id
expect(StatsMemberPnl.second.currency_id).to eq coin_deposit.currency_id
expect(StatsMemberPnl.second.pnl_currency_id).to eq Market.first.quote_unit
expect(StatsMemberPnl.second.total_credit).to eq coin_deposit.amount
expect(StatsMemberPnl.second.total_debit).to eq 0
expect(StatsMemberPnl.second.total_debit_value).to eq 0
expect(StatsMemberPnl.second.total_credit_fees).to eq coin_deposit.fee
expect(StatsMemberPnl.second.total_debit_fees).to eq 0
expect(StatsMemberPnl.second.total_credit_value).to eq coin_deposit.amount * 1.0
expect(StatsMemberPnl.last.member_id).to eq coin_deposit.member_id
expect(StatsMemberPnl.last.currency_id).to eq coin_deposit.currency_id
expect(StatsMemberPnl.last.pnl_currency_id).to eq Market.second.quote_unit
expect(StatsMemberPnl.last.total_credit).to eq coin_deposit.amount
expect(StatsMemberPnl.last.total_debit).to eq 0
expect(StatsMemberPnl.last.total_debit_value).to eq 0
expect(StatsMemberPnl.last.total_credit_fees).to eq coin_deposit.fee
expect(StatsMemberPnl.last.total_debit_fees).to eq 0
expect(StatsMemberPnl.last.total_credit_value).to eq coin_deposit.amount * 1.0
end
end
context 'liability for reference type deposit (not yet collected)' do
let!(:coin_deposit) { create(:deposit, :deposit_btc) }
let!(:pnl) { create(:stats_member_pnl) }
before do
Jobs::Cron::StatsMemberPnl.stubs(:price_at).returns(1.0.to_f)
coin_deposit.accept!
coin_deposit.process!
end
it do
expect { Jobs::Cron::StatsMemberPnl.process }.to change { StatsMemberPnl.count }.by(2)
end
end
end
context 'scenario1_internal_sell' do
before do
Jobs::Cron::StatsMemberPnl.stubs(:pnl_currencies).returns([Currency.find('usd')])
end
def scenario1_internal_sell_with_partial_refund(msrc, mdst)
key = (Time.now.to_f * 1000).to_i
create(:deposit_usd, member: mdst, amount: 100).accept!
d = create(:deposit_btc, member: msrc, amount: 0.09)
d.accept!
d.process!
d.dispatch!
transfers_attr = [
{
key: key,
category: Transfer::CATEGORIES_MAPPING[:purchases],
operations: [
{
currency: :usd,
amount: 100,
account_src: {
code: 201,
uid: mdst.uid
},
account_dst: {
code: 211,
uid: mdst.uid
}
}
]
},
{
key: key + 1,
category: Transfer::CATEGORIES_MAPPING[:purchases],
operations: [
{
# Refund (unlock) user 10 usd
currency: :usd,
amount: 10,
account_src: {
code: 211,
uid: mdst.uid
},
account_dst: {
code: 201,
uid: mdst.uid
}
},
{
# Transfer 89 usd from user to the platform
currency: :usd,
amount: 89,
account_src: {
code: 211,
uid: mdst.uid
},
account_dst: {
code: 201,
uid: msrc.uid
}
},
{
# Transfer 1 usd from user to the platform fees
currency: :usd,
amount: 1,
account_src: {
code: 211,
uid: mdst.uid
},
account_dst: {
code: 301,
uid: mdst.uid
}
},
{
# Transfer 0.09 btc from the platform to the user
currency: :btc,
amount: 0.09,
account_src: {
code: 202,
uid: msrc.uid
},
account_dst: {
code: 202,
uid: mdst.uid
}
}
]
}
]
transfers_attr.each do |transfer_attrs|
create_transfer(transfer_attrs)
end
end
it 'excludes makers' do
scenario1_internal_sell_with_partial_refund(maker2, maker)
Jobs::Cron::StatsMemberPnl.process
expect(StatsMemberPnl.count).to eq(0)
end
it do
scenario1_internal_sell_with_partial_refund(member_platform, member)
Jobs::Cron::StatsMemberPnl.stubs(:price_at).with('usd', 'usd', anything).returns(1)
Jobs::Cron::StatsMemberPnl.stubs(:price_at).with('btc', 'usd', anything).returns(10_000)
Jobs::Cron::StatsMemberPnl.process
expect(StatsMemberPnl.count).to eq(4)
musd = StatsMemberPnl.find_by(member_id: member.id, pnl_currency_id: 'usd', currency_id: 'usd')
expect(musd.total_credit).to eq(100)
expect(musd.total_credit_fees).to eq(0)
expect(musd.total_debit_fees).to eq(1)
expect(musd.total_debit).to eq(89)
expect(musd.total_credit_value).to eq(100)
expect(musd.total_debit_value).to eq(89)
expect(musd.total_balance_value).to eq(10)
expect(musd.average_balance_price).to eq(1)
mbtc = StatsMemberPnl.find_by(member_id: member.id, pnl_currency_id: 'usd', currency_id: 'btc')
expect(mbtc.total_credit).to eq(0.09)
expect(mbtc.total_credit_fees).to eq(0)
expect(mbtc.total_debit_fees).to eq(0)
expect(mbtc.total_debit).to eq(0)
expect(mbtc.total_credit_value).to eq(90)
expect(mbtc.total_debit_value).to eq(0)
expect(mbtc.total_balance_value).to eq(90)
expect(mbtc.average_balance_price).to eq(1000)
end
def scenario2_internal_sell
key = (Time.now.to_f * 1000).to_i
create(:deposit_usd, member: member, amount: 100).accept!
d = create(:deposit_btc, member: member_platform, amount: 0.09)
d.accept!
d.process!
d.dispatch!
transfers_attr = [
{
key: key,
category: Transfer::CATEGORIES_MAPPING[:purchases],
operations: [
{
currency: :usd,
amount: 100,
account_src: {
code: 201,
uid: member.uid
},
account_dst: {
code: 211,
uid: member.uid
}
}
]
},
{
key: key + 1,
category: Transfer::CATEGORIES_MAPPING[:purchases],
operations: [
{
# Transfer 99 usd from user to the platform
currency: :usd,
amount: 99,
account_src: {
code: 211,
uid: member.uid
},
account_dst: {
code: 201,
uid: member_platform.uid
}
},
{
# Transfer 1 usd from user to the platform fees
currency: :usd,
amount: 1,
account_src: {
code: 211,
uid: member.uid
},
account_dst: {
code: 301,
uid: member.uid
}
},
{
# Transfer 0.09 btc from the platform to the user
currency: :btc,
amount: 0.09,
account_src: {
code: 202,
uid: member_platform.uid
},
account_dst: {
code: 202,
uid: member.uid
}
}
]
}
]
transfers_attr.each do |transfer_attrs|
create_transfer(transfer_attrs)
end
end
it do
scenario2_internal_sell
Jobs::Cron::StatsMemberPnl.stubs(:price_at).with('usd', 'usd', anything).returns(1)
Jobs::Cron::StatsMemberPnl.stubs(:price_at).with('btc', 'usd', anything).returns(10_000)
Jobs::Cron::StatsMemberPnl.process
expect(StatsMemberPnl.count).to eq(4)
musd = StatsMemberPnl.find_by(member_id: member.id, pnl_currency_id: 'usd', currency_id: 'usd')
expect(musd.total_credit).to eq(100)
expect(musd.total_credit_fees).to eq(0)
expect(musd.total_debit_fees).to eq(1)
expect(musd.total_debit).to eq(99)
expect(musd.total_credit_value).to eq(100)
expect(musd.total_debit_value).to eq(99)
expect(musd.total_balance_value).to eq(0)
expect(musd.average_balance_price).to eq(1)
mbtc = StatsMemberPnl.find_by(member_id: member.id, pnl_currency_id: 'usd', currency_id: 'btc')
expect(mbtc.total_credit).to eq(0.09)
expect(mbtc.total_credit_fees).to eq(0)
expect(mbtc.total_debit_fees).to eq(0)
expect(mbtc.total_debit).to eq(0)
expect(mbtc.total_credit_value).to be_within(0.0001).of(100)
expect(mbtc.total_debit_value).to eq(0)
expect(mbtc.total_balance_value).to be_within(0.0001).of(100)
expect(mbtc.average_balance_price).to be_within(0.01).of(1111.11)
end
end
context 'parse_conversion_paths' do
it do
expect(Jobs::Cron::StatsMemberPnl.parse_conversion_paths(nil)).to eq({})
expect(Jobs::Cron::StatsMemberPnl.parse_conversion_paths('')).to eq({})
expect(Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdt/abc:usdt/usd,usd/abc')).to eq(
'usdt/abc' => [['usdt', 'usd', false], ['usd', 'abc', false]],
)
expect(Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdt/abc:usdt/usd,usd/abc;usdt/def:usdt/usd,def/abc,abc/usd')).to eq(
'usdt/abc' => [['usdt', 'usd', false], ['usd', 'abc', false]],
'usdt/def' => [['usdt', 'usd', false], ['def', 'abc', false], ['abc', 'usd', false]]
)
expect(Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdt/abc:_usd/usdt,usd/abc')).to eq(
'usdt/abc' => [['usd', 'usdt', true], ['usd', 'abc', false]],
)
expect { Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdt/abc,abc/usd') }.to raise_error(StandardError)
expect { Jobs::Cron::StatsMemberPnl.parse_conversion_paths(':usdt/abc,abc/usd') }.to raise_error(StandardError)
expect { Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdtabc:usdt/usd,usd/abc') }.to raise_error(StandardError)
expect { Jobs::Cron::StatsMemberPnl.parse_conversion_paths('usdt/abc:usdtusd,usdabc') }.to raise_error(StandardError)
end
end
context 'conversion path' do
before(:each) do
Trade.stubs(:nearest_trade_from_influx).with('btceth', anything).returns(price: 0.95)
Trade.stubs(:nearest_trade_from_influx).with('btcusd', anything).returns(price: 10_000)
end
it do
expect(Jobs::Cron::StatsMemberPnl.price_at('btc', 'eth', 0)).to eq(0.95)
expect(Jobs::Cron::StatsMemberPnl.price_at('btc', 'usd', 0)).to eq(10_000)
end
it 'uses direct markets prices' do
Jobs::Cron::StatsMemberPnl.stubs(:conversion_paths).returns(
'btc/abc' => [['btc', 'eth', false], ['btc', 'usd', false]]
)
expect(Jobs::Cron::StatsMemberPnl.price_at('btc', 'abc', 0)).to eq(9500)
end
it 'reverses a market price' do
Jobs::Cron::StatsMemberPnl.stubs(:conversion_paths).returns(
'btc/abc' => [['btc', 'eth', true], ['btc', 'usd', false]]
)
expect(Jobs::Cron::StatsMemberPnl.price_at('btc', 'abc', 0)).to be_within(0.0001).of(10526.3157)
end
end
end

Some files were not shown because too many files have changed in this diff Show More