Initial commit
This commit is contained in:
93
spec/models/account_spec.rb
Normal file
93
spec/models/account_spec.rb
Normal file
@@ -0,0 +1,93 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Account do
|
||||
subject { create_account(:btc, balance: '10.0'.to_d, locked: '10.0'.to_d) }
|
||||
|
||||
it { expect(subject.amount).to be_d '20' }
|
||||
it { expect(subject.sub_funds('1.0'.to_d).balance).to eql '9.0'.to_d }
|
||||
it { expect(subject.plus_funds('1.0'.to_d).balance).to eql '11.0'.to_d }
|
||||
it { expect(subject.unlock_funds('1.0'.to_d).locked).to eql '9.0'.to_d }
|
||||
it { expect(subject.unlock_funds('1.0'.to_d).balance).to eql '11.0'.to_d }
|
||||
it { expect(subject.lock_funds('1.0'.to_d).locked).to eql '11.0'.to_d }
|
||||
it { expect(subject.lock_funds('1.0'.to_d).balance).to eql '9.0'.to_d }
|
||||
|
||||
it { expect(subject.unlock_and_sub_funds('1.0'.to_d).balance).to be_d '10' }
|
||||
it { expect(subject.unlock_and_sub_funds('1.0'.to_d).locked).to be_d '9' }
|
||||
|
||||
it { expect(subject.sub_funds('0.1'.to_d).balance).to eql '9.9'.to_d }
|
||||
it { expect(subject.plus_funds('0.1'.to_d).balance).to eql '10.1'.to_d }
|
||||
it { expect(subject.unlock_funds('0.1'.to_d).locked).to eql '9.9'.to_d }
|
||||
it { expect(subject.unlock_funds('0.1'.to_d).balance).to eql '10.1'.to_d }
|
||||
it { expect(subject.lock_funds('0.1'.to_d).locked).to eql '10.1'.to_d }
|
||||
it { expect(subject.lock_funds('0.1'.to_d).balance).to eql '9.9'.to_d }
|
||||
|
||||
it { expect(subject.sub_funds('10.0'.to_d).balance).to eql '0.0'.to_d }
|
||||
it { expect(subject.plus_funds('10.0'.to_d).balance).to eql '20.0'.to_d }
|
||||
it { expect(subject.unlock_funds('10.0'.to_d).locked).to eql '0.0'.to_d }
|
||||
it { expect(subject.unlock_funds('10.0'.to_d).balance).to eql '20.0'.to_d }
|
||||
it { expect(subject.lock_funds('10.0'.to_d).locked).to eql '20.0'.to_d }
|
||||
it { expect(subject.lock_funds('10.0'.to_d).balance).to eql '0.0'.to_d }
|
||||
|
||||
it { expect { subject.sub_funds('11.0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
it { expect { subject.lock_funds('11.0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
it { expect { subject.unlock_funds('11.0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
|
||||
it { expect { subject.sub_funds('-1.0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
it { expect { subject.plus_funds('-1.0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
it { expect { subject.lock_funds('-1.0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
it { expect { subject.unlock_funds('-1.0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
it { expect { subject.sub_funds('0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
it { expect { subject.plus_funds('0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
it { expect { subject.lock_funds('0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
it { expect { subject.unlock_funds('0'.to_d) }.to raise_error(Account::AccountError) }
|
||||
|
||||
describe 'double operation' do
|
||||
let(:strike_volume) { '10.0'.to_d }
|
||||
let(:account) { create_account }
|
||||
|
||||
it 'expect double operation funds' do
|
||||
expect do
|
||||
account.plus_funds(strike_volume)
|
||||
account.sub_funds(strike_volume)
|
||||
end.to_not(change { account.balance })
|
||||
end
|
||||
end
|
||||
|
||||
describe 'concurrent lock_funds' do
|
||||
it 'should raise error on the second lock_funds' do
|
||||
account1 = Account.find subject.id
|
||||
account2 = Account.find subject.id
|
||||
|
||||
expect(subject.reload.balance).to eq 10.to_d
|
||||
|
||||
expect do
|
||||
ActiveRecord::Base.transaction do
|
||||
account1.lock_funds(8)
|
||||
end
|
||||
ActiveRecord::Base.transaction do
|
||||
account2.lock_funds(8)
|
||||
end
|
||||
end.to raise_error(Account::AccountError) { |e| expect(e.message).to eq "Cannot lock funds (account id: #{subject.id}, amount: 8, balance: 2.0, locked: 18.0)." }
|
||||
|
||||
expect(subject.reload.balance).to eq 2.to_d
|
||||
end
|
||||
end
|
||||
|
||||
describe '.visible' do
|
||||
before do
|
||||
create_account(:usd)
|
||||
create_account(:btc)
|
||||
create_account(:eth)
|
||||
end
|
||||
|
||||
it 'returns the accounts with currency visible' do
|
||||
currency = Currency.find(:eth)
|
||||
currency.transaction do
|
||||
# We have created 3 account.
|
||||
expect{ currency.update_columns(visible: false) }.to change { Account.visible.count }.by(-1)
|
||||
currency.update_columns(visible: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
256
spec/models/adjustment_spec.rb
Normal file
256
spec/models/adjustment_spec.rb
Normal file
@@ -0,0 +1,256 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Adjustment do
|
||||
let!(:member) { create(:member) }
|
||||
subject { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-202-#{member.uid}") }
|
||||
|
||||
context 'on create' do
|
||||
it 'does not insert liability' do
|
||||
expect {
|
||||
subject
|
||||
}.not_to change { Operations::Liability.count }
|
||||
end
|
||||
|
||||
it 'does not insert asset' do
|
||||
expect {
|
||||
subject
|
||||
}.not_to change { Operations::Asset.count }
|
||||
end
|
||||
|
||||
it 'builds operations' do
|
||||
operations = subject.fetch_operations
|
||||
|
||||
expect(operations).not_to be_empty
|
||||
expect(operations.length).to eq 2
|
||||
expect(operations.map(&:valid?)).to be_truthy
|
||||
expect(operations.map(&:reference_type)).to all eq 'Adjustment'
|
||||
end
|
||||
|
||||
describe 'accounting equation' do
|
||||
context 'single asset operation' do
|
||||
subject { build(:adjustment) }
|
||||
|
||||
before do
|
||||
subject.stubs(:fetch_operations).returns([build(:asset)])
|
||||
end
|
||||
|
||||
it 'invalidates transfer' do
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject).to include_ar_error(:base, /invalidates accounting equation/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'different operationts with invalid accounting sum' do
|
||||
subject do
|
||||
build(:adjustment,
|
||||
asset: asset,
|
||||
liability: liability)
|
||||
end
|
||||
|
||||
let(:asset) { build(:asset, credit: 1, currency_id: :btc) }
|
||||
let(:liability) { build(:liability, :with_member, credit: 5, currency_id: :btc) }
|
||||
|
||||
before do
|
||||
subject.stubs(:fetch_operations).returns([asset, liability])
|
||||
end
|
||||
|
||||
it 'invalidates transfer' do
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject).to include_ar_error(:base, /invalidates accounting equation/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'different operations with valid accounting sum' do
|
||||
subject do
|
||||
build(:adjustment,
|
||||
asset: asset,
|
||||
liability: liability)
|
||||
end
|
||||
|
||||
let(:asset) { build(:asset, credit: 1, currency_id: :btc) }
|
||||
let(:liability) { build(:liability, :with_member, credit: 1, currency_id: :btc) }
|
||||
|
||||
it 'invalidates transfer' do
|
||||
expect(subject.valid?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context '#prebuild_operations' do
|
||||
subject { adjustment.prebuild_operations }
|
||||
|
||||
context 'asset and liability' do
|
||||
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-202-#{member.uid}", amount: 1) }
|
||||
|
||||
it do
|
||||
expect(subject.first.is_a?(Operations::Asset)).to be_truthy
|
||||
expect(subject.second.is_a?(Operations::Liability)).to be_truthy
|
||||
expect(subject.first.credit).to eq(1)
|
||||
expect(subject.second.credit).to eq(1)
|
||||
end
|
||||
|
||||
context 'negative amount' do
|
||||
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-202-#{member.uid}", amount: -1) }
|
||||
|
||||
it do
|
||||
expect(subject.first.debit).to eq(1)
|
||||
expect(subject.second.debit).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'asset and revenue' do
|
||||
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-302", amount: 1) }
|
||||
|
||||
it do
|
||||
expect(subject.first.is_a?(Operations::Asset)).to be_truthy
|
||||
expect(subject.second.is_a?(Operations::Revenue)).to be_truthy
|
||||
expect(subject.first.credit).to eq(1)
|
||||
expect(subject.second.credit).to eq(1)
|
||||
end
|
||||
|
||||
context 'negative amount' do
|
||||
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-302", amount: -1) }
|
||||
|
||||
it do
|
||||
expect(subject.first.debit).to eq(1)
|
||||
expect(subject.second.debit).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'asset and expense' do
|
||||
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-402", amount: 1) }
|
||||
|
||||
it do
|
||||
expect(subject.first.is_a?(Operations::Asset)).to be_truthy
|
||||
expect(subject.second.is_a?(Operations::Expense)).to be_truthy
|
||||
expect(subject.first.credit).to eq(1)
|
||||
expect(subject.second.debit).to eq(1)
|
||||
end
|
||||
|
||||
context 'negative amount' do
|
||||
let!(:adjustment) { create(:adjustment, currency_id: 'btc', receiving_account_number: "btc-402", amount: -1) }
|
||||
|
||||
it do
|
||||
expect(subject.first.debit).to eq(1)
|
||||
expect(subject.second.credit).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'on accept' do
|
||||
it { expect { subject.accept!(validator: member) }.to change { Operations::Asset.count }.by(1) }
|
||||
it { expect { subject.accept!(validator: member) }.to change { Operations::Liability.count }.by(1) }
|
||||
it { expect { subject.accept!(validator: member) }.to change { subject.state }.to('accepted') }
|
||||
|
||||
it 'operaions have correct reference' do
|
||||
subject.accept!(validator: member)
|
||||
operations = subject.fetch_operations
|
||||
expect(operations.map(&:reference_type)).to all eq 'Adjustment'
|
||||
expect(operations.map(&:reference_id)).to all eq subject.id
|
||||
end
|
||||
|
||||
it 'updates legacy balances (credit for main account)' do
|
||||
expect {
|
||||
subject.accept!(validator: member)
|
||||
}.to change { member.get_account(subject.currency).balance }.by(subject.amount)
|
||||
end
|
||||
|
||||
context 'updates legacy balances (debit for locked account)' do
|
||||
subject { create(:adjustment, currency_id: 'btc', amount: -1, receiving_account_number: "btc-212-#{member.uid}") }
|
||||
|
||||
before do
|
||||
member.get_account(:btc).update!(locked: 1)
|
||||
end
|
||||
|
||||
it 'updates legacy balances (credit for main account)' do
|
||||
expect {
|
||||
subject.accept!(validator: member)
|
||||
}.to change { member.accounts.find_by(currency: subject.currency).locked }.by(subject.amount)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not accept with invalid attributes' do
|
||||
subject.update(asset_account_code: 101)
|
||||
|
||||
expect {
|
||||
subject.accept!(validator: member)
|
||||
}.to_not change { subject.state }
|
||||
end
|
||||
|
||||
it 'does not accept without validator' do
|
||||
subject.update(asset_account_code: 101)
|
||||
|
||||
expect {
|
||||
subject.accept!(validator: nil)
|
||||
}.to_not change { subject.state }
|
||||
end
|
||||
|
||||
it 'does not create operations with invalid attributes' do
|
||||
subject.update(asset_account_code: 101)
|
||||
expect {
|
||||
subject.accept!(validator: member)
|
||||
}.not_to change { Operations::Asset.count }
|
||||
|
||||
expect {
|
||||
subject.accept!(validator: member)
|
||||
}.not_to change { Operations::Liability.count }
|
||||
end
|
||||
|
||||
context 'accepted' do
|
||||
before { subject.accept!(validator: member) }
|
||||
|
||||
it { expect { subject.accept!(validator: member) }.not_to change { subject.state } }
|
||||
it { expect { subject.accept!(validator: member) }.not_to change { member.accounts } }
|
||||
it { expect { subject.reject!(validator: member) }.not_to change { subject.state } }
|
||||
end
|
||||
|
||||
context 'accept without validator_id (presence validation)' do
|
||||
before { subject.update(state: 'accepted') }
|
||||
|
||||
it { expect(*subject.errors.full_messages).to eq('Validator can\'t be blank') }
|
||||
end
|
||||
|
||||
context 'accept with validator_id (presence validation)' do
|
||||
before { subject.update(state: 'accepted', validator: member) }
|
||||
|
||||
it { expect(subject.save).to be_truthy }
|
||||
end
|
||||
|
||||
context 'user account creation' do
|
||||
before do
|
||||
subject.accept!(validator: member)
|
||||
end
|
||||
|
||||
it do
|
||||
expect(member.accounts.find_by(currency_id: 'btc').present?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'on reject' do
|
||||
it { expect { subject.reject!(validator: member) }.to change { subject.state }.to('rejected') }
|
||||
|
||||
it 'does not reject without validator' do
|
||||
subject.update(asset_account_code: 101)
|
||||
|
||||
expect {
|
||||
subject.reject!(validator: nil)
|
||||
}.to_not change { subject.state }
|
||||
end
|
||||
|
||||
context 'rejected' do
|
||||
before do
|
||||
subject.reject!(validator: member)
|
||||
end
|
||||
|
||||
it { expect { subject.accept!(validator: member) }.not_to change { subject.state } }
|
||||
it { expect { subject.accept!(validator: member) }.not_to change { member.accounts } }
|
||||
it { expect { subject.reject!(validator: member) }.not_to change { subject.state } }
|
||||
end
|
||||
end
|
||||
end
|
||||
220
spec/models/admin_ability_spec.rb
Normal file
220
spec/models/admin_ability_spec.rb
Normal file
@@ -0,0 +1,220 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe AdminAbility do
|
||||
|
||||
context 'abilities for superadmin' do
|
||||
let(:member) { create(:member, role: 'superadmin') }
|
||||
subject(:ability) { AdminAbility.new(member) }
|
||||
|
||||
it do
|
||||
is_expected.to be_able_to(:manage, Account.new)
|
||||
is_expected.to be_able_to(:manage, Currency.new)
|
||||
is_expected.to be_able_to(:manage, Deposit.new)
|
||||
is_expected.to be_able_to(:manage, Withdraw.new)
|
||||
is_expected.to be_able_to(:manage, Operations::Account.new)
|
||||
is_expected.to be_able_to(:manage, Operations::Asset.new)
|
||||
is_expected.to be_able_to(:manage, Operations::Expense.new)
|
||||
is_expected.to be_able_to(:manage, Operations::Liability.new)
|
||||
is_expected.to be_able_to(:manage, Operations::Revenue.new)
|
||||
is_expected.to be_able_to(:manage, Market.new)
|
||||
is_expected.to be_able_to(:manage, Blockchain.new)
|
||||
is_expected.to be_able_to(:manage, Wallet.new)
|
||||
is_expected.to be_able_to(:manage, PaymentAddress.new)
|
||||
is_expected.to be_able_to(:manage, Member.new)
|
||||
is_expected.to be_able_to(:manage, WhitelistedSmartContract.new)
|
||||
|
||||
is_expected.to be_able_to(:read, Account.new)
|
||||
is_expected.to be_able_to(:read, Currency.new)
|
||||
is_expected.to be_able_to(:read, Deposit.new)
|
||||
is_expected.to be_able_to(:read, Withdraw.new)
|
||||
is_expected.to be_able_to(:read, Operations::Account.new)
|
||||
is_expected.to be_able_to(:read, Operations::Asset.new)
|
||||
is_expected.to be_able_to(:read, Operations::Expense.new)
|
||||
is_expected.to be_able_to(:read, Operations::Liability.new)
|
||||
is_expected.to be_able_to(:read, Operations::Revenue.new)
|
||||
is_expected.to be_able_to(:read, Market.new)
|
||||
is_expected.to be_able_to(:read, Blockchain.new)
|
||||
is_expected.to be_able_to(:read, Wallet.new)
|
||||
is_expected.to be_able_to(:read, PaymentAddress.new)
|
||||
is_expected.to be_able_to(:read, Member.new)
|
||||
|
||||
is_expected.to be_able_to(:update, Account.new)
|
||||
is_expected.to be_able_to(:update, Currency.new)
|
||||
is_expected.to be_able_to(:update, Deposit.new)
|
||||
is_expected.to be_able_to(:update, Withdraw.new)
|
||||
is_expected.to be_able_to(:update, Operations::Account.new)
|
||||
is_expected.to be_able_to(:update, Operations::Asset.new)
|
||||
is_expected.to be_able_to(:update, Operations::Expense.new)
|
||||
is_expected.to be_able_to(:update, Operations::Liability.new)
|
||||
is_expected.to be_able_to(:update, Operations::Revenue.new)
|
||||
is_expected.to be_able_to(:update, Market.new)
|
||||
is_expected.to be_able_to(:update, Blockchain.new)
|
||||
is_expected.to be_able_to(:update, Wallet.new)
|
||||
is_expected.to be_able_to(:update, PaymentAddress.new)
|
||||
is_expected.to be_able_to(:update, Member.new)
|
||||
|
||||
is_expected.to be_able_to(:read, Order.new)
|
||||
is_expected.to be_able_to(:update, Order.new)
|
||||
is_expected.to be_able_to(:read, Trade.new)
|
||||
is_expected.not_to be_able_to(:update, Trade.new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'abilities for admin' do
|
||||
let(:member) { create(:member, role: 'admin') }
|
||||
subject(:ability) { AdminAbility.new(member) }
|
||||
|
||||
it do
|
||||
is_expected.to be_able_to(:manage, Currency.new)
|
||||
is_expected.to be_able_to(:manage, Deposit.new)
|
||||
is_expected.to be_able_to(:manage, Withdraw.new)
|
||||
is_expected.to be_able_to(:manage, Operations::Account.new)
|
||||
is_expected.to be_able_to(:manage, Operations::Asset.new)
|
||||
is_expected.to be_able_to(:manage, Operations::Expense.new)
|
||||
is_expected.to be_able_to(:manage, Operations::Liability.new)
|
||||
is_expected.to be_able_to(:manage, Operations::Revenue.new)
|
||||
is_expected.to be_able_to(:manage, Account.new)
|
||||
is_expected.to be_able_to(:manage, Market.new)
|
||||
is_expected.to be_able_to(:manage, Blockchain.new)
|
||||
is_expected.to be_able_to(:manage, Wallet.new)
|
||||
is_expected.to be_able_to(:manage, PaymentAddress.new)
|
||||
is_expected.to be_able_to(:read, PaymentAddress.new)
|
||||
is_expected.to be_able_to(:read, Member.new)
|
||||
is_expected.to be_able_to(:update, Member.new)
|
||||
is_expected.to be_able_to(:read, Order.new)
|
||||
is_expected.to be_able_to(:update, Order.new)
|
||||
is_expected.to be_able_to(:read, Trade.new)
|
||||
is_expected.not_to be_able_to(:update, Trade.new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'abilities for compliance' do
|
||||
let(:member) { create(:member, role: 'compliance') }
|
||||
subject(:ability) { AdminAbility.new(member) }
|
||||
|
||||
it do
|
||||
is_expected.to be_able_to(:read, Account.new)
|
||||
is_expected.to be_able_to(:read, Deposit.new)
|
||||
is_expected.to be_able_to(:read, Withdraw.new)
|
||||
is_expected.to be_able_to(:read, PaymentAddress.new)
|
||||
is_expected.to be_able_to(:read, Operations::Account.new)
|
||||
is_expected.to be_able_to(:read, Operations::Asset.new)
|
||||
is_expected.to be_able_to(:read, Operations::Expense.new)
|
||||
is_expected.to be_able_to(:read, Operations::Liability.new)
|
||||
is_expected.to be_able_to(:read, Member.new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'abilities for support' do
|
||||
let(:member) { create(:member, role: 'support') }
|
||||
subject(:ability) { AdminAbility.new(member) }
|
||||
|
||||
it do
|
||||
is_expected.to be_able_to(:read, Account.new)
|
||||
is_expected.to be_able_to(:read, Deposit.new)
|
||||
is_expected.to be_able_to(:read, Withdraw.new)
|
||||
is_expected.to be_able_to(:read, Member.new)
|
||||
is_expected.not_to be_able_to(:update, Member.new)
|
||||
is_expected.not_to be_able_to(:update, Account.new)
|
||||
is_expected.not_to be_able_to(:update, Deposit.new)
|
||||
is_expected.not_to be_able_to(:update, Withdraw.new)
|
||||
is_expected.not_to be_able_to(:update, PaymentAddress.new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'abilities for technical' do
|
||||
let(:member) { create(:member, role: 'technical') }
|
||||
subject(:ability) { AdminAbility.new(member) }
|
||||
|
||||
it do
|
||||
is_expected.to be_able_to(:manage, Market.new)
|
||||
is_expected.to be_able_to(:manage, Currency.new)
|
||||
is_expected.to be_able_to(:manage, Blockchain.new)
|
||||
is_expected.to be_able_to(:manage, Wallet.new)
|
||||
is_expected.to be_able_to(:read, Member.new)
|
||||
is_expected.to be_able_to(:manage, Engine.new)
|
||||
is_expected.to be_able_to(:manage, TradingFee.new)
|
||||
is_expected.to be_able_to(:read, Operations::Account.new)
|
||||
is_expected.to be_able_to(:read, Operations::Asset.new)
|
||||
is_expected.to be_able_to(:read, Operations::Expense.new)
|
||||
is_expected.to be_able_to(:read, Operations::Liability.new)
|
||||
is_expected.to be_able_to(:read, Order.new)
|
||||
is_expected.to be_able_to(:read, Trade.new)
|
||||
is_expected.to be_able_to(:read, Member.new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'abilities for accountant' do
|
||||
let(:member) { create(:member, role: 'accountant') }
|
||||
subject(:ability) { AdminAbility.new(member) }
|
||||
|
||||
it do
|
||||
is_expected.to be_able_to(:read, Deposit.new)
|
||||
is_expected.to be_able_to(:read, Withdraw.new)
|
||||
is_expected.to be_able_to(:read, Account.new)
|
||||
is_expected.to be_able_to(:read, PaymentAddress.new)
|
||||
is_expected.to be_able_to(:read, Operations::Account.new)
|
||||
is_expected.to be_able_to(:read, Operations::Asset.new)
|
||||
is_expected.to be_able_to(:read, Operations::Expense.new)
|
||||
is_expected.to be_able_to(:read, Operations::Liability.new)
|
||||
is_expected.to be_able_to(:read, Operations::Revenue.new)
|
||||
is_expected.to be_able_to(:read, Member.new)
|
||||
is_expected.to be_able_to(:read, Deposit.new)
|
||||
is_expected.to be_able_to(:create, Deposits::Fiat.new)
|
||||
is_expected.to be_able_to(:create, Adjustment.new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'abilities for member' do
|
||||
let(:member) { create(:member, role: 'member') }
|
||||
subject(:ability) { AdminAbility.new(member) }
|
||||
|
||||
it do
|
||||
is_expected.not_to be_able_to(:read, Account.new)
|
||||
is_expected.not_to be_able_to(:read, Currency.new)
|
||||
is_expected.not_to be_able_to(:read, Deposit.new)
|
||||
is_expected.not_to be_able_to(:read, Withdraw.new)
|
||||
is_expected.not_to be_able_to(:read, Operations::Account.new)
|
||||
is_expected.not_to be_able_to(:read, Operations::Asset.new)
|
||||
is_expected.not_to be_able_to(:read, Operations::Expense.new)
|
||||
is_expected.not_to be_able_to(:read, Operations::Liability.new)
|
||||
is_expected.not_to be_able_to(:read, Operations::Revenue.new)
|
||||
is_expected.not_to be_able_to(:read, Market.new)
|
||||
is_expected.not_to be_able_to(:read, Blockchain.new)
|
||||
is_expected.not_to be_able_to(:read, Wallet.new)
|
||||
is_expected.not_to be_able_to(:read, PaymentAddress.new)
|
||||
is_expected.not_to be_able_to(:read, Member.new)
|
||||
|
||||
is_expected.not_to be_able_to(:update, Account.new)
|
||||
is_expected.not_to be_able_to(:update, Currency.new)
|
||||
is_expected.not_to be_able_to(:update, Deposit.new)
|
||||
is_expected.not_to be_able_to(:update, Withdraw.new)
|
||||
is_expected.not_to be_able_to(:update, Operations::Account.new)
|
||||
is_expected.not_to be_able_to(:update, Operations::Asset.new)
|
||||
is_expected.not_to be_able_to(:update, Operations::Expense.new)
|
||||
is_expected.not_to be_able_to(:update, Operations::Liability.new)
|
||||
is_expected.not_to be_able_to(:update, Operations::Revenue.new)
|
||||
is_expected.not_to be_able_to(:update, Market.new)
|
||||
is_expected.not_to be_able_to(:update, Blockchain.new)
|
||||
is_expected.not_to be_able_to(:update, Wallet.new)
|
||||
is_expected.not_to be_able_to(:update, PaymentAddress.new)
|
||||
is_expected.not_to be_able_to(:update, Member.new)
|
||||
|
||||
is_expected.not_to be_able_to(:destroy, Account.new)
|
||||
is_expected.not_to be_able_to(:destroy, Currency.new)
|
||||
is_expected.not_to be_able_to(:destroy, Deposit.new)
|
||||
is_expected.not_to be_able_to(:destroy, Withdraw.new)
|
||||
is_expected.not_to be_able_to(:destroy, Operations::Account.new)
|
||||
is_expected.not_to be_able_to(:destroy, Operations::Asset.new)
|
||||
is_expected.not_to be_able_to(:destroy, Operations::Expense.new)
|
||||
is_expected.not_to be_able_to(:destroy, Operations::Liability.new)
|
||||
is_expected.not_to be_able_to(:destroy, Operations::Revenue.new)
|
||||
is_expected.not_to be_able_to(:destroy, Market.new)
|
||||
is_expected.not_to be_able_to(:destroy, Blockchain.new)
|
||||
is_expected.not_to be_able_to(:destroy, Wallet.new)
|
||||
is_expected.not_to be_able_to(:destroy, PaymentAddress.new)
|
||||
is_expected.not_to be_able_to(:destroy, Member.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
65
spec/models/amqp_config_spec.rb
Normal file
65
spec/models/amqp_config_spec.rb
Normal file
@@ -0,0 +1,65 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Workers
|
||||
module AMQP
|
||||
class Test
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe AMQP::Config do
|
||||
let(:config) do
|
||||
Hashie::Mash.new(connect: { host: '127.0.0.1' },
|
||||
exchange: { testx: { name: 'testx', type: 'fanout' },
|
||||
testd: { name: 'testd', type: 'direct' },
|
||||
topicx: { name: 'topicx', type: 'topic' } },
|
||||
queue: { testq: { name: 'testq', durable: true } },
|
||||
binding: {
|
||||
test: { queue: 'testq', exchange: 'testx' },
|
||||
testd: { queue: 'testq', exchange: 'testd' },
|
||||
topic: { queue: 'testq', exchange: 'topicx', topics: 'test.a,test.b' },
|
||||
default: { queue: 'testq' }
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
AMQP::Config.stubs(:data).returns(config)
|
||||
end
|
||||
|
||||
it 'should tell client how to connect' do
|
||||
expect(AMQP::Config.connect).to eq ({ 'host' => '127.0.0.1' })
|
||||
end
|
||||
|
||||
it 'should return queue settings' do
|
||||
expect(AMQP::Config.queue(:testq)).to eq ['testq', { durable: true }]
|
||||
end
|
||||
|
||||
it 'should return exchange settings' do
|
||||
expect(AMQP::Config.exchange(:testx)).to eq %w[fanout testx]
|
||||
end
|
||||
|
||||
it 'should return binding queue' do
|
||||
expect(AMQP::Config.binding_queue(:test)).to eq ['testq', { durable: true }]
|
||||
end
|
||||
|
||||
it 'should return binding exchange' do
|
||||
expect(AMQP::Config.binding_exchange(:test)).to eq %w[fanout testx]
|
||||
end
|
||||
|
||||
it 'should set exchange to nil when binding use default exchange' do
|
||||
expect(AMQP::Config.binding_exchange(:default)).to be_nil
|
||||
end
|
||||
|
||||
it 'should find binding worker' do
|
||||
expect(AMQP::Config.binding_worker(:test)).to be_instance_of(Workers::AMQP::Test)
|
||||
end
|
||||
|
||||
it 'should return queue name of binding' do
|
||||
expect(AMQP::Config.routing_key(:testd)).to eq 'testq'
|
||||
end
|
||||
|
||||
it 'should return topics to subscribe' do
|
||||
expect(AMQP::Config.topics(:topic)).to eq ['test.a', 'test.b']
|
||||
end
|
||||
end
|
||||
44
spec/models/amqp_queue_spec.rb
Normal file
44
spec/models/amqp_queue_spec.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe AMQP::Queue do
|
||||
let(:config) do
|
||||
Hashie::Mash.new(connect: { host: '127.0.0.1' },
|
||||
exchange: { testx: { name: 'testx', type: 'fanout' } },
|
||||
queue: { testq: { name: 'testq', durable: true },
|
||||
testd: { name: 'testd' } },
|
||||
binding: {
|
||||
test: { queue: 'testq', exchange: 'testx' },
|
||||
testd: { queue: 'testd' },
|
||||
default: { queue: 'testq' }
|
||||
})
|
||||
end
|
||||
|
||||
let(:default_exchange) { stub('default_exchange') }
|
||||
let(:channel) { stub('channel', default_exchange: default_exchange) }
|
||||
|
||||
before do
|
||||
AMQP::Config.stubs(:data).returns(config)
|
||||
|
||||
AMQP::Queue.unstub(:publish)
|
||||
AMQP::Queue.stubs(:exchanges).returns(default: default_exchange)
|
||||
AMQP::Queue.stubs(:channel).returns(channel)
|
||||
end
|
||||
|
||||
it 'should instantiate exchange use exchange config' do
|
||||
channel.expects(:fanout).with('testx')
|
||||
AMQP::Queue.exchange(:testx)
|
||||
end
|
||||
|
||||
it 'should publish message on selected exchange' do
|
||||
exchange = mock('test exchange')
|
||||
channel.expects(:fanout).with('testx').returns(exchange)
|
||||
exchange.expects(:publish).with(JSON.dump(data: 'hello'), {})
|
||||
AMQP::Queue.publish(:testx, data: 'hello')
|
||||
end
|
||||
|
||||
it 'should publish message on default exchange' do
|
||||
default_exchange.expects(:publish).with(JSON.dump(data: 'hello'), routing_key: 'testd')
|
||||
AMQP::Queue.enqueue(:testd, data: 'hello')
|
||||
end
|
||||
end
|
||||
209
spec/models/beneficiary_spec.rb
Normal file
209
spec/models/beneficiary_spec.rb
Normal file
@@ -0,0 +1,209 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
# TODO: AASM tests.
|
||||
# TODO: Event API tests.
|
||||
|
||||
describe Beneficiary, 'Relationships' do
|
||||
context 'beneficiary build by factory' do
|
||||
subject { build(:beneficiary) }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
|
||||
context 'belongs to member' do
|
||||
context 'null member_id' do
|
||||
subject { build(:beneficiary, member: nil) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'belongs to currency' do
|
||||
context 'null currency_id' do
|
||||
subject { build(:beneficiary, currency: nil) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Beneficiary, 'Validations' do
|
||||
context 'pin presence' do
|
||||
context 'nil pin' do
|
||||
subject { build(:beneficiary) }
|
||||
before { Beneficiary.expects(:generate_pin).returns(nil) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'pin numericality only_integer' do
|
||||
context 'float pin' do
|
||||
subject { build(:beneficiary) }
|
||||
before { Beneficiary.expects(:generate_pin).returns(3.14) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'state inclusion' do
|
||||
context 'wrong state' do
|
||||
subject { build(:beneficiary, state: :wrong) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'data presence' do
|
||||
context 'nil data' do
|
||||
subject { build(:beneficiary, data: nil) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
|
||||
context 'empty hash data' do
|
||||
subject { build(:beneficiary, data: {}) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'data address presence' do
|
||||
context 'fiat' do
|
||||
context 'blank address' do
|
||||
let(:fiat) { Currency.find(:usd)}
|
||||
subject { build(:beneficiary, currency: fiat).tap { |b| b.data.delete('address') } }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
context 'coin' do
|
||||
context 'blank address' do
|
||||
let(:coin) { Currency.find(:btc)}
|
||||
subject { build(:beneficiary, currency_id: coin).tap { |b| b.data.delete('address') } }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'data full_name presence' do
|
||||
# TODO: Write me.
|
||||
end
|
||||
end
|
||||
|
||||
describe Beneficiary, 'Callback' do
|
||||
context 'before_validation on create' do
|
||||
subject { build(:beneficiary) }
|
||||
it 'generates pin' do
|
||||
expect(subject.pin).to be_nil
|
||||
subject.validate!
|
||||
expect(subject.pin).to_not be_nil
|
||||
pin = subject.pin
|
||||
subject.validate!
|
||||
expect(subject.pin).to eq(pin)
|
||||
end
|
||||
end
|
||||
|
||||
context 'before_create' do
|
||||
subject { build(:beneficiary) }
|
||||
it 'generates sent_at' do
|
||||
expect(subject.sent_at).to be_nil
|
||||
subject.save!
|
||||
expect(subject.sent_at).to_not be_nil
|
||||
expect(subject.sent_at.round).to eq(subject.created_at.round)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Beneficiary, 'Instance Methods' do
|
||||
context 'rid' do
|
||||
context 'fiat' do
|
||||
let(:full_name) { Faker::Name.name_with_middle }
|
||||
let(:fiat) { Currency.find(:usd)}
|
||||
|
||||
subject do
|
||||
create(:beneficiary,
|
||||
currency: fiat,
|
||||
data: generate(:fiat_beneficiary_data).merge(full_name: full_name))
|
||||
end
|
||||
|
||||
it do
|
||||
expect(subject.rid).to include(*full_name.downcase.split)
|
||||
expect(subject.rid).to include(subject.id.to_s)
|
||||
expect(subject.rid).to include(subject.currency_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'coin' do
|
||||
let(:address) { Faker::Blockchain::Ethereum.address }
|
||||
let(:coin) { Currency.find(:btc) }
|
||||
|
||||
subject do
|
||||
create(:beneficiary,
|
||||
currency: coin,
|
||||
data: generate(:coin_beneficiary_data).merge(address: address))
|
||||
end
|
||||
|
||||
it do
|
||||
expect(subject.rid).to include(address)
|
||||
end
|
||||
end
|
||||
|
||||
context 'masked fields' do
|
||||
context 'account number' do
|
||||
context 'fiat beneficiary' do
|
||||
let!(:fiat_beneficiary) { create(:beneficiary, currency: Currency.find('usd'),
|
||||
data: {
|
||||
full_name: Faker::Name.name_with_middle,
|
||||
address: Faker::Address.full_address,
|
||||
country: Faker::Address.country,
|
||||
account_number: '0399261557'
|
||||
})}
|
||||
|
||||
it { expect(fiat_beneficiary.masked_account_number).to eq '03****1557'}
|
||||
end
|
||||
|
||||
context 'coin beneficiary' do
|
||||
let!(:coin_beneficiary) { create(:beneficiary, currency: Currency.find('btc'))}
|
||||
it { expect(coin_beneficiary.masked_account_number).to eq nil }
|
||||
end
|
||||
end
|
||||
|
||||
context 'masked data' do
|
||||
context 'fiat beneficiary' do
|
||||
let!(:fiat_beneficiary) { create(:beneficiary, currency: Currency.find('usd'),
|
||||
data: {
|
||||
full_name: 'Full name',
|
||||
address: 'Address',
|
||||
country: 'Country',
|
||||
account_number: '0399261557'
|
||||
})}
|
||||
|
||||
it 'should mask account number' do
|
||||
expect(fiat_beneficiary.masked_data).to match ({
|
||||
full_name: 'Full name',
|
||||
address: 'Address',
|
||||
country: 'Country',
|
||||
account_number: '03****1557'
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'coin beneficiary' do
|
||||
let!(:coin_beneficiary) { create(:beneficiary, currency: Currency.find('btc'))}
|
||||
|
||||
it 'data shouldnt change' do
|
||||
expect(coin_beneficiary.masked_data).to match (coin_beneficiary.data)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'regenerate pin' do
|
||||
subject { create(:beneficiary) }
|
||||
|
||||
it do
|
||||
sent_at = subject.sent_at
|
||||
pin = subject.pin
|
||||
|
||||
Time.stubs(:now).returns(Time.mktime(1970,1,1))
|
||||
subject.regenerate_pin!
|
||||
expect(subject.pin).to_not eq(pin)
|
||||
expect(subject.sent_at).to_not eq(sent_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
61
spec/models/blockchain_spec.rb
Normal file
61
spec/models/blockchain_spec.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Blockchain do
|
||||
context 'validations' do
|
||||
|
||||
subject { build(:blockchain, 'eth-mainet') }
|
||||
|
||||
it 'checks valid record' do
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'validates presence of key' do
|
||||
subject.key = nil
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ["Key can't be blank"]
|
||||
end
|
||||
|
||||
it 'validates client' do
|
||||
subject.client = 'zephyreum'
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ["Client is not included in the list"]
|
||||
end
|
||||
|
||||
it 'validates presence of name' do
|
||||
subject.name = nil
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ["Name can't be blank"]
|
||||
end
|
||||
|
||||
it 'validates presence of client' do
|
||||
subject.client = nil
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to include "Client can't be blank"
|
||||
end
|
||||
|
||||
it 'validates inclusion of status' do
|
||||
subject.status = 'abc'
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ["Status is not included in the list"]
|
||||
end
|
||||
|
||||
it 'validates height should be greater than or equal to 1' do
|
||||
subject.height = 0
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ["Height must be greater than or equal to 1"]
|
||||
end
|
||||
|
||||
it 'validates min_confirmations should be greater than or equal to 1' do
|
||||
subject.min_confirmations = 0
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ["Min confirmations must be greater than or equal to 1"]
|
||||
end
|
||||
|
||||
it 'validates structure of server' do
|
||||
subject.server = 'Wrong URL'
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ["Server is not a valid URL"]
|
||||
end
|
||||
end
|
||||
end
|
||||
29
spec/models/concerns/precision_validator_spec.rb
Normal file
29
spec/models/concerns/precision_validator_spec.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Validatable
|
||||
include ActiveModel::Validations
|
||||
attr_accessor :amount
|
||||
validates :amount, precision: { less_than_or_eq_to: 2 }
|
||||
end
|
||||
|
||||
describe PrecisionValidator do
|
||||
subject { Validatable.new }
|
||||
|
||||
it 'returns valid record' do
|
||||
subject.stubs(amount: 1)
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'returns invalid record with errors' do
|
||||
subject.stubs(amount: 0.001)
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:amount]).to include(/precision must be less than or equal to 2/)
|
||||
end
|
||||
|
||||
it 'returns invalid record with errors' do
|
||||
subject.stubs(amount: '0.001')
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:amount]).to include(/must be a number/)
|
||||
end
|
||||
end
|
||||
297
spec/models/currency_spec.rb
Normal file
297
spec/models/currency_spec.rb
Normal file
@@ -0,0 +1,297 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Currency do
|
||||
context 'fiat' do
|
||||
let(:currency) { Currency.find(:usd) }
|
||||
it 'allows to change deposit fee' do
|
||||
currency.update!(deposit_fee: 0.25)
|
||||
expect(currency.deposit_fee).to eq 0.25
|
||||
end
|
||||
end
|
||||
|
||||
context 'coin' do
|
||||
let(:currency) { Currency.find(:btc) }
|
||||
it 'doesn\'t allow to change deposit fee' do
|
||||
currency.update!(deposit_fee: 0.25)
|
||||
expect(currency.deposit_fee).to eq 0
|
||||
end
|
||||
|
||||
it 'validates blockchain_key' do
|
||||
currency.blockchain_key = 'an-nonexistent-key'
|
||||
expect(currency.valid?).to be_falsey
|
||||
expect(currency.errors[:blockchain_key].size).to eq(1)
|
||||
|
||||
currency.blockchain_key = 'btc-testnet' # an existent key
|
||||
expect(currency.valid?).to be_truthy
|
||||
expect(currency.errors[:blockchain_key]).to be_empty
|
||||
end
|
||||
|
||||
it 'validates position' do
|
||||
currency.position = 0
|
||||
expect(currency.valid?).to be_falsey
|
||||
expect(currency.errors[:position].size).to eq(1)
|
||||
end
|
||||
|
||||
it 'validate position value on update' do
|
||||
currency.update(position: nil)
|
||||
expect(currency.valid?).to eq false
|
||||
expect(currency.errors[:position].size).to eq(2)
|
||||
|
||||
currency.update(position: 0)
|
||||
expect(currency.valid?).to eq false
|
||||
expect(currency.errors[:position].size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'token' do
|
||||
let!(:currency) { Currency.find(:ring) }
|
||||
let!(:trst_currency) { Currency.find(:trst) }
|
||||
let!(:fiat_currency) { Currency.find(:eur) }
|
||||
|
||||
# coin configuration
|
||||
it 'validate parent_id presence' do
|
||||
currency.parent_id = nil
|
||||
expect(currency.valid?).to eq true
|
||||
end
|
||||
|
||||
# token configuration
|
||||
it 'validate parent_id value' do
|
||||
currency.parent_id = fiat_currency.id
|
||||
expect(currency.valid?).to be_falsey
|
||||
expect(currency.errors[:parent_id]).to eq ["is not included in the list"]
|
||||
|
||||
currency.parent_id = trst_currency.id
|
||||
expect(currency.valid?).to be_falsey
|
||||
expect(currency.errors[:parent_id]).to eq ["is not included in the list"]
|
||||
end
|
||||
end
|
||||
|
||||
context 'scopes' do
|
||||
let(:currency) { Currency.find(:btc) }
|
||||
|
||||
context 'visible' do
|
||||
it 'changes visible scope count' do
|
||||
visible = Currency.visible.count
|
||||
currency.update(visible: false)
|
||||
expect(Currency.visible.count).to eq(visible - 1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'deposit_enabled' do
|
||||
it 'changes deposit_enabled scope count' do
|
||||
deposit_enabled = Currency.deposit_enabled.count
|
||||
currency.update(deposit_enabled: false)
|
||||
expect(Currency.deposit_enabled.count).to eq(deposit_enabled - 1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'withdrawal_enabled' do
|
||||
it 'changes withdrawal_enabled scope count' do
|
||||
withdrawal_enabled = Currency.withdrawal_enabled.count
|
||||
currency.update(withdrawal_enabled: false)
|
||||
expect(Currency.withdrawal_enabled.count).to eq(withdrawal_enabled - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'subunits=' do
|
||||
let!(:currency) { Currency.find(:btc) }
|
||||
|
||||
it 'updates base_factor' do
|
||||
expect { currency.subunits = 4 }.to change { currency.base_factor }.to 10_000
|
||||
end
|
||||
end
|
||||
|
||||
context 'read only attributes' do
|
||||
let!(:fake_currency) { create(:currency, :btc, id: 'fake') }
|
||||
|
||||
it 'should not update the base factor' do
|
||||
fake_currency.update_attributes :base_factor => 8
|
||||
expect(fake_currency.reload.base_factor).to eq(fake_currency.base_factor)
|
||||
end
|
||||
|
||||
it 'should not update the type' do
|
||||
fake_currency.update_attributes :type => 'fiat'
|
||||
expect(fake_currency.reload.type).to eq(fake_currency.type)
|
||||
end
|
||||
end
|
||||
|
||||
context 'subunits' do
|
||||
let!(:fake_currency) { create(:currency, :btc, id: 'fake', base_factor: 100) }
|
||||
|
||||
it 'return currency subunits' do
|
||||
expect(fake_currency.subunits).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'serialization' do
|
||||
let!(:currency) { Currency.find(:ring) }
|
||||
|
||||
let(:options) { { "gas_price" => "standard", "erc20_contract_address" => "0x022e292b44b5a146f2e8ee36ff44d3dd863c915c", "gas_limit" => "100000" } }
|
||||
|
||||
it 'should serialize/deserialize options' do
|
||||
currency.update(options: options)
|
||||
expect(Currency.find(:ring).options).to eq options
|
||||
end
|
||||
end
|
||||
|
||||
context 'validate max currency' do
|
||||
before { ENV['MAX_CURRENCIES'] = '6' }
|
||||
after { ENV['MAX_CURRENCIES'] = nil }
|
||||
|
||||
it 'should raise validation error for max currency' do
|
||||
record = build(:currency, :fake, id: 'fake2', type: 'fiat', base_factor: 100)
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/Max Currency limit has been reached/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'Methods' do
|
||||
context 'token?' do
|
||||
let!(:coin) { Currency.find(:btc) }
|
||||
let!(:token) { Currency.find(:trst) }
|
||||
|
||||
it { expect(coin.token?).to eq false }
|
||||
it { expect(token.token?).to eq true }
|
||||
end
|
||||
end
|
||||
|
||||
context 'Callbacks' do
|
||||
context 'blockchain key' do
|
||||
let!(:coin) { Currency.find(:btc) }
|
||||
let!(:token) { Currency.find(:trst) }
|
||||
|
||||
it 'should update blockchain key' do
|
||||
token.update_attributes :blockchain_key => coin.blockchain_key
|
||||
expect(token.reload.blockchain_key).to eq(coin.blockchain_key)
|
||||
end
|
||||
|
||||
it 'should create currency with default blockchain key' do
|
||||
currency = Currency.new(code: 'test', parent_id: coin.id)
|
||||
|
||||
expect(currency.blockchain_key).to eq nil
|
||||
expect(currency.valid?).to eq true
|
||||
expect(currency.blockchain_key).to eq coin.blockchain_key
|
||||
end
|
||||
|
||||
it 'should create currency with non default blockchain key' do
|
||||
currency = Currency.new(code: 'test', parent_id: coin.id, blockchain_key: token.blockchain_key)
|
||||
|
||||
expect(currency.blockchain_key).to eq token.blockchain_key
|
||||
expect(currency.valid?).to eq true
|
||||
expect(currency.blockchain_key).to eq token.blockchain_key
|
||||
end
|
||||
end
|
||||
|
||||
context 'after_create' do
|
||||
let!(:coin) { Currency.find(:btc) }
|
||||
|
||||
it 'move to the bottom if there is no position' do
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
Currency.create(code: 'test', parent_id: coin.id)
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3], ['eth', 4],
|
||||
['trst', 5], ['ring', 6], ['test', 7]]
|
||||
end
|
||||
|
||||
it 'move to the bottom of all currencies' do
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
Currency.create(code: 'test', parent_id: coin.id, position: 7)
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3], ['eth', 4],
|
||||
['trst', 5], ['ring', 6], ['test', 7]]
|
||||
end
|
||||
|
||||
it 'move to the bottom when position is greater that currencies count' do
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
Currency.create(code: 'test', parent_id: coin.id, position: Currency.all.count + 2)
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3], ['eth', 4],
|
||||
['trst', 5], ['ring', 6], ['test', 7]]
|
||||
end
|
||||
|
||||
it 'move to the top of all currencies' do
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
Currency.create(code: 'test', parent_id: coin.id, position: 1)
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['test', 1], ['usd', 2], ['eur', 3], ['btc', 4],
|
||||
['eth', 5], ['trst', 6], ['ring', 7]]
|
||||
end
|
||||
|
||||
it 'move to the middle of all currencies' do
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
Currency.create(code: 'test', parent_id: coin.id, position: 5)
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['test', 5], ['trst', 6], ['ring', 7]]
|
||||
end
|
||||
|
||||
it 'position equal to currencies amount' do
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
Currency.create(code: 'test', parent_id: coin.id, position: 6)
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3], ['eth', 4],
|
||||
['trst', 5], ['test', 6], ['ring', 7]]
|
||||
end
|
||||
|
||||
context 'link_wallets' do
|
||||
let!(:coin) { Currency.find(:eth) }
|
||||
let!(:wallet) { Wallet.deposit_wallet(:eth) }
|
||||
|
||||
context 'without parent id' do
|
||||
it 'should not create currency wallet' do
|
||||
currency = Currency.create(code: 'test')
|
||||
expect(CurrencyWallet.find_by(currency_id: currency.id, wallet_id: wallet.id)).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with parent id' do
|
||||
it 'should create currency wallet' do
|
||||
currency = Currency.create(code: 'test', parent_id: coin.id)
|
||||
c_w = CurrencyWallet.find_by(currency_id: currency.id, wallet_id: wallet.id)
|
||||
|
||||
expect(c_w.present?).to eq true
|
||||
expect(c_w.currency_id).to eq currency.id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'before update' do
|
||||
let!(:coin) { Currency.find(:btc) }
|
||||
|
||||
it 'move to the bottom of all currencies' do
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
coin.update(position: 6)
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['eth', 3],
|
||||
['trst', 4], ['ring', 5], ['btc', 6]]
|
||||
end
|
||||
|
||||
it 'move to the bottom when position is greater that currencies count' do
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
coin.update(position: Currency.all.count + 2)
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['eth', 3],
|
||||
['trst', 4], ['ring', 5], ['btc', 6]]
|
||||
end
|
||||
|
||||
it 'move to the top of all currencies' do
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
coin.update(position: 1)
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['btc', 1], ['usd', 2], ['eur', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
end
|
||||
|
||||
it 'move to the middle of all currencies' do
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['btc', 3],
|
||||
['eth', 4], ['trst', 5], ['ring', 6]]
|
||||
coin.update(position: 4)
|
||||
expect(Currency.all.ordered.pluck(:id, :position)).to eq [['usd', 1], ['eur', 2], ['eth', 3],
|
||||
['btc', 4], ['trst', 5], ['ring', 6]]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
329
spec/models/deposit_spec.rb
Normal file
329
spec/models/deposit_spec.rb
Normal file
@@ -0,0 +1,329 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Deposit do
|
||||
let(:member) { create(:member) }
|
||||
let(:amount) { 100.to_d }
|
||||
let(:deposit) { create(:deposit_usd, member: member, amount: amount, currency: currency) }
|
||||
let(:currency) { Currency.find(:usd) }
|
||||
|
||||
it 'computes fee' do
|
||||
expect(deposit.fee).to eql 0.to_d
|
||||
expect(deposit.amount).to eql 100.to_d
|
||||
end
|
||||
|
||||
context 'fee is set to fixed value of 10' do
|
||||
before { Currency.any_instance.expects(:deposit_fee).once.returns(10) }
|
||||
|
||||
it 'computes fee' do
|
||||
expect(deposit.fee).to eql 10.to_d
|
||||
expect(deposit.amount).to eql 90.to_d
|
||||
end
|
||||
end
|
||||
|
||||
context 'fee exceeds amount' do
|
||||
before { Currency.any_instance.expects(:deposit_fee).once.returns(1.1) }
|
||||
let(:amount) { 1 }
|
||||
let(:deposit) { build(:deposit_usd, member: member, amount: amount, currency: currency) }
|
||||
it 'fails validation' do
|
||||
expect(deposit.save).to eq false
|
||||
expect(deposit.errors.full_messages).to eq ['Amount must be greater than 0.0', 'Amount must be greater than or equal to 0.0']
|
||||
end
|
||||
end
|
||||
|
||||
it 'automatically generates TID if it is blank' do
|
||||
expect(create(:deposit_btc).tid).not_to be_blank
|
||||
end
|
||||
|
||||
it 'doesn\'t generate TID if it is not blank' do
|
||||
expect(create(:deposit_btc, tid: 'TID1234567890xyz').tid).to eq 'TID1234567890xyz'
|
||||
end
|
||||
|
||||
it 'validates uniqueness of TID' do
|
||||
record1 = create(:deposit_btc)
|
||||
record2 = build(:deposit_btc, tid: record1.tid)
|
||||
record2.save
|
||||
expect(record2.errors.full_messages.first).to match(/tid has already been taken/i)
|
||||
end
|
||||
|
||||
it 'uppercases automatically generated TID' do
|
||||
record = create(:deposit_btc)
|
||||
expect(record.tid).to eq record.tid.upcase
|
||||
end
|
||||
|
||||
context 'calculates confirmations' do
|
||||
let(:deposit) { create(:deposit_btc) }
|
||||
|
||||
it 'uses height from blockchain by default' do
|
||||
deposit.blockchain.stubs(:processed_height).returns(100)
|
||||
deposit.stubs(:block_number).returns(90)
|
||||
expect(deposit.confirmations).to eql(10)
|
||||
end
|
||||
end
|
||||
|
||||
context :spread_between_wallets! do
|
||||
let(:spread) do
|
||||
[Peatio::Transaction.new(to_address: 'to-address-1', amount: 1.2),
|
||||
Peatio::Transaction.new(to_address: 'to-address-2', amount: 2.5)]
|
||||
end
|
||||
let(:deposit) { create(:deposit_btc, amount: 3.7) }
|
||||
|
||||
before do
|
||||
WalletService.any_instance.expects(:spread_deposit).returns(spread)
|
||||
end
|
||||
|
||||
it 'spreads deposit between wallets' do
|
||||
expect(deposit.spread).to eq([])
|
||||
expect(deposit.spread_between_wallets!).to be_truthy
|
||||
expect(deposit.reload.spread).to eq(spread.map(&:as_json).map(&:symbolize_keys))
|
||||
|
||||
expect(deposit.spread_between_wallets!).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context :accept do
|
||||
context :record_complete_operations! do
|
||||
subject { deposit }
|
||||
it 'creates single asset operation' do
|
||||
expect { subject.accept! }.to change { Operations::Asset.count }.by(1)
|
||||
end
|
||||
|
||||
it 'credits assets with correct amount' do
|
||||
subject.accept!
|
||||
asset_operation = Operations::Asset.find_by(reference: subject)
|
||||
expect(asset_operation.credit).to eq(subject.amount)
|
||||
end
|
||||
|
||||
it 'creates single liability operation' do
|
||||
expect { subject.accept! }.to change { Operations::Liability.count }.by(1)
|
||||
end
|
||||
|
||||
it 'credits liabilities with correct amount' do
|
||||
subject.accept!
|
||||
liability_operation = Operations::Liability.find_by(reference: subject)
|
||||
expect(liability_operation.credit).to eq(subject.amount)
|
||||
end
|
||||
|
||||
context 'zero deposit fee' do
|
||||
it 'doesn\'t create revenue operation' do
|
||||
expect { subject.accept! }.to_not change { Operations::Revenue.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'greater than zero deposit fee' do
|
||||
let(:currency) do
|
||||
Currency.find(:usd).tap { |c| c.update(deposit_fee: 0.01) }
|
||||
end
|
||||
|
||||
let(:deposit) do
|
||||
create(:deposit_usd, member: member, amount: amount, currency: currency)
|
||||
end
|
||||
|
||||
it 'creates single revenue operation' do
|
||||
expect { subject.accept! }.to change { Operations::Revenue.count }.by(1)
|
||||
end
|
||||
|
||||
it 'credits revenues with fee amount' do
|
||||
subject.accept!
|
||||
revenue_operation = Operations::Revenue.find_by(reference: subject)
|
||||
expect(revenue_operation.credit).to eq(subject.fee)
|
||||
end
|
||||
|
||||
it 'creates revenue from member' do
|
||||
expect { subject.accept! }.to change { Operations::Revenue.where(member: member).count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'credits both legacy and operations based member balance for fiat deposit' do
|
||||
subject.accept!
|
||||
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'coin deposit' do
|
||||
context 'without locked funds' do
|
||||
context 'credits both legacy and operations based member balance for coin deposit' do
|
||||
subject { create(:deposit_btc, amount: 3.7) }
|
||||
it do
|
||||
subject.accept!
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
end
|
||||
|
||||
expect(subject.member.balance_for(currency: subject.currency, kind: :locked)).to eq 0
|
||||
expect(subject.member.balance_for(currency: subject.currency, kind: :main)).to eq 3.7
|
||||
|
||||
# Transfer funds directly to main account on deposit accepted step (1 liability)
|
||||
liabilities = Operations::Liability.where(currency_id: subject.currency.id, member_id: subject.member.id, reference_type: 'Deposit')
|
||||
expect(liabilities.count).to eq 1
|
||||
|
||||
account = Operations::Account.find_by(kind: :main, currency_type: subject.currency.type, type: 'liability')
|
||||
expect(liabilities.first.code).to eq account.code
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with locked funds' do
|
||||
before { Peatio::App.config.stubs(:deposit_funds_locked).returns(true) }
|
||||
context 'credits both legacy and operations based member balance for coin deposit' do
|
||||
subject { create(:deposit_btc, amount: 3.7) }
|
||||
|
||||
it do
|
||||
subject.accept!
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
end
|
||||
|
||||
expect(subject.member.balance_for(currency: subject.currency, kind: :locked)).to eq 3.7
|
||||
expect(subject.member.balance_for(currency: subject.currency, kind: :main)).to eq 0
|
||||
|
||||
# Lock funds on deposit accepted step (1 liability)
|
||||
liabilities = Operations::Liability.where(currency_id: subject.currency.id, member_id: subject.member.id, reference_type: 'Deposit')
|
||||
expect(liabilities.count).to eq 1
|
||||
|
||||
account = Operations::Account.find_by(kind: :locked, currency_type: subject.currency.type, type: 'liability', scope: 'member')
|
||||
expect(liabilities.first.code).to eq account.code
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context :process do
|
||||
let(:crypto_deposit) { create(:deposit_btc, amount: 3.7) }
|
||||
|
||||
it 'doesnt process fiat deposit' do
|
||||
deposit.accept!
|
||||
expect(deposit.process!).to eq false
|
||||
end
|
||||
|
||||
it 'process coin deposit' do
|
||||
crypto_deposit = create(:deposit_btc, amount: 3.7)
|
||||
crypto_deposit.accept!
|
||||
expect(crypto_deposit.process!).to eq true
|
||||
expect(crypto_deposit.processing?).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context :processing do
|
||||
let(:crypto_deposit) { create(:deposit_btc, amount: 3.7) }
|
||||
|
||||
before do
|
||||
Peatio::App.config.stubs(:deposit_funds_locked).returns(true)
|
||||
crypto_deposit.accept!
|
||||
crypto_deposit.process!
|
||||
end
|
||||
|
||||
subject { crypto_deposit }
|
||||
|
||||
it 'commit deposit to collected' do
|
||||
# Minus locked and plus main accounts
|
||||
expect { subject.dispatch! }.to change { Operations::Liability.count }.by(2)
|
||||
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
end
|
||||
expect(crypto_deposit.aasm_state).to eq('collected')
|
||||
end
|
||||
|
||||
it 'dispatch deposit to skipped' do
|
||||
# Minus locked and plus main accounts
|
||||
expect { subject.skip! }.to change { Operations::Liability.count }.by(0)
|
||||
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
end
|
||||
expect(crypto_deposit.aasm_state).to eq('skipped')
|
||||
end
|
||||
end
|
||||
context :dispatch do
|
||||
let(:crypto_deposit) { create(:deposit_btc, amount: 3.7) }
|
||||
|
||||
subject { crypto_deposit }
|
||||
|
||||
context 'with locked funds' do
|
||||
before do
|
||||
Peatio::App.config.stubs(:deposit_funds_locked).returns(true)
|
||||
crypto_deposit.accept!
|
||||
crypto_deposit.process!
|
||||
end
|
||||
|
||||
it 'dispatches deposit' do
|
||||
subject.dispatch!
|
||||
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
|
||||
liabilities = Operations::Liability.where(currency_id: subject.currency.id, member_id: subject.member.id)
|
||||
expect(liabilities.count).to eq 3
|
||||
|
||||
# Lock funds on deposit accepted step (1 liability)
|
||||
account = Operations::Account.find_by(kind: :locked, currency_type: subject.currency.type, type: 'liability', scope: 'member')
|
||||
expect(liabilities.first.code).to eq account.code
|
||||
|
||||
# Moved funds from locked to main (2 liabilities)
|
||||
# debit
|
||||
account = Operations::Account.find_by(kind: :locked, currency_type: subject.currency.type, type: 'liability', scope: 'member')
|
||||
expect(liabilities.second.code).to eq account.code
|
||||
|
||||
# credit
|
||||
account = Operations::Account.find_by(kind: :main, currency_type: subject.currency.type, type: 'liability', scope: 'member')
|
||||
expect(liabilities.last.code).to eq account.code
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without locked funds' do
|
||||
before do
|
||||
crypto_deposit.accept!
|
||||
crypto_deposit.process!
|
||||
end
|
||||
|
||||
it 'dispatches deposit' do
|
||||
subject.dispatch!
|
||||
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
|
||||
# There is only one liability with transferring funds directly to main account
|
||||
liabilities = Operations::Liability.where(currency_id: subject.currency.id, member_id: subject.member.id, reference_type: 'Deposit')
|
||||
expect(liabilities.count).to eq 1
|
||||
|
||||
account = Operations::Account.find_by(kind: :main, currency_type: subject.currency.type, type: 'liability')
|
||||
expect(account.code).to eq liabilities.first.code
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
269
spec/models/market_spec.rb
Normal file
269
spec/models/market_spec.rb
Normal file
@@ -0,0 +1,269 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Market do
|
||||
context 'market attributes' do
|
||||
subject { Market.find(:btcusd) }
|
||||
|
||||
it 'id' do
|
||||
expect(subject.id).to eq 'btcusd'
|
||||
end
|
||||
|
||||
it 'name' do
|
||||
expect(subject.name).to eq 'BTC/USD'
|
||||
end
|
||||
|
||||
it 'base_currency' do
|
||||
expect(subject.base_unit).to eq 'btc'
|
||||
expect(subject.base_currency).to eq 'btc'
|
||||
end
|
||||
|
||||
it 'quote_currency' do
|
||||
expect(subject.quote_unit).to eq 'usd'
|
||||
expect(subject.quote_currency).to eq 'usd'
|
||||
end
|
||||
|
||||
it 'state' do
|
||||
expect(subject.state).to eq 'enabled'
|
||||
end
|
||||
|
||||
it 'data' do
|
||||
expect(subject.data).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'validations' do
|
||||
let(:valid_attributes) do
|
||||
{ base_currency: :btc,
|
||||
quote_currency: :trst,
|
||||
engine: create(:engine),
|
||||
min_amount: 0.0001,
|
||||
min_price: 0.0001,
|
||||
amount_precision: 4,
|
||||
price_precision: 4,
|
||||
position: 100 }
|
||||
end
|
||||
|
||||
let(:mirror_attributes) do
|
||||
{ base_currency: :usd,
|
||||
quote_currency: :btc,
|
||||
engine: create(:engine),
|
||||
min_amount: 0.0001,
|
||||
min_price: 0.0001,
|
||||
amount_precision: 4,
|
||||
price_precision: 4,
|
||||
position: 100 }
|
||||
end
|
||||
|
||||
let(:disabled_currency) { Currency.find_by_id(:eur) }
|
||||
|
||||
it 'creates valid record' do
|
||||
record = Market.new(valid_attributes)
|
||||
|
||||
expect(record.save).to eq true
|
||||
end
|
||||
|
||||
it 'validates quote currency duplication' do
|
||||
record = Market.new(valid_attributes.merge(quote_currency: valid_attributes[:base_currency]))
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/quote currency duplicates base currency/i)
|
||||
end
|
||||
|
||||
it 'validates same market' do
|
||||
record = build(:market, :btcusd)
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/market already exists/i)
|
||||
end
|
||||
|
||||
it 'validates mirror market pair' do
|
||||
record = Market.new(mirror_attributes)
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/market already exists/i)
|
||||
end
|
||||
|
||||
it 'validates presence of currencies' do
|
||||
%i[base_currency quote_currency].each do |field|
|
||||
record = Market.new(valid_attributes.except(field))
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/#{to_readable(field)} can't be blank/i)
|
||||
end
|
||||
end
|
||||
|
||||
it 'validates fields to be greater than or equal to 0' do
|
||||
%i[price_precision amount_precision].each do |field|
|
||||
record = Market.new(valid_attributes.merge(field => -1))
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/#{to_readable(field)} must be greater than or equal to 0/i)
|
||||
end
|
||||
end
|
||||
|
||||
it 'validates fields to be greater than or equal to top position' do
|
||||
record = Market.new(valid_attributes.merge(:position => 0))
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/position must be greater than or equal to 1/i)
|
||||
end
|
||||
|
||||
it 'validates fields to be greater than or equal to 0' do
|
||||
%i[price_precision amount_precision].each do |field|
|
||||
record = Market.new(valid_attributes.merge(field => 'test'))
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/#{to_readable(field)} is not a number/i)
|
||||
end
|
||||
end
|
||||
|
||||
it 'validates fields to be integer' do
|
||||
%i[price_precision amount_precision position].each do |field|
|
||||
record = Market.new(valid_attributes.merge(field => 0.1))
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/#{to_readable(field)} must be an integer/i)
|
||||
end
|
||||
end
|
||||
|
||||
it 'validates currencies codes to be inclusion of currency codes' do
|
||||
%i[base_currency quote_currency].each do |field|
|
||||
record = Market.new(valid_attributes.merge(field => :bad))
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/#{to_readable(field)} is not included in the list/i)
|
||||
end
|
||||
end
|
||||
|
||||
it 'validate position value on update' do
|
||||
market = Market.find(:btcusd)
|
||||
market.update(position: nil)
|
||||
expect(market.valid?).to eq false
|
||||
expect(market.errors[:position].size).to eq(2)
|
||||
|
||||
market.update(position: 0)
|
||||
expect(market.valid?).to eq false
|
||||
expect(market.errors[:position].size).to eq(1)
|
||||
end
|
||||
|
||||
it 'allows to disable all markets' do
|
||||
Market.where.not(id: :btcusd).update_all(state: :disabled)
|
||||
market = Market.find(:btcusd)
|
||||
market.update(state: :disabled)
|
||||
market.valid?
|
||||
expect(market.errors[:market].size).to eq(0)
|
||||
end
|
||||
|
||||
it 'validates min_amount from amount_precision variable' do
|
||||
record = Market.new(valid_attributes)
|
||||
expect(record.save).to eq true
|
||||
|
||||
# Delete record since amount_precision is readonly attribute.
|
||||
record.delete
|
||||
|
||||
record = Market.new(valid_attributes.merge(amount_precision: 2))
|
||||
expect(record.save).to eq false
|
||||
expect(record.errors.full_messages).to include(/#{to_readable(:min_amount)} must be greater than or equal to 0.01/i)
|
||||
end
|
||||
|
||||
it 'allows to set min_amount greater than value defined by amount_precision' do
|
||||
record = Market.new(valid_attributes.merge(min_amount: 1))
|
||||
expect(record.save).to eq true
|
||||
end
|
||||
|
||||
def to_readable(field)
|
||||
field.to_s.humanize.downcase
|
||||
end
|
||||
end
|
||||
|
||||
context 'relationships' do
|
||||
subject { Market.find(:btcusd) }
|
||||
before do
|
||||
create(:trading_fee, market_id: :btcusd)
|
||||
create(:trading_fee, market_id: :btceth)
|
||||
create(:trading_fee)
|
||||
end
|
||||
|
||||
it 'deletes only btcusd trading_fee' do
|
||||
expect { subject.destroy! }.to change(TradingFee, :count).by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'validate max market' do
|
||||
before { ENV['MAX_MARKETS'] = '2' }
|
||||
after { ENV['MAX_MARKETS'] = nil }
|
||||
|
||||
it 'should raise validation error for max market' do
|
||||
record = build(:market, :btctrst)
|
||||
record.save
|
||||
expect(record.errors.full_messages).to include(/Max Market limit has been reached/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'callbacks' do
|
||||
let(:valid_attributes) do
|
||||
{ base_currency: :btc,
|
||||
quote_currency: :trst,
|
||||
engine: create(:engine),
|
||||
min_amount: 0.0001,
|
||||
min_price: 0.0001,
|
||||
amount_precision: 4,
|
||||
price_precision: 4
|
||||
}
|
||||
end
|
||||
|
||||
context 'after_create' do
|
||||
|
||||
it 'move to the bottom if there is no position' do
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2]]
|
||||
Market.create(valid_attributes)
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2], ["btctrst", 3]]
|
||||
end
|
||||
|
||||
it 'move to the bottom of all currencies' do
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2]]
|
||||
Market.create(valid_attributes.merge(position: 3))
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2], ["btctrst", 3]]
|
||||
end
|
||||
|
||||
it 'move to the bottom when position is greater that currencies count' do
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2]]
|
||||
Market.create(valid_attributes.merge(position: Market.all.count + 2))
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2], ["btctrst", 3]]
|
||||
end
|
||||
|
||||
it 'move to the top of all currencies' do
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2]]
|
||||
Market.create(valid_attributes.merge(position: 1))
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btctrst", 1], ["btcusd", 2], ["btceth", 3]]
|
||||
end
|
||||
|
||||
it 'move to the middle of all currencies' do
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2]]
|
||||
Market.create(valid_attributes.merge(position: 2))
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btctrst", 2], ["btceth", 3]]
|
||||
end
|
||||
end
|
||||
|
||||
context 'before update' do
|
||||
let!(:btctrst) { Market.create(valid_attributes) }
|
||||
let(:btceth) { Market.find(:btceth) }
|
||||
|
||||
it 'move to the bottom of all currencies' do
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2], ["btctrst", 3]]
|
||||
btceth.update(position: 3)
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btctrst", 2], ["btceth", 3]]
|
||||
end
|
||||
|
||||
it 'move to the bottom when position is greater that markets count' do
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2], ["btctrst", 3]]
|
||||
btceth.update(position: Market.all.count + 2)
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btctrst", 2], ["btceth", 3]]
|
||||
end
|
||||
|
||||
it 'move to the top of all currencies' do
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2], ["btctrst", 3]]
|
||||
btceth.update(position: 1)
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btceth", 1], ["btcusd", 2], ["btctrst", 3]]
|
||||
end
|
||||
|
||||
it 'move to the middle of all currencies' do
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btceth", 2], ["btctrst", 3]]
|
||||
btctrst.update(position: 2)
|
||||
expect(Market.all.ordered.pluck(:id, :position)).to eq [["btcusd", 1], ["btctrst", 2], ["btceth", 3]]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
51
spec/models/member_spec.rb
Normal file
51
spec/models/member_spec.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Member do
|
||||
let(:member) { build(:member, :level_3) }
|
||||
subject { member }
|
||||
|
||||
describe 'uid' do
|
||||
subject(:member) { create(:member, :level_3) }
|
||||
it do
|
||||
expect(member.uid).to_not be_nil
|
||||
expect(member.uid).to_not be_empty
|
||||
expect(member.uid).to match(/\AID[A-Z0-9]{10}$/)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'username' do
|
||||
subject(:member) { create(:member, username: 'foobar') }
|
||||
it do
|
||||
expect(member.username).to_not be_nil
|
||||
expect(member.username).to_not be_empty
|
||||
expect(member.username).to eq 'foobar'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'before_create' do
|
||||
it 'should unify email' do
|
||||
create(:member, email: 'foo@example.com')
|
||||
expect(build(:member, email: 'Foo@example.com')).to_not be_valid
|
||||
end
|
||||
|
||||
it 'doesnt creates accounts for the member' do
|
||||
expect do
|
||||
member.save!
|
||||
end.not_to change(member.accounts, :count)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#trades' do
|
||||
subject { create(:member, :level_3) }
|
||||
|
||||
it 'should find all trades belong to user' do
|
||||
ask = create(:order_ask, :btcusd, member: member)
|
||||
bid = create(:order_bid, :btcusd, member: member)
|
||||
t1 = create(:trade, :btcusd, maker_order: ask)
|
||||
t2 = create(:trade, :btcusd, taker_order: bid)
|
||||
expect(member.trades.order('id')).to eq [t1, t2]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
108
spec/models/operations/account_spec.rb
Normal file
108
spec/models/operations/account_spec.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Operations::Account do
|
||||
let(:account) { Operations::Account.find_by(code: '101') }
|
||||
|
||||
describe '#code' do
|
||||
it 'validates presence' do
|
||||
account.code = nil
|
||||
account.valid?
|
||||
expect(account.errors[:code]).to include("can't be blank")
|
||||
end
|
||||
|
||||
it 'validates uniqueness' do
|
||||
account.code = '102' # an already existing code
|
||||
account.valid?
|
||||
expect(account.errors[:code]).to include("has already been taken")
|
||||
|
||||
account.code = '901' # a new code
|
||||
account.valid?
|
||||
expect(account.errors[:code]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#type' do
|
||||
it 'validates presence' do
|
||||
account.type = nil
|
||||
account.valid?
|
||||
expect(account.errors[:type]).to include("can't be blank")
|
||||
end
|
||||
|
||||
it 'validates inclusion' do
|
||||
account.type = 'an-nonexistent-type'
|
||||
account.valid?
|
||||
expect(account.errors[:type]).to include("is not included in the list")
|
||||
|
||||
account.type = Operations::Account::TYPES.first # an existent type
|
||||
account.valid?
|
||||
expect(account.errors[:type]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#kind' do
|
||||
it 'validates presence' do
|
||||
account.kind = nil
|
||||
account.valid?
|
||||
expect(account.errors[:kind]).to include("can't be blank")
|
||||
end
|
||||
|
||||
it 'validates uniqueness scoped to type and currency_type' do
|
||||
account = Operations::Account.new
|
||||
|
||||
# an already existing (kind, type, currency_type)
|
||||
account.kind = :main
|
||||
account.type = :asset
|
||||
account.currency_type = :fiat
|
||||
account.valid?
|
||||
expect(account.errors[:kind]).to include("has already been taken")
|
||||
|
||||
# a different type
|
||||
account.type = :different_type
|
||||
account.valid?
|
||||
expect(account.errors[:kind]).to be_empty
|
||||
|
||||
# restore existing type, but different currency_type
|
||||
account.type = :asset
|
||||
account.currency_type = :different_currency_type
|
||||
account.valid?
|
||||
expect(account.errors[:kind]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#currency_type' do
|
||||
it 'validates presence' do
|
||||
account.currency_type = nil
|
||||
account.valid?
|
||||
expect(account.errors[:currency_type]).to include("can't be blank")
|
||||
end
|
||||
|
||||
it 'validates inclusion' do
|
||||
account.currency_type = 'an-nonexistent-currency-type'
|
||||
account.valid?
|
||||
expect(account.errors[:currency_type]).to include("is not included in the list")
|
||||
|
||||
account.currency_type = Currency.types.first # an existent currency_type
|
||||
account.valid?
|
||||
expect(account.errors[:currency_type]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#scope' do
|
||||
it 'validates presence' do
|
||||
account.scope = nil
|
||||
account.valid?
|
||||
expect(account.errors[:scope]).to include("can't be blank")
|
||||
end
|
||||
|
||||
it 'validates inclusion' do
|
||||
account.scope = 'an-nonexistent-scope'
|
||||
account.valid?
|
||||
expect(account.errors[:scope]).to include("is not included in the list")
|
||||
|
||||
account.scope = Operations::Account::SCOPES.first # an existent scope
|
||||
account.valid?
|
||||
expect(account.errors[:scope]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
31
spec/models/operations/liability_spec.rb
Normal file
31
spec/models/operations/liability_spec.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Operations::Liability do
|
||||
context :after_commit do
|
||||
let(:member) { create(:member, :level_3, :barong) }
|
||||
|
||||
let(:deposit) { create(:deposit_usd, member: member, amount: 0.1) }
|
||||
|
||||
let(:expected_message) do
|
||||
{ subject: :operation,
|
||||
payload: {
|
||||
code: 201,
|
||||
currency: 'usd',
|
||||
member_id: member.id,
|
||||
reference_id: deposit.id,
|
||||
reference_type: 'deposit',
|
||||
debit: '0.0'.to_d,
|
||||
credit: '0.1'.to_d
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
before { AMQP::Queue.expects(:enqueue).with(:events_processor, expected_message) }
|
||||
|
||||
it 'publishes message to rabbitmq' do
|
||||
# Accept deposit for creation of liability.
|
||||
expect(deposit.accept!).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
14
spec/models/operations_spec.rb
Normal file
14
spec/models/operations_spec.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Operations do
|
||||
describe '.build_account_number' do
|
||||
it 'works' do
|
||||
expect(Operations.build_account_number(currency_id: :usd, account_code: 101, member_uid: "UID123")).to eq "usd-101-UID123"
|
||||
end
|
||||
|
||||
it 'works if UID not given' do
|
||||
expect(Operations.build_account_number(currency_id: :usd, account_code: 101)).to eq "usd-101"
|
||||
end
|
||||
end
|
||||
end
|
||||
52
spec/models/order_ask_spec.rb
Normal file
52
spec/models/order_ask_spec.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe OrderAsk do
|
||||
subject { create(:order_ask, :btcusd) }
|
||||
|
||||
it { expect(subject.compute_locked).to eq subject.volume }
|
||||
|
||||
let(:market) do
|
||||
Market.find(:btcusd).tap { |m| m.update(min_price: 0.1, min_amount: 0.1, max_price: 0.11) }
|
||||
end
|
||||
|
||||
context 'compute locked for market order' do
|
||||
let!(:orders) do
|
||||
create(:order_ask, :btcusd, price: '202', volume: '10.0', state: :wait)
|
||||
create(:order_ask, :btcusd, price: '201', volume: '10.0', state: :wait)
|
||||
create(:order_ask, :btcusd, price: '200', volume: '10.0', state: :wait)
|
||||
create(:order_ask, :btcusd, price: '100', volume: '10.0', state: :wait)
|
||||
end
|
||||
|
||||
it 'should require locked in order funds' do
|
||||
bid = OrderBid.new(market_id: :btcusd, volume: '5'.to_d, ord_type: 'market').compute_locked
|
||||
expect(bid).to eq('500'.to_d)
|
||||
end
|
||||
|
||||
it 'should make sure price is greater than min_price' do
|
||||
ask = OrderAsk.new(market_id: market.id, price: '0.0'.to_d, ord_type: 'limit')
|
||||
expect(ask).not_to be_valid
|
||||
expect(ask.errors[:price]).to include "must be greater than or equal to #{market.min_price}"
|
||||
end
|
||||
|
||||
it 'should raise error if the market is not deep enough' do
|
||||
expect do
|
||||
OrderAsk.new(market_id: :btcusd, volume: '50'.to_d, ord_type: 'market').compute_locked
|
||||
end.to raise_error(Order::InsufficientMarketLiquidity)
|
||||
end
|
||||
|
||||
it 'should make sure amount is greater than zero' do
|
||||
ask_amount = OrderAsk.new(market_id: market.id, origin_volume: '0.0'.to_d)
|
||||
expect(ask_amount).not_to be_valid
|
||||
|
||||
expect(ask_amount.errors[:origin_volume]).to include "must be greater than 0"
|
||||
end
|
||||
|
||||
it 'should make sure amount is greater than min_amount' do
|
||||
ask_amount = OrderAsk.new(market_id: market.id, origin_volume: '0.05'.to_d)
|
||||
expect(ask_amount).not_to be_valid
|
||||
|
||||
expect(ask_amount.errors[:origin_volume]).to include "must be greater than or equal to #{market.min_amount}"
|
||||
end
|
||||
end
|
||||
end
|
||||
54
spec/models/order_bid_spec.rb
Normal file
54
spec/models/order_bid_spec.rb
Normal file
@@ -0,0 +1,54 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe OrderBid do
|
||||
subject { create(:order_bid, :btcusd) }
|
||||
|
||||
it { expect(subject.compute_locked).to eq subject.volume * subject.price }
|
||||
|
||||
let(:market) do
|
||||
Market.find(:btcusd).tap { |m| m.update(max_price: 1.0, min_amount: 0.1)}
|
||||
end
|
||||
|
||||
context 'compute locked for market order' do
|
||||
let!(:ask_orders) do
|
||||
create(:order_ask, :btcusd, price: '200', volume: '10.0', state: :wait)
|
||||
create(:order_ask, :btcusd, price: '102', volume: '10.0', state: :wait)
|
||||
create(:order_ask, :btcusd, price: '101', volume: '10.0', state: :wait)
|
||||
create(:order_ask, :btcusd, price: '100', volume: '10.0', state: :wait)
|
||||
end
|
||||
|
||||
let!(:bid_orders) do
|
||||
create(:order_bid, :btcusd, price: '200', volume: '10.0', state: :wait)
|
||||
create(:order_bid, :btcusd, price: '102', volume: '10.0', state: :wait)
|
||||
create(:order_bid, :btcusd, price: '101', volume: '10.0', state: :wait)
|
||||
create(:order_bid, :btcusd, price: '100', volume: '10.0', state: :wait)
|
||||
end
|
||||
|
||||
it 'should require a volume' do
|
||||
expect(OrderAsk.new(market_id: :btcusd, volume: '5'.to_d, ord_type: 'market').compute_locked).to eq '5'.to_d
|
||||
end
|
||||
|
||||
it 'should require a volume' do
|
||||
expect(OrderAsk.new(market_id: :btcusd, volume: '25'.to_d, ord_type: 'market').compute_locked).to eq '25'.to_d
|
||||
end
|
||||
|
||||
it 'should raise error if the market is not deep enough' do
|
||||
expect do
|
||||
OrderBid.new(market_id: :btcusd, volume: '50'.to_d, ord_type: 'market').compute_locked
|
||||
end.to raise_error(Order::InsufficientMarketLiquidity)
|
||||
end
|
||||
|
||||
it 'should make sure price is less than max_price' do
|
||||
bid = OrderBid.new(market_id: market.id, price: '10.0'.to_d, ord_type: 'limit')
|
||||
expect(bid).not_to be_valid
|
||||
expect(bid.errors[:price]).to include "must be less than or equal to #{market.max_price}"
|
||||
end
|
||||
|
||||
it 'should make sure amount is greater than min_bid' do
|
||||
bid_amount = OrderBid.new(market_id: market.id, origin_volume: '0.0'.to_d)
|
||||
expect(bid_amount).not_to be_valid
|
||||
expect(bid_amount.errors[:origin_volume]).to include "must be greater than or equal to #{market.min_amount}"
|
||||
end
|
||||
end
|
||||
end
|
||||
377
spec/models/order_spec.rb
Normal file
377
spec/models/order_spec.rb
Normal file
@@ -0,0 +1,377 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Order, 'validations', type: :model do
|
||||
context 'validations' do
|
||||
subject do
|
||||
Order.validators
|
||||
.select { |v| v.is_a? ActiveRecord::Validations::PresenceValidator }
|
||||
.map(&:attributes)
|
||||
.flatten
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to include :ord_type
|
||||
is_expected.to include :volume
|
||||
is_expected.to include :origin_volume
|
||||
is_expected.to include :locked
|
||||
is_expected.to include :origin_locked
|
||||
end
|
||||
end
|
||||
|
||||
context 'limit order' do
|
||||
it 'should make sure price is present' do
|
||||
order = OrderAsk.new(market_id: 'btcusd', price: nil, ord_type: 'limit')
|
||||
expect(order).not_to be_valid
|
||||
expect(order.errors[:price]).to include 'is not a number'
|
||||
end
|
||||
end
|
||||
|
||||
context 'market order' do
|
||||
it 'should make sure price is not present' do
|
||||
order = OrderAsk.new(market_id: 'btcusd', price: '0.0'.to_d, ord_type: 'market')
|
||||
expect(order).not_to be_valid
|
||||
expect(order.errors[:price]).to include 'must not be present'
|
||||
end
|
||||
end
|
||||
|
||||
context 'attr_readonly' do
|
||||
let!(:order) { create(:order_bid, :btcusd) }
|
||||
|
||||
it "does not allow updating readonly attributes" do
|
||||
expect { order.update_attribute(:member_id, 1) }.to \
|
||||
raise_error(ActiveRecord::ActiveRecordError, 'member_id is marked as readonly')
|
||||
|
||||
expect { order.update_attribute(:bid, 'xyz') }.to \
|
||||
raise_error(ActiveRecord::ActiveRecordError, 'bid is marked as readonly')
|
||||
|
||||
expect { order.update_attribute(:ask, 'abc') }.to \
|
||||
raise_error(ActiveRecord::ActiveRecordError, 'ask is marked as readonly')
|
||||
|
||||
expect { order.update_attribute(:market_id, 'abcxyz') }.to \
|
||||
raise_error(ActiveRecord::ActiveRecordError, 'market_id is marked as readonly')
|
||||
|
||||
expect { order.update_attribute(:ord_type, 'market') }.to \
|
||||
raise_error(ActiveRecord::ActiveRecordError, 'ord_type is marked as readonly')
|
||||
|
||||
expect { order.update_attribute(:origin_volume, 1) }.to \
|
||||
raise_error(ActiveRecord::ActiveRecordError, 'origin_volume is marked as readonly')
|
||||
|
||||
expect { order.update_attribute(:origin_locked, 1) }.to \
|
||||
raise_error(ActiveRecord::ActiveRecordError, 'origin_locked is marked as readonly')
|
||||
|
||||
expect { order.update_attribute(:created_at, '2009-01-03') }.to \
|
||||
raise_error(ActiveRecord::ActiveRecordError, 'created_at is marked as readonly')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Order, '#submit' do
|
||||
let(:order) { create(:order_bid, :with_deposit_liability, state: 'pending', price: '12.32'.to_d, volume: '123.12345678') }
|
||||
let(:rejected_order) { create(:order_bid, :with_deposit_liability, state: 'reject', price: '12.32'.to_d, volume: '123.12345678') }
|
||||
let(:order_bid) { create(:order_bid, :with_deposit_liability, state: 'pending', price: '12.32'.to_d, volume: '123.12345678') }
|
||||
let(:order_ask) { create(:order_ask, :with_deposit_liability, state: 'pending', price: '12.32'.to_d, volume: '123.12345678') }
|
||||
|
||||
before do
|
||||
Order.submit(order_bid.id)
|
||||
Order.submit(order_ask.id)
|
||||
end
|
||||
|
||||
it do
|
||||
expect(order_bid.reload.state).to eq 'wait'
|
||||
expect(Operations::Liability.where(reference: order_ask).count).to eq 2
|
||||
expect(Operations::Liability.where(reference: order_bid).count).to eq 2
|
||||
end
|
||||
|
||||
context 'validations' do
|
||||
before do
|
||||
order.member.accounts.find_by_currency_id(order.currency).update(balance: 0)
|
||||
end
|
||||
|
||||
it 'insufficient balance' do
|
||||
expect {
|
||||
Order.submit(order.id)
|
||||
}.to raise_error(Account::AccountError)
|
||||
expect(order.reload.state).to eq('reject')
|
||||
end
|
||||
|
||||
it 'rejected order' do
|
||||
Order.submit(rejected_order.id)
|
||||
expect(rejected_order.reload.state).to eq('reject')
|
||||
end
|
||||
end
|
||||
|
||||
it 'mysql connection error' do
|
||||
ActiveRecord::Base.stubs(:transaction).raises(Mysql2::Error::ConnectionError.new(''))
|
||||
expect { Order.submit(order.id) }.to raise_error(Mysql2::Error::ConnectionError)
|
||||
end
|
||||
end
|
||||
|
||||
describe Order, '#cancel' do
|
||||
let(:order) { create(:order_bid, :with_deposit_liability, state: 'pending', price: '12.32'.to_d, volume: '123.12345678') }
|
||||
|
||||
it 'mysql connection error' do
|
||||
ActiveRecord::Base.stubs(:transaction).raises(Mysql2::Error::ConnectionError.new(''))
|
||||
expect { Order.cancel(order.id) }.to raise_error(Mysql2::Error::ConnectionError)
|
||||
end
|
||||
end
|
||||
|
||||
describe Order, 'precision validations', type: :model do
|
||||
let(:order_bid) { build(:order_bid, :btcusd, price: '12.32'.to_d, volume: '123.123456789') }
|
||||
let(:order_ask) { build(:order_ask, :btcusd, price: '12.326'.to_d, volume: '123.12345678') }
|
||||
|
||||
it 'validates origin_volume precision' do
|
||||
record = order_bid
|
||||
expect(record.save).to eq false
|
||||
expect(record.errors[:origin_volume]).to include(/precision must be less than or equal to 8/i)
|
||||
end
|
||||
|
||||
it 'validates price precision' do
|
||||
record = order_ask
|
||||
expect(record.save).to eq false
|
||||
expect(record.errors[:price]).to include(/precision must be less than or equal to 2/i)
|
||||
end
|
||||
end
|
||||
|
||||
describe Order, '#done', type: :model do
|
||||
let(:ask_fee) { '0.003'.to_d }
|
||||
let(:bid_fee) { '0.001'.to_d }
|
||||
let(:order) { order_bid }
|
||||
let(:order_bid) { create(:order_bid, :btcusd, price: '1.2'.to_d, volume: '10.0'.to_d) }
|
||||
let(:order_ask) { create(:order_ask, :btcusd, price: '1.2'.to_d, volume: '10.0'.to_d) }
|
||||
let(:hold_account) { create_account(:usd, locked: '100.0'.to_d) }
|
||||
let(:expect_account) { create_account(:btc) }
|
||||
|
||||
before do
|
||||
order_bid.stubs(:hold_account!).returns(hold_account.lock!)
|
||||
order_bid.stubs(:expect_account!).returns(expect_account.lock!)
|
||||
order_ask.stubs(:hold_account!).returns(hold_account.lock!)
|
||||
order_ask.stubs(:expect_account!).returns(expect_account.lock!)
|
||||
OrderBid.any_instance.stubs(:fee).returns(bid_fee)
|
||||
OrderAsk.any_instance.stubs(:fee).returns(ask_fee)
|
||||
end
|
||||
|
||||
def mock_trade(volume, price)
|
||||
build(:trade, volume: volume, price: price, id: rand(10))
|
||||
end
|
||||
|
||||
shared_examples 'trade done' do
|
||||
before do
|
||||
hold_account.reload
|
||||
expect_account.reload
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Order, '#kind' do
|
||||
it 'should be ask for ask order' do
|
||||
expect(OrderAsk.new.kind).to eq 'ask'
|
||||
end
|
||||
|
||||
it 'should be bid for bid order' do
|
||||
expect(OrderBid.new.kind).to eq 'bid'
|
||||
end
|
||||
end
|
||||
|
||||
describe Order, 'related accounts' do
|
||||
let(:alice) { who_is_billionaire }
|
||||
let(:bob) { who_is_billionaire }
|
||||
|
||||
context OrderAsk do
|
||||
it 'should hold btc and expect usd' do
|
||||
ask = create(:order_ask, :btcusd, member: alice)
|
||||
expect(ask.hold_account).to eq alice.get_account(:btc)
|
||||
expect(ask.expect_account).to eq alice.get_account(:usd)
|
||||
end
|
||||
end
|
||||
|
||||
context OrderBid do
|
||||
it 'should hold usd and expect btc' do
|
||||
bid = create(:order_bid, :btcusd, member: bob)
|
||||
expect(bid.hold_account).to eq bob.get_account(:usd)
|
||||
expect(bid.expect_account).to eq bob.get_account(:btc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Order, '#avg_price' do
|
||||
it 'should be zero if not filled yet' do
|
||||
expect(
|
||||
OrderAsk.new(
|
||||
locked: '1.0',
|
||||
origin_locked: '1.0',
|
||||
volume: '1.0',
|
||||
origin_volume: '1.0',
|
||||
funds_received: '0'
|
||||
).avg_price
|
||||
).to eq '0'.to_d
|
||||
|
||||
expect(
|
||||
OrderBid.new(
|
||||
locked: '1.0',
|
||||
origin_locked: '1.0',
|
||||
volume: '1.0',
|
||||
origin_volume: '1.0',
|
||||
funds_received: '0'
|
||||
).avg_price
|
||||
).to eq '0'.to_d
|
||||
end
|
||||
|
||||
it 'should calculate average price of bid order' do
|
||||
expect(
|
||||
OrderBid.new(
|
||||
market_id: 'btcusd',
|
||||
locked: '10.0',
|
||||
origin_locked: '20.0',
|
||||
volume: '1.0',
|
||||
origin_volume: '3.0',
|
||||
funds_received: '2.0'
|
||||
).avg_price
|
||||
).to eq '5'.to_d
|
||||
end
|
||||
|
||||
it 'should calculate average price of ask order' do
|
||||
expect(
|
||||
OrderAsk.new(
|
||||
market_id: 'btcusd',
|
||||
locked: '1.0',
|
||||
origin_locked: '2.0',
|
||||
volume: '1.0',
|
||||
origin_volume: '2.0',
|
||||
funds_received: '10.0'
|
||||
).avg_price
|
||||
).to eq '10'.to_d
|
||||
end
|
||||
end
|
||||
|
||||
describe Order, '#record_submit_operations!' do
|
||||
# Persist Order in database.
|
||||
let!(:order){ create(:order_ask, :btcusd, :with_deposit_liability) }
|
||||
|
||||
subject { order }
|
||||
|
||||
it 'creates two liability operations' do
|
||||
expect{ subject.record_submit_operations! }.to change{ Operations::Liability.count }.by(2)
|
||||
end
|
||||
|
||||
it 'doesn\'t create asset operations' do
|
||||
expect{ subject.record_submit_operations! }.to_not change{ Operations::Asset.count }
|
||||
end
|
||||
|
||||
it 'debits main liabilities for member' do
|
||||
expect{ subject.record_submit_operations! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :main)
|
||||
}.by(-subject.locked)
|
||||
end
|
||||
|
||||
it 'credits locked liabilities for member' do
|
||||
expect{ subject.record_submit_operations! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
||||
}.by(subject.locked)
|
||||
end
|
||||
end
|
||||
|
||||
describe Order, '#record_cancel_operations!' do
|
||||
# Persist Order in database.
|
||||
let!(:order){ create(:order_ask, :with_deposit_liability) }
|
||||
|
||||
subject { order }
|
||||
before { subject.record_submit_operations! }
|
||||
|
||||
it 'creates two liability operations' do
|
||||
expect{ subject.record_cancel_operations! }.to change{ Operations::Liability.count }.by(2)
|
||||
end
|
||||
|
||||
it 'doesn\'t create asset operations' do
|
||||
expect{ subject.record_cancel_operations! }.to_not change{ Operations::Asset.count }
|
||||
end
|
||||
|
||||
it 'credits main liabilities for member' do
|
||||
expect{ subject.record_cancel_operations! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :main)
|
||||
}.by(subject.locked)
|
||||
end
|
||||
|
||||
it 'debits locked liabilities for member' do
|
||||
expect{ subject.record_cancel_operations! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
||||
}.by(-subject.locked)
|
||||
end
|
||||
end
|
||||
|
||||
describe Order, '#trigger_event' do
|
||||
|
||||
context 'trigger pusher event for limit order' do
|
||||
let!(:order){ create(:order_ask, :with_deposit_liability) }
|
||||
|
||||
subject { order }
|
||||
|
||||
let(:data) do
|
||||
{
|
||||
id: subject.id,
|
||||
market: subject.market_id,
|
||||
kind: subject.kind,
|
||||
side: subject.side,
|
||||
ord_type: subject.ord_type,
|
||||
price: subject.price&.to_s('F'),
|
||||
avg_price: subject.avg_price&.to_s('F'),
|
||||
state: subject.state,
|
||||
origin_volume: subject.origin_volume.to_s('F'),
|
||||
remaining_volume: subject.volume.to_s('F'),
|
||||
executed_volume: (subject.origin_volume - subject.volume).to_s('F'),
|
||||
at: subject.created_at.to_i,
|
||||
created_at: subject.created_at.to_i,
|
||||
updated_at: subject.updated_at.to_i,
|
||||
trades_count: subject.trades_count,
|
||||
}
|
||||
end
|
||||
|
||||
before { ::AMQP::Queue.expects(:enqueue_event).with('private', subject.member.uid, 'order', data) }
|
||||
|
||||
it { subject.trigger_event }
|
||||
end
|
||||
|
||||
context 'trigger pusher event for market order' do
|
||||
let!(:order) { create(:order_ask, :with_deposit_liability, ord_type: 'market', price: nil) }
|
||||
|
||||
subject { order }
|
||||
|
||||
let(:data) do
|
||||
{
|
||||
id: subject.id,
|
||||
market: subject.market_id,
|
||||
kind: subject.kind,
|
||||
side: subject.side,
|
||||
ord_type: subject.ord_type,
|
||||
price: subject.price&.to_s('F'),
|
||||
avg_price: subject.avg_price&.to_s('F'),
|
||||
state: subject.state,
|
||||
origin_volume: subject.origin_volume.to_s('F'),
|
||||
remaining_volume: subject.volume.to_s('F'),
|
||||
executed_volume: (subject.origin_volume - subject.volume).to_s('F'),
|
||||
at: subject.created_at.to_i,
|
||||
created_at: subject.created_at.to_i,
|
||||
updated_at: subject.updated_at.to_i,
|
||||
trades_count: subject.trades_count
|
||||
}
|
||||
end
|
||||
|
||||
it 'doesnt push event for active market order' do
|
||||
::AMQP::Queue.expects(:enqueue_event).with(:order, data).never
|
||||
subject.trigger_event
|
||||
end
|
||||
|
||||
it 'pushes event for completed market order' do
|
||||
subject.expects(:trigger_event)
|
||||
subject.update!(state: 'done')
|
||||
end
|
||||
|
||||
context do
|
||||
it do
|
||||
subject.update!(state: 'done')
|
||||
::AMQP::Queue.expects(:enqueue_event).with('private', subject.member.uid, 'order', data)
|
||||
subject.trigger_event
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
42
spec/models/payment_address_spec.rb
Normal file
42
spec/models/payment_address_spec.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe PaymentAddress do
|
||||
context '.create' do
|
||||
let(:member) { create(:member, :level_3) }
|
||||
let!(:account) { member.get_account(:btc) }
|
||||
let!(:wallet) { Wallet.joins(:currencies).find_by(currencies: { id: :btc }) }
|
||||
let(:secret) { 's3cr3t' }
|
||||
let(:details) { { 'a' => 'b', 'b' => 'c' } }
|
||||
let!(:addr) { create(:payment_address, :btc_address, address: nil, secret: secret, wallet_id: wallet.id) }
|
||||
|
||||
it 'generate address after commit' do
|
||||
AMQP::Queue.expects(:enqueue).with(:deposit_coin_address, { member_id: member.id, wallet_id: wallet.id }, { persistent: true })
|
||||
member.payment_address(wallet.id)
|
||||
end
|
||||
|
||||
it 'updates secret' do
|
||||
expect {
|
||||
addr.update(secret: 'new_secret')
|
||||
}.to change { addr.reload.secret_encrypted }.and change { addr.reload.secret }.to 'new_secret'
|
||||
end
|
||||
|
||||
it 'updates details' do
|
||||
expect {
|
||||
addr.update(details: details)
|
||||
}.to change { addr.reload.details_encrypted }.and change { addr.reload.details }.to details
|
||||
end
|
||||
|
||||
it 'long secret' do
|
||||
expect {
|
||||
addr.update(secret: Faker::String.random(1024))
|
||||
}.to raise_error ActiveRecord::ValueTooLong
|
||||
end
|
||||
|
||||
it 'long details' do
|
||||
expect {
|
||||
addr.update(details: { test: Faker::String.random(1024) })
|
||||
}.to raise_error ActiveRecord::ValueTooLong
|
||||
end
|
||||
end
|
||||
end
|
||||
348
spec/models/trade_spec.rb
Normal file
348
spec/models/trade_spec.rb
Normal file
@@ -0,0 +1,348 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Trade, '#for_notify' do
|
||||
let(:order_ask) { create(:order_ask, :btcusd) }
|
||||
let(:order_bid) { create(:order_bid, :btcusd) }
|
||||
let(:trade) { create(:trade, :btcusd, maker_order: order_ask, taker_order: order_bid) }
|
||||
|
||||
subject(:notify) { trade.for_notify(order_ask.member) }
|
||||
|
||||
it do
|
||||
expect(notify).not_to be_blank
|
||||
expect(notify[:side]).not_to be_blank
|
||||
expect(notify[:created_at]).not_to be_blank
|
||||
expect(notify[:price]).not_to be_blank
|
||||
expect(notify[:amount]).not_to be_blank
|
||||
expect(notify[:order_id]).to eq(order_ask.id)
|
||||
end
|
||||
|
||||
it 'should use side as kind' do
|
||||
expect(trade.for_notify(Member.find(trade.maker_id))[:side]).to eq 'sell'
|
||||
end
|
||||
|
||||
context 'notify for bid member' do
|
||||
subject(:notify) { trade.for_notify(order_bid.member) }
|
||||
|
||||
it do
|
||||
expect(notify).not_to be_blank
|
||||
expect(notify[:side]).not_to be_blank
|
||||
expect(notify[:created_at]).not_to be_blank
|
||||
expect(notify[:price]).not_to be_blank
|
||||
expect(notify[:amount]).not_to be_blank
|
||||
expect(notify[:order_id]).to eq(order_bid.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Trade, '#trade_from_influx_after_date' do
|
||||
after { delete_measurments("trades") }
|
||||
|
||||
context 'no trades executed yet' do
|
||||
it 'returns trade' do
|
||||
expect(Trade.trade_from_influx_after_date(:btcusd, Time.now)).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'single trade was executing' do
|
||||
let(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
|
||||
let(:expected_trade) do
|
||||
{
|
||||
:id=>trade.id,
|
||||
:price=>5,
|
||||
:amount=>1.1,
|
||||
:total=>5.5,
|
||||
:taker_type=>trade.taker_type,
|
||||
:market=>'btcusd',
|
||||
:created_at=>trade.created_at.to_i
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
trade.write_to_influx
|
||||
end
|
||||
|
||||
it 'returns trade' do
|
||||
expect(Trade.trade_from_influx_after_date(trade.market_id, Time.now - 1.minutes).except(:time)).to eq(expected_trade)
|
||||
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, 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 + 3.minutes)}
|
||||
let!(:trade3) { create(:trade, :btcusd, price: '7.0'.to_d, amount: '0.9'.to_d, total: '5.4'.to_d, created_at: Time.now + 4.minutes)}
|
||||
|
||||
let(:expected_trade) do
|
||||
{
|
||||
:id=>trade1.id,
|
||||
:price=>5.0,
|
||||
:amount=>1.1,
|
||||
:total=>5.5,
|
||||
:taker_type=>trade1.taker_type,
|
||||
:market=>'btcusd',
|
||||
:created_at=>trade1.created_at.to_i
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
trade1.write_to_influx
|
||||
trade2.write_to_influx
|
||||
trade3.write_to_influx
|
||||
end
|
||||
|
||||
it 'returns trade' do
|
||||
expect(Trade.trade_from_influx_after_date(trade1.market_id, Time.now - 1.minutes).except(:time)).to eq(expected_trade)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Trade, '#trade_from_influx_before_date' do
|
||||
after { delete_measurments("trades") }
|
||||
|
||||
context 'no trades executed yet' do
|
||||
it 'returns trade' do
|
||||
expect(Trade.trade_from_influx_before_date(:btcusd, Time.now)).to eq([])
|
||||
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_trade) do
|
||||
{
|
||||
:id=>trade.id,
|
||||
:price=>5,
|
||||
:amount=>1.1,
|
||||
:total=>5.5,
|
||||
:taker_type=>trade.taker_type,
|
||||
:market=>'btcusd',
|
||||
:created_at=>trade.created_at.to_i
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
trade.write_to_influx
|
||||
end
|
||||
|
||||
it 'returns trade' do
|
||||
expect(Trade.trade_from_influx_before_date(trade.market_id, Time.now + 3.minutes).except(:time)).to eq(expected_trade)
|
||||
end
|
||||
end
|
||||
|
||||
context 'multiple trades were executed' do
|
||||
let!(:trade3) { create(:trade, :btcusd, price: '7.0'.to_d, amount: '0.9'.to_d, total: '5.4'.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: 2.days.ago)}
|
||||
let!(:trade1) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d, created_at: 3.days.ago)}
|
||||
let(:expected_trade) do
|
||||
{
|
||||
:id=>trade3.id,
|
||||
:price=>7,
|
||||
:amount=>0.9,
|
||||
:total=>5.4,
|
||||
:taker_type=>trade3.taker_type,
|
||||
:market=>'btcusd',
|
||||
:created_at=>trade3.created_at.to_i
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
trade1.write_to_influx
|
||||
trade2.write_to_influx
|
||||
trade3.write_to_influx
|
||||
end
|
||||
|
||||
it 'returns trade' do
|
||||
expect(Trade.trade_from_influx_before_date(trade1.market_id, Time.now + 3.minutes).except(:time)).to eq(expected_trade)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Trade, '#nearest_trade_from_influx' do
|
||||
after { delete_measurments("trades") }
|
||||
|
||||
context 'no trades executed yet' do
|
||||
it 'returns trade' do
|
||||
expect(Trade.nearest_trade_from_influx(:btcusd, Time.now)).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'trade executed before date' do
|
||||
let(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
|
||||
let(:expected_trade) do
|
||||
{
|
||||
:id=>trade.id,
|
||||
:price=>5,
|
||||
:amount=>1.1,
|
||||
:total=>5.5,
|
||||
:taker_type=>trade.taker_type,
|
||||
:market=>'btcusd',
|
||||
:created_at=>trade.created_at.to_i
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
trade.write_to_influx
|
||||
end
|
||||
|
||||
it 'returns trade' do
|
||||
expect(Trade.nearest_trade_from_influx(trade.market_id, Time.now + 3.minutes).except(:time)).to eq(expected_trade)
|
||||
end
|
||||
end
|
||||
|
||||
context 'trade executed after date' do
|
||||
let(:trade) { create(:trade, :btcusd, price: '5.0'.to_d, amount: '1.1'.to_d, total: '5.5'.to_d)}
|
||||
let(:expected_trade) do
|
||||
{
|
||||
:id=>trade.id,
|
||||
:price=>5,
|
||||
:amount=>1.1,
|
||||
:total=>5.5,
|
||||
:taker_type=>trade.taker_type,
|
||||
:market=>'btcusd',
|
||||
:created_at=>trade.created_at.to_i
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
trade.write_to_influx
|
||||
end
|
||||
|
||||
it 'returns trade' do
|
||||
expect(Trade.nearest_trade_from_influx(trade.market_id, Time.now).except(:time)).to eq(expected_trade)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Trade, '#market_ticker_from_influx' do
|
||||
after { delete_measurments("trades") }
|
||||
|
||||
context 'no trades executed yet' do
|
||||
it 'returns ticker' do
|
||||
expect(Trade.market_ticker_from_influx(:btcusd)).to eq([])
|
||||
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
|
||||
{
|
||||
:min=>5,
|
||||
:max=>5,
|
||||
:first=>5,
|
||||
:last=>5,
|
||||
:volume=>5.5,
|
||||
:amount=>1.1,
|
||||
:vwap=>5
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
trade.write_to_influx
|
||||
end
|
||||
|
||||
it 'returns ticker' do
|
||||
expect(Trade.market_ticker_from_influx(trade.market_id).except(:time)).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!(:trade3) { create(:trade, :btcusd, price: '7.0'.to_d, amount: '0.9'.to_d, total: '5.4'.to_d)}
|
||||
let(:expected_ticker) do
|
||||
{
|
||||
:amount => 2.9,
|
||||
:first => 5,
|
||||
:last => 7,
|
||||
:max => 7,
|
||||
:min => 5,
|
||||
:volume => 16.3,
|
||||
:vwap => 5.620689655172415
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
trade1.write_to_influx
|
||||
trade2.write_to_influx
|
||||
trade3.write_to_influx
|
||||
end
|
||||
|
||||
it 'returns ticker' do
|
||||
expect(Trade.market_ticker_from_influx(trade1.market_id).except(:time)).to eq(expected_ticker)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Trade, '#record_complete_operations!' do
|
||||
# Persist orders and trades in database.
|
||||
let!(:trade){ create(:trade, :btcusd, :with_deposit_liability) }
|
||||
|
||||
let(:ask){ trade.maker_order }
|
||||
let(:bid){ trade.taker_order }
|
||||
|
||||
let(:ask_currency_outcome){ trade.amount }
|
||||
let(:bid_currency_outcome){ trade.total }
|
||||
|
||||
let(:ask_currency_fee){ trade.amount * trade.order_fee(bid) }
|
||||
let(:bid_currency_fee){ trade.total * trade.order_fee(ask) }
|
||||
|
||||
let(:ask_currency_income){ ask_currency_outcome - ask_currency_fee }
|
||||
let(:bid_currency_income){ bid_currency_outcome - bid_currency_fee }
|
||||
|
||||
subject{ trade }
|
||||
|
||||
it 'creates four liability operations' do
|
||||
expect{ subject.record_complete_operations! }.to change{ Operations::Liability.count }.by(4)
|
||||
end
|
||||
|
||||
it 'doesn\'t create asset operations' do
|
||||
expect{ subject.record_complete_operations! }.to_not change{ Operations::Asset.count }
|
||||
end
|
||||
|
||||
it 'debits locked ask liabilities for ask creator' do
|
||||
expect{ subject.record_complete_operations! }.to change {
|
||||
ask.member.balance_for(currency: ask.currency, kind: :locked)
|
||||
}.by(-ask_currency_outcome)
|
||||
end
|
||||
|
||||
it 'debits locked bid liabilities for bid creator' do
|
||||
expect{ subject.record_complete_operations! }.to change {
|
||||
bid.member.balance_for(currency: bid.currency, kind: :locked)
|
||||
}.by(-bid_currency_outcome)
|
||||
end
|
||||
|
||||
it 'credits main bid liabilities for ask creator' do
|
||||
expect{ subject.record_complete_operations! }.to change {
|
||||
ask.member.balance_for(currency: bid.currency, kind: :main)
|
||||
}.by(bid_currency_income)
|
||||
end
|
||||
|
||||
it 'credits main ask liabilities for bid creator' do
|
||||
expect{ subject.record_complete_operations! }.to change {
|
||||
bid.member.balance_for(currency: ask.currency, kind: :main)
|
||||
}.by(ask_currency_income)
|
||||
end
|
||||
|
||||
it 'credits ask currency revenues' do
|
||||
expect{ subject.record_complete_operations! }.to change {
|
||||
Operations::Revenue.balance(currency: ask.currency)
|
||||
}.by(ask_currency_fee)
|
||||
end
|
||||
|
||||
it 'credits bid currency revenues' do
|
||||
expect{ subject.record_complete_operations! }.to change {
|
||||
Operations::Revenue.balance(currency: bid.currency)
|
||||
}.by(bid_currency_fee)
|
||||
end
|
||||
|
||||
it 'creates ask currency revenue from bid creator' do
|
||||
expect{ subject.record_complete_operations! }.to change {
|
||||
Operations::Revenue.where(currency: ask.currency, member: bid.member).count
|
||||
}.by(1)
|
||||
end
|
||||
|
||||
it 'creates bid currency revenue from ask creator' do
|
||||
expect{ subject.record_complete_operations! }.to change {
|
||||
Operations::Revenue.where(currency: bid.currency, member: ask.member).count
|
||||
}.by(1)
|
||||
end
|
||||
end
|
||||
223
spec/models/tradinng_fee_spec.rb
Normal file
223
spec/models/tradinng_fee_spec.rb
Normal file
@@ -0,0 +1,223 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe TradingFee, 'Relationships' do
|
||||
context 'belongs to market' do
|
||||
context 'null market_id' do
|
||||
subject { build(:trading_fee) }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
|
||||
context 'existing market_id' do
|
||||
subject { build(:trading_fee, market_id: :btcusd) }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
|
||||
context 'non-existing market_id' do
|
||||
subject { build(:trading_fee, market_id: :usdbtc) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe TradingFee, 'Validations' do
|
||||
before(:each) { TradingFee.delete_all }
|
||||
|
||||
context 'group presence' do
|
||||
context 'nil group' do
|
||||
subject { build(:trading_fee, market_id: :btceth, group: nil) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
|
||||
context 'empty string group' do
|
||||
subject { build(:trading_fee, market_id: :btceth, group: '') }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'group uniqueness' do
|
||||
context 'different markets' do
|
||||
before { create(:trading_fee, market_id: :btcusd, group: 'vip-1') }
|
||||
|
||||
context 'same group' do
|
||||
subject { build(:trading_fee, market_id: :btceth, group: 'vip-1') }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
|
||||
context 'different group' do
|
||||
subject { build(:trading_fee, market_id: :btceth, group: 'vip-2') }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
|
||||
context ':any group' do
|
||||
before { create(:trading_fee, market_id: :btcusd, group: :any) }
|
||||
subject { build(:trading_fee, market_id: :btceth, group: :any) }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
context 'same market' do
|
||||
before { create(:trading_fee, market_id: :btcusd, group: 'vip-1') }
|
||||
|
||||
context 'same group' do
|
||||
subject { build(:trading_fee, market_id: :btcusd, group: 'vip-1') }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
|
||||
context 'different group' do
|
||||
subject { build(:trading_fee, market_id: :btcusd, group: 'vip-2') }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
|
||||
context ':any group' do
|
||||
before { create(:trading_fee, market_id: :btcusd, group: :any) }
|
||||
subject { build(:trading_fee, market_id: :btcusd, group: :any) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context ':any market' do
|
||||
before { create(:trading_fee, group: 'vip-1') }
|
||||
|
||||
context 'same group' do
|
||||
subject { build(:trading_fee, group: 'vip-1') }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
|
||||
context 'different group' do
|
||||
subject { build(:trading_fee, group: 'vip-2') }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
|
||||
context ':any group' do
|
||||
before { create(:trading_fee, group: :any) }
|
||||
subject { build(:trading_fee, group: :any) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'maker, taker numericality' do
|
||||
context 'non decimal maker/taker' do
|
||||
subject { build(:trading_fee, maker: '1', taker: '1') }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
|
||||
context 'valid trading_fee' do
|
||||
subject { build(:trading_fee, maker: 0.1, taker: 0.2) }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
context 'market_id presence' do
|
||||
context 'nil group' do
|
||||
subject { build(:trading_fee, market_id: nil) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
|
||||
context 'empty string group' do
|
||||
subject { build(:trading_fee, market_id: '') }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'market_id inclusion in' do
|
||||
context 'invalid market_id' do
|
||||
subject { build(:trading_fee, market_id: :ethusd) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
|
||||
context 'valid trading_fee' do
|
||||
subject { build(:trading_fee, market_id: :btcusd) }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe TradingFee, 'Class Methods' do
|
||||
before(:each) { TradingFee.delete_all }
|
||||
|
||||
context '#for' do
|
||||
let!(:member) { create(:member) }
|
||||
|
||||
context 'get trading_fee with marker_id and group' do
|
||||
before do
|
||||
create(:trading_fee, market_id: :btcusd, group: 'vip-0')
|
||||
create(:trading_fee, market_id: :any, group: 'vip-0')
|
||||
create(:trading_fee, market_id: :btcusd, group: :any)
|
||||
create(:trading_fee, market_id: :any, group: :any)
|
||||
end
|
||||
|
||||
let(:order) { Order.new(member: member, market_id: :btcusd) }
|
||||
subject { TradingFee.for(group: order.member.group, market_id: order.market_id) }
|
||||
|
||||
it do
|
||||
expect(subject).to be_truthy
|
||||
expect(subject.market_id).to eq('btcusd')
|
||||
expect(subject.group).to eq('vip-0')
|
||||
end
|
||||
end
|
||||
|
||||
context 'get trading_fee with group' do
|
||||
before do
|
||||
create(:trading_fee, market_id: :any, group: 'vip-1')
|
||||
create(:trading_fee, market_id: :btcusd, group: :any)
|
||||
create(:trading_fee, market_id: :any, group: :any)
|
||||
end
|
||||
|
||||
let(:order) { Order.new(member: member, market_id: :btcusd) }
|
||||
subject { TradingFee.for(group: order.member.group, market_id: order.market_id) }
|
||||
|
||||
it do
|
||||
expect(subject).to be_truthy
|
||||
expect(subject.market_id).to eq('btcusd')
|
||||
expect(subject.group).to eq('any')
|
||||
end
|
||||
end
|
||||
|
||||
context 'get trading_fee with market_id' do
|
||||
before do
|
||||
create(:trading_fee, market_id: :any, group: 'vip-0')
|
||||
create(:trading_fee, market_id: :btcusd, group: :any)
|
||||
create(:trading_fee, market_id: :any, group: :any)
|
||||
end
|
||||
|
||||
let(:order) { Order.new(member: member, market_id: :btceth) }
|
||||
subject { TradingFee.for(group: order.member.group, market_id: order.market_id) }
|
||||
|
||||
it do
|
||||
expect(subject).to be_truthy
|
||||
expect(subject.market_id).to eq('any')
|
||||
expect(subject.group).to eq('vip-0')
|
||||
end
|
||||
end
|
||||
|
||||
context 'get default trading_fee' do
|
||||
before do
|
||||
create(:trading_fee, market_id: :any, group: 'vip-1')
|
||||
create(:trading_fee, market_id: :btcusd, group: :any)
|
||||
create(:trading_fee, market_id: :any, group: :any)
|
||||
end
|
||||
|
||||
let(:order) { Order.new(member: member, market_id: :btceth) }
|
||||
subject { TradingFee.for(group: order.member.group, market_id: order.market_id) }
|
||||
|
||||
it do
|
||||
expect(subject).to be_truthy
|
||||
expect(subject.market_id).to eq('any')
|
||||
expect(subject.group).to eq('any')
|
||||
end
|
||||
end
|
||||
|
||||
context 'get default trading_fee (doesnt create it)' do
|
||||
let(:order) { Order.new(member: member, market_id: :btceth) }
|
||||
subject { TradingFee.for(group: order.member.group, market_id: order.market_id) }
|
||||
|
||||
it do
|
||||
expect(subject).to be_truthy
|
||||
expect(subject.market_id).to eq('any')
|
||||
expect(subject.group).to eq('any')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
250
spec/models/transfer_spec.rb
Normal file
250
spec/models/transfer_spec.rb
Normal file
@@ -0,0 +1,250 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Transfer do
|
||||
let(:currency_btc) { Currency.find(:btc) }
|
||||
let(:currency_eth) { Currency.find(:eth) }
|
||||
let(:currency_usd) { Currency.find(:usd) }
|
||||
|
||||
context 'validations' do
|
||||
subject { build(:transfer) }
|
||||
|
||||
describe 'key' do
|
||||
it 'uniqueness' do
|
||||
existing_transfer = create(:transfer)
|
||||
subject.key = existing_transfer.key
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject).to include_ar_error(:key, /has already been taken/)
|
||||
end
|
||||
|
||||
it 'presence' do
|
||||
subject.key = nil
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject).to include_ar_error(:key, /can't be blank/)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'category' do
|
||||
it 'presence' do
|
||||
subject.category = nil
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject).to include_ar_error(:category, /can't be blank/)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'accounting equation' do
|
||||
context 'single asset operation' do
|
||||
subject { build(:transfer, assets: build_list(:asset, 5)) }
|
||||
|
||||
it 'invalidates transfer' do
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject).to include_ar_error(:base, /invalidates accounting equation/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'different operations with invalid accounting sum' do
|
||||
subject do
|
||||
build(:transfer,
|
||||
assets: assets,
|
||||
liabilities: liabilities,
|
||||
revenues: revenues,
|
||||
expenses: expenses)
|
||||
end
|
||||
|
||||
context 'with single currency' do
|
||||
let(:assets) { [build(:asset, credit: 1, currency: currency_btc)] }
|
||||
let(:liabilities) { [build(:liability, :with_member, credit: 5, currency: currency_btc)] }
|
||||
let(:revenues) { [build(:revenue, credit: 5, currency: currency_btc)] }
|
||||
let(:expenses) { [build(:expense, credit: 1, currency: currency_btc)] }
|
||||
|
||||
it 'invalidates transfer' do
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject).to include_ar_error(:base, /invalidates accounting equation/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different currencies' do
|
||||
let(:assets) { [build(:asset, credit: 1, currency: currency_btc)] }
|
||||
let(:liabilities) { [build(:liability, :with_member, credit: 5, currency: currency_eth)] }
|
||||
let(:revenues) { [build(:revenue, credit: 5, currency: currency_usd)] }
|
||||
let(:expenses) { [build(:expense, credit: 1, currency: currency_btc)] }
|
||||
|
||||
it 'invalidates transfer' do
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject).to include_ar_error(:base, /invalidates accounting equation/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'multiple operations per operation type' do
|
||||
# assets - liabilities = revenues - expenses
|
||||
#
|
||||
# BTC:
|
||||
# (10 + 15) - (9 + 12) = (3 + 5) - (1 + 3)
|
||||
# 25 - 21 = 8 - 4
|
||||
# BTC accounting is correct.
|
||||
let(:asset1) { build(:asset, credit: 10, currency: currency_btc) }
|
||||
let(:asset2) { build(:asset, credit: 15, currency: currency_btc) }
|
||||
|
||||
let(:liability1) { build(:liability, :with_member, credit: 9, currency: currency_btc) }
|
||||
let(:liability2) { build(:liability, :with_member, credit: 12, currency: currency_btc) }
|
||||
|
||||
let(:revenue1) { build(:revenue, credit: 3, currency: currency_btc) }
|
||||
let(:revenue2) { build(:revenue, credit: 5, currency: currency_btc) }
|
||||
|
||||
let(:expense1) { build(:expense, credit: 1, currency: currency_btc) }
|
||||
let(:expense2) { build(:expense, credit: 3, currency: currency_btc) }
|
||||
|
||||
# assets - liabilities = revenues - expenses
|
||||
#
|
||||
# USD:
|
||||
# (90 + 25) - (88 + 25) = (4 + 2) - (2 + 1)
|
||||
# 115 - 113 = 6 - 3
|
||||
# USD accounting is broken.
|
||||
let(:asset3) { build(:asset, credit: 90, currency: currency_usd) }
|
||||
let(:asset4) { build(:asset, credit: 25, currency: currency_usd) }
|
||||
|
||||
let(:liability3) { build(:liability, :with_member, credit: 88, currency: currency_usd) }
|
||||
let(:liability4) { build(:liability, :with_member, credit: 25, currency: currency_usd) }
|
||||
|
||||
let(:revenue3) { build(:revenue, credit: 4, currency: currency_usd) }
|
||||
let(:revenue4) { build(:revenue, credit: 2, currency: currency_usd) }
|
||||
|
||||
let(:expense3) { build(:expense, credit: 2, currency: currency_usd) }
|
||||
let(:expense4) { build(:expense, credit: 1, currency: currency_usd) }
|
||||
|
||||
let(:assets) { [asset1, asset2, asset3, asset4] }
|
||||
let(:liabilities) { [liability1, liability2, liability3, liability4] }
|
||||
let(:revenues) { [revenue1, revenue2, revenue3, revenue4] }
|
||||
let(:expenses) { [expense1, expense2, expense3, expense4] }
|
||||
|
||||
it 'invalidates transfer' do
|
||||
expect(subject.valid?).to be_falsey
|
||||
expect(subject).to include_ar_error(:base, /invalidates accounting equation/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'valid accounting sum' do
|
||||
subject do
|
||||
build(:transfer,
|
||||
assets: assets,
|
||||
liabilities: liabilities,
|
||||
revenues: revenues,
|
||||
expenses: expenses)
|
||||
end
|
||||
context 'with single currency' do
|
||||
# assets - liabilities = revenues - expenses
|
||||
#
|
||||
# BTC:
|
||||
# (30 + 45 - 12) - (9 + 12 - 2) = (28 + 20 - 2) - (1 + 4 - 3)
|
||||
# 63 - 19 = 46 - 2
|
||||
# BTC accounting is correct.
|
||||
let(:member1) { create(:member, :level_3).tap { |m| m.get_account(currency_btc).plus_funds(50.0) } }
|
||||
|
||||
let(:asset1) { build(:asset, credit: 30, currency: currency_btc) }
|
||||
let(:asset2) { build(:asset, credit: 45, currency: currency_btc) }
|
||||
let(:asset3) { build(:asset, :debit, debit: 12, currency: currency_btc) }
|
||||
|
||||
let(:liability1) { build(:liability, :with_member, credit: 9, currency: currency_btc) }
|
||||
let(:liability2) { build(:liability, :with_member, credit: 12, currency: currency_btc) }
|
||||
let(:liability3) { build(:liability, :debit, :with_member, debit: 2, member: member1, currency: currency_btc) }
|
||||
|
||||
let(:revenue1) { build(:revenue, credit: 28, currency: currency_btc) }
|
||||
let(:revenue2) { build(:revenue, credit: 20, currency: currency_btc) }
|
||||
let(:revenue3) { build(:revenue, :debit, debit: 2, currency: currency_btc) }
|
||||
|
||||
let(:expense1) { build(:expense, credit: 1, currency: currency_btc) }
|
||||
let(:expense2) { build(:expense, credit: 4, currency: currency_btc) }
|
||||
let(:expense3) { build(:expense, :debit, debit: 3, currency: currency_btc) }
|
||||
|
||||
|
||||
let(:assets) { [asset1, asset2, asset3] }
|
||||
let(:liabilities) { [liability1, liability2, liability3] }
|
||||
let(:revenues) { [revenue1, revenue2, revenue3] }
|
||||
let(:expenses) { [expense1, expense2, expense3] }
|
||||
|
||||
it 'validates transfer' do
|
||||
expect(subject.save!).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'do_transfer!' do
|
||||
subject do
|
||||
Transfer.create!(attributes_for(:transfer,
|
||||
liabilities: liabilities,
|
||||
assets: assets,
|
||||
revenues: revenues,
|
||||
expenses: expenses))
|
||||
end
|
||||
let(:asset1) { build(:asset, credit: 9, currency: currency_btc) }
|
||||
let(:asset2) { build(:asset, :debit, debit: 6, currency: currency_btc) }
|
||||
let(:revenue1) { build(:revenue, credit: 12, currency: currency_btc) }
|
||||
let(:revenue2) { build(:revenue, :debit, debit: 3, currency: currency_btc) }
|
||||
let(:expense1) { build(:expense, credit: 8, currency: currency_btc) }
|
||||
let(:expense2) { build(:expense, :debit, debit: 2, currency: currency_btc) }
|
||||
let(:assets) { [asset1, asset2] }
|
||||
let(:revenues) { [revenue1, revenue2] }
|
||||
let(:expenses) { [expense1, expense2] }
|
||||
let(:liabilities) { [] }
|
||||
|
||||
it 'creates transfer' do
|
||||
expect {
|
||||
subject
|
||||
}.to change { Transfer.count }.by 1
|
||||
end
|
||||
|
||||
context 'update_legacy_balances' do
|
||||
context 'without liabilities' do
|
||||
it 'does not change legacy balances' do
|
||||
expect {
|
||||
subject
|
||||
}.not_to change { Member.all.map(&:accounts) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with liabilities' do
|
||||
let(:member1) { create(:member, :level_3).tap { |m| m.get_account(currency_btc).plus_funds(50.0) } }
|
||||
let(:member2) { create(:member, :level_3).tap { |m| m.get_account(currency_btc).plus_funds(50.0) } }
|
||||
let(:member3) { create(:member, :level_3).tap { |m| m.get_account(currency_btc).plus_funds(50.0) } }
|
||||
let(:credit) { build(:liability, credit: 9, member: member1, currency: currency_btc) }
|
||||
let(:debit1) { build(:liability, :debit, debit: 5, member: member2, currency: currency_btc) }
|
||||
let(:debit2) { build(:liability, :debit, debit: 4, member: member3, currency: currency_btc) }
|
||||
|
||||
let(:liabilities) { [credit, debit1, debit2] }
|
||||
|
||||
it 'increases balance for member1' do
|
||||
expect {
|
||||
subject
|
||||
}.to change { member1.accounts.find_by(currency: currency_btc).balance }.by(9)
|
||||
end
|
||||
|
||||
it 'decreases balance for member2' do
|
||||
expect {
|
||||
subject
|
||||
}.to change { member2.accounts.find_by(currency: currency_btc).balance }.by(-5)
|
||||
end
|
||||
|
||||
it 'decreases balance for member3' do
|
||||
expect {
|
||||
subject
|
||||
}.to change { member3.accounts.find_by(currency: currency_btc).balance }.by(-4)
|
||||
end
|
||||
|
||||
context 'legacy balance update raise error' do
|
||||
before do
|
||||
Account.any_instance.expects(:sub_funds).raises(Account::AccountError)
|
||||
end
|
||||
|
||||
it 'does not create transfer' do
|
||||
expect {
|
||||
subject rescue Account::AccountError; nil
|
||||
}.to_not change{ Transfer.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
10
spec/models/trigger_spec.rb
Normal file
10
spec/models/trigger_spec.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Trigger do
|
||||
let(:trigger){ create(:trigger) }
|
||||
|
||||
it do
|
||||
expect{trigger}.to_not raise_error
|
||||
end
|
||||
end
|
||||
74
spec/models/user_ability_spec.rb
Normal file
74
spec/models/user_ability_spec.rb
Normal file
@@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe UserAbility do
|
||||
|
||||
context 'abilities for member' do
|
||||
let(:member) { create(:member, role: 'member') }
|
||||
subject(:ability) { UserAbility.new(member) }
|
||||
|
||||
it { is_expected.to be_able_to(:manage, :all) }
|
||||
end
|
||||
|
||||
context 'abilities for superadmin' do
|
||||
let(:member) { create(:member, role: 'superadmin') }
|
||||
subject(:ability) { UserAbility.new(member) }
|
||||
|
||||
it { is_expected.to be_able_to(:manage, :all) }
|
||||
end
|
||||
|
||||
context 'abilities for admin' do
|
||||
let(:member) { create(:member, role: 'admin') }
|
||||
subject(:ability) { UserAbility.new(member) }
|
||||
|
||||
it { is_expected.to be_able_to(:manage, :all) }
|
||||
end
|
||||
|
||||
context 'abilities for compliance' do
|
||||
let(:member) { create(:member, role: 'compliance') }
|
||||
subject(:ability) { UserAbility.new(member) }
|
||||
|
||||
it { is_expected.to be_able_to(:manage, :all) }
|
||||
end
|
||||
|
||||
context 'abilities for support' do
|
||||
let(:member) { create(:member, role: 'support') }
|
||||
subject(:ability) { UserAbility.new(member) }
|
||||
|
||||
it { is_expected.to be_able_to(:manage, :all) }
|
||||
end
|
||||
|
||||
context 'abilities for technical' do
|
||||
let(:member) { create(:member, role: 'technical') }
|
||||
subject(:ability) { UserAbility.new(member) }
|
||||
|
||||
it { is_expected.to be_able_to(:manage, :all) }
|
||||
end
|
||||
|
||||
context 'abilities for accountant' do
|
||||
let(:member) { create(:member, role: 'accountant') }
|
||||
subject(:ability) { UserAbility.new(member) }
|
||||
|
||||
it { is_expected.to be_able_to(:manage, :all) }
|
||||
end
|
||||
|
||||
context 'abilities for broker' do
|
||||
let(:member) { create(:member, role: 'broker') }
|
||||
subject(:ability) { UserAbility.new(member) }
|
||||
|
||||
it { is_expected.to be_able_to(:manage, :all) }
|
||||
end
|
||||
|
||||
context 'abilities for trader' do
|
||||
let(:member) { create(:member, role: 'trader') }
|
||||
subject(:ability) { UserAbility.new(member) }
|
||||
|
||||
it { is_expected.to be_able_to(:manage, :all) }
|
||||
end
|
||||
|
||||
context 'abilities for maker' do
|
||||
let(:member) { create(:member, role: 'maker') }
|
||||
subject(:ability) { UserAbility.new(member) }
|
||||
|
||||
it { is_expected.to be_able_to(:manage, :all) }
|
||||
end
|
||||
end
|
||||
71
spec/models/wallet_spec.rb
Normal file
71
spec/models/wallet_spec.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Wallet do
|
||||
context 'validations' do
|
||||
|
||||
subject { build(:wallet, :eth_cold) }
|
||||
|
||||
it 'checks valid record' do
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'validates presence of address' do
|
||||
subject.address = nil
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ['Address can\'t be blank']
|
||||
end
|
||||
|
||||
it 'validates presence of name' do
|
||||
subject.name = nil
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ['Name can\'t be blank']
|
||||
end
|
||||
|
||||
it 'validates inclusion of status' do
|
||||
subject.status = 'abc'
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ['Status is not included in the list']
|
||||
end
|
||||
|
||||
it 'validates inclusion of kind' do
|
||||
subject.kind = 'abc'
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ['Kind is not included in the list']
|
||||
end
|
||||
|
||||
it 'validates name uniqueness' do
|
||||
subject.name = Wallet.first.name
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ['Name has already been taken']
|
||||
end
|
||||
|
||||
it 'saves settings in encrypted column' do
|
||||
subject.save
|
||||
expect {
|
||||
subject.uri = 'http://geth:8545/'
|
||||
subject.save
|
||||
}.to change { subject.settings_encrypted }
|
||||
end
|
||||
|
||||
it 'does not update settings_encrypted before model is saved' do
|
||||
subject.save
|
||||
expect {
|
||||
subject.uri = 'http://geth:8545/'
|
||||
}.not_to change { subject.settings_encrypted }
|
||||
end
|
||||
|
||||
it 'updates setting fields' do
|
||||
expect {
|
||||
subject.uri = 'http://geth:8545/'
|
||||
}.to change { subject.settings['uri'] }.to 'http://geth:8545/'
|
||||
end
|
||||
|
||||
it 'long encrypted secret' do
|
||||
expect {
|
||||
subject.secret = Faker::String.random(1024)
|
||||
subject.save!
|
||||
}.to raise_error ActiveRecord::ValueTooLong
|
||||
end
|
||||
end
|
||||
end
|
||||
41
spec/models/whitelisted_smart_contract_spec.rb
Normal file
41
spec/models/whitelisted_smart_contract_spec.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe WhitelistedSmartContract, 'Validations' 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) }
|
||||
|
||||
context 'whitelisted addresses model' do
|
||||
subject { build(:whitelisted_smart_contract, :address_5) }
|
||||
|
||||
it 'checks whitelisted address valid record' do
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'validates whitelisted address presence of address' do
|
||||
subject.address = nil
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ['Address can\'t be blank']
|
||||
end
|
||||
|
||||
it 'validates whitelisted address presence of blockchain_key' do
|
||||
subject.blockchain_key = nil
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ["Blockchain key can't be blank", "Blockchain key is not included in the list"]
|
||||
end
|
||||
|
||||
it 'validates whitelisted address inclusion of state' do
|
||||
subject.state = 'abc'
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ['State is not included in the list']
|
||||
end
|
||||
|
||||
it 'validates whitelisted address address uniqueness' do
|
||||
subject.address = WhitelistedSmartContract.first.address
|
||||
expect(subject).to_not be_valid
|
||||
expect(subject.errors.full_messages).to eq ['Address has already been taken']
|
||||
end
|
||||
end
|
||||
end
|
||||
137
spec/models/withdraw_limit_spec.rb
Normal file
137
spec/models/withdraw_limit_spec.rb
Normal file
@@ -0,0 +1,137 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe WithdrawLimit, 'Validations' do
|
||||
before(:each) { WithdrawLimit.delete_all }
|
||||
|
||||
context 'group presence' do
|
||||
context 'nil group' do
|
||||
subject { build(:withdraw_limit, group: nil) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
|
||||
context 'empty string group' do
|
||||
subject { build(:withdraw_limit, group: '') }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'group uniqueness' do
|
||||
context 'same kyc_level' do
|
||||
before { create(:withdraw_limit, kyc_level: 1, group: 'vip-1') }
|
||||
|
||||
context 'same group' do
|
||||
subject { build(:withdraw_limit, kyc_level: 1, group: 'vip-1') }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
|
||||
context 'different group' do
|
||||
subject { build(:withdraw_limit, kyc_level: 1, group: 'vip-2') }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
|
||||
context ':any group' do
|
||||
before { create(:withdraw_limit, kyc_level: 1, group: :any) }
|
||||
subject { build(:withdraw_limit, kyc_level: 1, group: :any) }
|
||||
it { expect(subject.valid?).to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'limit_24_hour, limit_1_month numericality' do
|
||||
context 'non decimal limit_24_hour/limit_1_month' do
|
||||
subject { build(:withdraw_limit, limit_24_hour: '1', limit_1_month: '1') }
|
||||
it do
|
||||
expect(subject.valid?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'valid withdraw_limit' do
|
||||
subject { build(:withdraw_limit, limit_24_hour: 0.1, limit_1_month: 0.2) }
|
||||
it { expect(subject.valid?).to be_truthy }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe WithdrawLimit, 'Class Methods' do
|
||||
before(:each) { WithdrawLimit.delete_all }
|
||||
|
||||
context '#for' do
|
||||
let!(:member) { create(:member) }
|
||||
|
||||
|
||||
context 'get withdraw_limit with kyc_level and group' do
|
||||
let!(:member) { create(:member, level: 1) }
|
||||
before do
|
||||
create(:withdraw_limit, kyc_level: 1, group: 'vip-0')
|
||||
create(:withdraw_limit, group: 'vip-0')
|
||||
create(:withdraw_limit, kyc_level: 2, group: :any)
|
||||
create(:withdraw_limit, kyc_level: 3, group: :any)
|
||||
end
|
||||
|
||||
let(:withdraw) { Withdraw.new(member: member) }
|
||||
subject { WithdrawLimit.for(kyc_level: withdraw.member.level, group: withdraw.member.group) }
|
||||
|
||||
it do
|
||||
expect(subject).to be_truthy
|
||||
expect(subject.group).to eq('vip-0')
|
||||
expect(subject.kyc_level).to eq('1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'get withdraw_limit with group' do
|
||||
before do
|
||||
create(:withdraw_limit, group: 'vip-0')
|
||||
create(:withdraw_limit, group: 'vip-1')
|
||||
create(:withdraw_limit, group: :any)
|
||||
end
|
||||
|
||||
let(:withdraw) { Withdraw.new(member: member) }
|
||||
subject { WithdrawLimit.for(kyc_level: withdraw.member.level, group: withdraw.member.group) }
|
||||
|
||||
it do
|
||||
expect(subject).to be_truthy
|
||||
expect(subject.group).to eq('vip-0')
|
||||
end
|
||||
end
|
||||
|
||||
context 'get withdraw_limit with kyc_level' do
|
||||
before do
|
||||
create(:withdraw_limit, kyc_level: 1)
|
||||
end
|
||||
|
||||
let(:withdraw) { Withdraw.new(member: member) }
|
||||
subject { WithdrawLimit.for(kyc_level: withdraw.member.level, group: withdraw.member.group) }
|
||||
|
||||
it do
|
||||
expect(subject).to be_truthy
|
||||
expect(subject.group).to eq('any')
|
||||
end
|
||||
end
|
||||
|
||||
context 'get default withdraw_limit' do
|
||||
before do
|
||||
create(:withdraw_limit, group: 'vip-1')
|
||||
create(:withdraw_limit, group: :any)
|
||||
end
|
||||
|
||||
let(:withdraw) { Withdraw.new(member: member) }
|
||||
subject { WithdrawLimit.for(kyc_level: withdraw.member.level, group: withdraw.member.group) }
|
||||
|
||||
it do
|
||||
expect(subject).to be_truthy
|
||||
expect(subject.group).to eq('any')
|
||||
end
|
||||
end
|
||||
|
||||
context 'get default withdraw_limit (doesnt create it)' do
|
||||
let(:withdraw) { Withdraw.new(member: member) }
|
||||
subject { WithdrawLimit.for(kyc_level: withdraw.member.level, group: withdraw.member.group) }
|
||||
|
||||
it do
|
||||
expect(subject).to be_truthy
|
||||
expect(subject.group).to eq('any')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
634
spec/models/withdraw_spec.rb
Normal file
634
spec/models/withdraw_spec.rb
Normal file
@@ -0,0 +1,634 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe Withdraw do
|
||||
context 'aasm_state' do
|
||||
subject { create(:usd_withdraw, :with_deposit_liability, sum: 1000) }
|
||||
|
||||
before do
|
||||
subject.stubs(:send_withdraw_confirm_email)
|
||||
end
|
||||
|
||||
it 'initializes with state :prepared' do
|
||||
expect(subject.prepared?).to be true
|
||||
end
|
||||
|
||||
it 'transitions to :rejected after calling #reject!' do
|
||||
subject.accept!
|
||||
subject.reject!
|
||||
|
||||
expect(subject.rejected?).to be true
|
||||
end
|
||||
|
||||
context :accept do
|
||||
it 'transitions to :submitted after calling #accept!' do
|
||||
subject.accept!
|
||||
expect(subject.accepted?).to be true
|
||||
expect(subject.sum).to eq subject.account.locked
|
||||
end
|
||||
|
||||
context :record_submit_operations! do
|
||||
it 'creates two liability operations' do
|
||||
expect{ subject.accept! }.to change{ Operations::Liability.count }.by(2)
|
||||
end
|
||||
|
||||
it 'doesn\'t create asset operations' do
|
||||
expect{ subject.accept! }.to_not change{ Operations::Asset.count }
|
||||
end
|
||||
|
||||
it 'debits main liabilities for member' do
|
||||
expect{ subject.accept! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :main)
|
||||
}.by(-subject.sum)
|
||||
end
|
||||
|
||||
it 'credits locked liabilities for member' do
|
||||
expect{ subject.accept! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
||||
}.by(subject.sum)
|
||||
end
|
||||
|
||||
it 'updates both legacy and operations based member balance' do
|
||||
subject.accept!
|
||||
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context :process do
|
||||
before { subject.accept! }
|
||||
before { subject.accept! }
|
||||
|
||||
it 'transitions to :processing after calling #process! when withdrawing fiat currency' do
|
||||
subject.currency.stubs(:coin?).returns(false)
|
||||
|
||||
subject.process!
|
||||
|
||||
expect(subject.processing?).to be true
|
||||
end
|
||||
|
||||
it 'transitions to :failed after calling #fail! when withdrawing fiat currency' do
|
||||
subject.currency.stubs(:coin?).returns(false)
|
||||
|
||||
subject.process!
|
||||
|
||||
expect { subject.fail! }.to_not change { subject.account.amount }
|
||||
|
||||
expect(subject.failed?).to be true
|
||||
end
|
||||
|
||||
it 'transitions to :processing after calling #process!' do
|
||||
subject.expects(:send_coins!)
|
||||
|
||||
subject.process!
|
||||
|
||||
expect(subject.processing?).to be true
|
||||
end
|
||||
|
||||
it 'transitions to :processing after calling #process from :skipped' do
|
||||
subject.process!
|
||||
expect(subject.processing?).to be true
|
||||
|
||||
subject.skip!
|
||||
expect(subject.skipped?).to be true
|
||||
|
||||
subject.process!
|
||||
expect(subject.processing?).to be true
|
||||
end
|
||||
|
||||
it 'transitions to :errored after calling #err from :processing' do
|
||||
subject.process!
|
||||
expect(subject.processing?).to be true
|
||||
|
||||
expect { subject.err! StandardError.new }.to_not change { subject.account.amount }
|
||||
expect(subject.errored?).to be true
|
||||
|
||||
subject.process!
|
||||
expect(subject.processing?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context :cancel do
|
||||
it 'transitions to :canceled after calling #cancel!' do
|
||||
subject.cancel!
|
||||
|
||||
expect(subject.canceled?).to be true
|
||||
end
|
||||
|
||||
it 'transitions from :submitted to :canceled after calling #cancel!' do
|
||||
subject.accept!
|
||||
subject.cancel!
|
||||
|
||||
expect(subject.canceled?).to be true
|
||||
end
|
||||
|
||||
it 'transitions from :accepted to :canceled after calling #cancel!' do
|
||||
subject.accept!
|
||||
subject.accept!
|
||||
subject.cancel!
|
||||
|
||||
expect(subject.canceled?).to be true
|
||||
end
|
||||
|
||||
context :record_cancel_operations do
|
||||
before do
|
||||
subject.accept!
|
||||
subject.accept!
|
||||
end
|
||||
it 'creates two liability operations' do
|
||||
expect{ subject.cancel! }.to change{ Operations::Liability.count }.by(2)
|
||||
end
|
||||
|
||||
it 'doesn\'t create asset operations' do
|
||||
expect{ subject.cancel! }.to_not change{ Operations::Asset.count }
|
||||
end
|
||||
|
||||
it 'credits main liabilities for member' do
|
||||
expect{ subject.cancel! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :main)
|
||||
}.by(subject.sum)
|
||||
end
|
||||
|
||||
it 'debits locked liabilities for member' do
|
||||
expect{ subject.cancel! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
||||
}.by(-subject.sum)
|
||||
end
|
||||
|
||||
it 'updates both legacy and operations based member balance' do
|
||||
subject.cancel!
|
||||
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context :skip do
|
||||
before do
|
||||
subject.accept!
|
||||
subject.accept!
|
||||
subject.process!
|
||||
end
|
||||
|
||||
it 'transitions from :accept to :skipped after calling #process' do
|
||||
subject.skip!
|
||||
|
||||
expect(subject.skipped?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context :reject do
|
||||
before do
|
||||
subject.accept!
|
||||
end
|
||||
|
||||
it 'transitions from :submitted to :rejected after calling #reject!' do
|
||||
subject.reject!
|
||||
expect(subject.rejected?).to be true
|
||||
end
|
||||
|
||||
it 'transitions from :accepted to :rejected after calling #reject!' do
|
||||
subject.accept!
|
||||
subject.reject!
|
||||
|
||||
expect(subject.rejected?).to be true
|
||||
end
|
||||
|
||||
context 'from to_rejected' do
|
||||
before do
|
||||
subject.update(aasm_state: :to_reject)
|
||||
end
|
||||
|
||||
it 'transitions from :accepted to :rejected after calling #reject!' do
|
||||
subject.reject!
|
||||
|
||||
expect(subject.rejected?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context :record_cancel_operations do
|
||||
|
||||
it 'creates two liability operations' do
|
||||
expect{ subject.reject! }.to change{ Operations::Liability.count }.by(2)
|
||||
end
|
||||
|
||||
it 'doesn\'t create asset operations' do
|
||||
expect{ subject.reject! }.to_not change{ Operations::Asset.count }
|
||||
end
|
||||
|
||||
it 'credits main liabilities for member' do
|
||||
expect{ subject.reject! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :main)
|
||||
}.by(subject.sum)
|
||||
end
|
||||
|
||||
it 'debits locked liabilities for member' do
|
||||
expect{ subject.reject! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
||||
}.by(-subject.sum)
|
||||
end
|
||||
|
||||
it 'updates both legacy and operations based member balance' do
|
||||
subject.reject!
|
||||
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context :success do
|
||||
|
||||
before do
|
||||
subject.accept!
|
||||
subject.accept!
|
||||
subject.process!
|
||||
subject.dispatch!
|
||||
end
|
||||
|
||||
it 'transitions from :confirming to :success after calling #success!' do
|
||||
subject.success!
|
||||
|
||||
expect(subject.succeed?).to be true
|
||||
end
|
||||
|
||||
context :record_complete_operations do
|
||||
|
||||
it 'creates single liability operation' do
|
||||
expect{ subject.success! }.to change{ Operations::Liability.count }.by(1)
|
||||
end
|
||||
|
||||
it 'creates asset operation' do
|
||||
expect{ subject.success! }.to change{ Operations::Asset.count }.by(1)
|
||||
end
|
||||
|
||||
it 'doesn\'t change main liability balance for member' do
|
||||
expect{ subject.success! }.to_not change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :main)
|
||||
}
|
||||
end
|
||||
|
||||
it 'debits locked liabilities for member' do
|
||||
expect{ subject.success! }.to change {
|
||||
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
||||
}.by(-subject.sum)
|
||||
end
|
||||
|
||||
it 'updates both legacy and operations based member balance' do
|
||||
subject.success!
|
||||
|
||||
%i[main locked].each do |kind|
|
||||
expect(
|
||||
subject.member.balance_for(currency: subject.currency, kind: kind)
|
||||
).to eq(
|
||||
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'credits revenues' do
|
||||
expect{ subject.success! }.to change {
|
||||
Operations::Revenue.balance(currency: subject.currency)
|
||||
}.by(subject.fee)
|
||||
end
|
||||
|
||||
it 'creates revenue operation from member' do
|
||||
expect{ subject.success! }.to change {
|
||||
Operations::Revenue.where(member: subject.member).count
|
||||
}.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
context :load do
|
||||
let(:txid) { 'a738cb8411e2141f3de43c5f3e7a3aabe71c099bb91d296ded84f0daf29d881c' }
|
||||
|
||||
subject { create(:btc_withdraw, :with_deposit_liability) }
|
||||
|
||||
before { subject.accept! }
|
||||
|
||||
it 'doesn\'t change state after calling #load! when withdrawing coin currency' do
|
||||
subject.load!
|
||||
expect(subject.accepted?).to be true
|
||||
end
|
||||
|
||||
it 'transitions to :confirming after calling #load! when withdrawing coin currency' do
|
||||
BlockchainService.any_instance.expects(:fetch_transaction).once.returns(Peatio::Transaction.new)
|
||||
subject.update(txid: txid)
|
||||
subject.load!
|
||||
expect(subject.confirming?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context :load do
|
||||
let(:txid) { 'a738cb8411e2141f3de43c5f3e7a3aabe71c099bb91d296ded84f0daf29d881c' }
|
||||
|
||||
subject { create(:btc_withdraw, :with_deposit_liability) }
|
||||
|
||||
before { subject.accept! }
|
||||
before { subject.accept! }
|
||||
|
||||
it 'doesn\'t change state after calling #load! when withdrawing coin currency' do
|
||||
subject.load!
|
||||
expect(subject.accepted?).to be true
|
||||
end
|
||||
|
||||
it 'transitions to :confirming after calling #load! when withdrawing coin currency' do
|
||||
BlockchainService.any_instance.expects(:fetch_transaction).once.returns(Peatio::Transaction.new)
|
||||
subject.update(txid: txid)
|
||||
subject.load!
|
||||
expect(subject.confirming?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context :fail do
|
||||
subject { create(:btc_withdraw, :with_deposit_liability) }
|
||||
|
||||
before { subject.accept! }
|
||||
before { subject.accept! }
|
||||
|
||||
context 'from errored' do
|
||||
before do
|
||||
subject.update!(aasm_state: :processing)
|
||||
subject.err!(Peatio::Wallet::ClientError.new('Something wrong with request'))
|
||||
end
|
||||
|
||||
it do
|
||||
subject.fail!
|
||||
expect(subject.failed?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'from skipped' do
|
||||
before do
|
||||
subject.update!(aasm_state: :skipped)
|
||||
end
|
||||
|
||||
it do
|
||||
subject.fail!
|
||||
expect(subject.failed?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with archived beneficiary' do
|
||||
let(:member) { create(:member) }
|
||||
let(:address) { Faker::Blockchain::Ethereum.address }
|
||||
let(:coin) { Currency.find(:btc) }
|
||||
|
||||
subject { create(:btc_withdraw, :with_deposit_liability, member: member, rid: address, beneficiary: beneficiary) }
|
||||
|
||||
before { subject.accept! }
|
||||
before { subject.accept! }
|
||||
|
||||
let!(:beneficiary) { create(:beneficiary,
|
||||
member: member,
|
||||
currency: coin,
|
||||
state: :active,
|
||||
data: generate(:coin_beneficiary_data).merge(address: address)) }
|
||||
|
||||
before do
|
||||
subject.update!(aasm_state: :processing)
|
||||
subject.err!(Peatio::Wallet::ClientError.new('Something wrong with request'))
|
||||
beneficiary.update!(state: :archived)
|
||||
end
|
||||
it do
|
||||
subject.fail!
|
||||
expect(subject.failed?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'fee is set to fixed value of 10' do
|
||||
let(:withdraw) { create(:usd_withdraw, :with_deposit_liability, sum: 200) }
|
||||
before { Currency.any_instance.expects(:withdraw_fee).once.returns(10) }
|
||||
it 'computes fee' do
|
||||
expect(withdraw.fee).to eql 10.to_d
|
||||
expect(withdraw.amount).to eql 190.to_d
|
||||
end
|
||||
end
|
||||
|
||||
context 'fee exceeds amount' do
|
||||
let(:member) { create(:member) }
|
||||
let!(:account) { member.get_account(:usd).tap { |x| x.update!(balance: 200.0.to_d) } }
|
||||
let(:withdraw) { build(:usd_withdraw, sum: 200, member: member) }
|
||||
before { Currency.any_instance.expects(:withdraw_fee).once.returns(200) }
|
||||
it 'fails validation' do
|
||||
expect(withdraw.save).to eq false
|
||||
expect(withdraw.errors[:amount]).to match(["must be greater than 0.0"])
|
||||
end
|
||||
end
|
||||
|
||||
it 'automatically generates TID if it is blank' do
|
||||
expect(create(:btc_withdraw, :with_deposit_liability).tid).not_to be_blank
|
||||
end
|
||||
|
||||
it 'doesn\'t generate TID if it is not blank' do
|
||||
expect(create(:btc_withdraw, :with_deposit_liability, tid: 'TID1234567890xyz').tid).to eq 'TID1234567890xyz'
|
||||
end
|
||||
|
||||
it 'validates uniqueness of TID' do
|
||||
record1 = create(:btc_withdraw, :with_deposit_liability)
|
||||
record2 = build(:btc_withdraw, tid: record1.tid, member: record1.member)
|
||||
record2.save
|
||||
expect(record2.errors[:tid]).to match(["has already been taken"])
|
||||
end
|
||||
|
||||
it 'uppercases TID' do
|
||||
record = create(:btc_withdraw, :with_deposit_liability)
|
||||
expect(record.tid).to eq record.tid.upcase
|
||||
end
|
||||
|
||||
context 'using beneficiary' do
|
||||
context 'fiat' do
|
||||
let(:withdraw) do
|
||||
create(:usd_withdraw,
|
||||
:with_beneficiary,
|
||||
:with_deposit_liability,
|
||||
sum: 200)
|
||||
end
|
||||
|
||||
it 'automatically sets rid from beneficiary' do
|
||||
expect(withdraw.rid).to eq withdraw.beneficiary.rid
|
||||
end
|
||||
end
|
||||
|
||||
context 'crypto' do
|
||||
let(:withdraw) do
|
||||
create(:btc_withdraw,
|
||||
:with_beneficiary,
|
||||
:with_deposit_liability,
|
||||
sum: 2)
|
||||
end
|
||||
|
||||
it 'automatically sets rid from beneficiary' do
|
||||
expect(withdraw.rid).to eq withdraw.beneficiary.rid
|
||||
end
|
||||
end
|
||||
|
||||
context 'non-active beneficiary' do
|
||||
let(:currency) { Currency.all.sample }
|
||||
let(:beneficiary) { create(:beneficiary, state: :pending, currency: currency) }
|
||||
|
||||
# Create deposit before withdraw for valid accounting cause withdraw
|
||||
# build callback doesn't trigger deposit creation.
|
||||
let!(:deposit) do
|
||||
create(:deposit_usd, member: beneficiary.member, amount: 12)
|
||||
.accept!
|
||||
end
|
||||
|
||||
let(:withdraw) do
|
||||
build(:usd_withdraw,
|
||||
:with_deposit_liability,
|
||||
beneficiary: beneficiary,
|
||||
sum: 10,
|
||||
member: beneficiary.member)
|
||||
end
|
||||
|
||||
it 'automatically sets rid from beneficiary' do
|
||||
expect(withdraw.valid?).to be_falsey
|
||||
expect(withdraw.errors[:beneficiary]).to include('not active')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'validate min withdrawal sum' do
|
||||
let(:member) { create(:member) }
|
||||
let!(:account) { member.get_account(:btc).tap { |x| x.update!(balance: 1.0.to_d) } }
|
||||
subject { build(:btc_withdraw, sum: 0.1, member: member) }
|
||||
|
||||
before do
|
||||
Currency.find('btc').update(min_withdraw_amount: 0.5.to_d)
|
||||
end
|
||||
|
||||
it { expect(subject).not_to be_valid }
|
||||
|
||||
it do
|
||||
subject.save
|
||||
expect(subject.errors[:sum]).to match(["must be greater than or equal to 0.5"])
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'validate note length' do
|
||||
let(:member) { create(:member) }
|
||||
let!(:account) { member.get_account(:btc).tap { |x| x.update!(balance: 1.0.to_d) } }
|
||||
let(:address) { 'bitcoincash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h' }
|
||||
|
||||
let :record do
|
||||
Withdraws::Coin.new \
|
||||
currency: Currency.find(:btc),
|
||||
member: member,
|
||||
rid: address,
|
||||
sum: 1.0.to_d,
|
||||
note: note
|
||||
end
|
||||
|
||||
context 'valid note' do
|
||||
let(:note) { 'TEST' }
|
||||
|
||||
it do
|
||||
expect(record.save).to eq true
|
||||
expect(record.note).to eq 'TEST'
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid note' do
|
||||
let(:note) { (0...257).map { (65 + rand(26)).chr }.join }
|
||||
|
||||
it do
|
||||
expect(record.save).to eq false
|
||||
expect(record.errors.full_messages).to include 'Note is too long (maximum is 256 characters)'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'validates sum precision' do
|
||||
let(:currency) { Currency.find(:usd) }
|
||||
let(:member) { create(:member) }
|
||||
|
||||
# Create deposit before withdraw for valid accounting cause withdraw
|
||||
# build callback doesn't trigger deposit creation.
|
||||
let!(:deposit) do
|
||||
create(:deposit_usd, member: member, amount: 12)
|
||||
.accept!
|
||||
end
|
||||
|
||||
let :record do
|
||||
build(:usd_withdraw, :with_deposit_liability, :with_beneficiary, member: member, sum: 0.1234)
|
||||
end
|
||||
|
||||
it do
|
||||
expect(record.valid?).to be_falsey
|
||||
expect(record.errors[:amount]).to include("precision must be less than or equal to #{currency.precision}")
|
||||
expect(record.errors[:sum]).to include("precision must be less than or equal to #{currency.precision}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'verify_limits' do
|
||||
let!(:member) { create(:member, group: 'vip-1', level: 1) }
|
||||
let!(:withdraw_limit) { create(:withdraw_limit, group: 'vip-1', kyc_level: 1, limit_24_hour: 6, limit_1_month: 10) }
|
||||
let(:withdraw) { build(:btc_withdraw, :with_deposit_liability, member: member, sum: 0.5.to_d) }
|
||||
|
||||
before do
|
||||
Currency.any_instance.unstub(:price)
|
||||
Currency.find('btc').update!(price: 10)
|
||||
member.get_account(:btc).update!(balance: 1000)
|
||||
end
|
||||
|
||||
context 'enough limits' do
|
||||
it { expect(withdraw.verify_limits).to be_truthy }
|
||||
end
|
||||
|
||||
context 'Withdraw 24 hours limit exceeded' do
|
||||
it do
|
||||
withdraw.sum = 100
|
||||
expect(withdraw.verify_limits).to be_falsey
|
||||
end
|
||||
|
||||
it 'withdraw in different currency' do
|
||||
Currency.find('usd').update!(price: 1)
|
||||
withdraw.sum = 100
|
||||
expect(withdraw.verify_limits).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'Withdraw 1 month limit exceeded' do
|
||||
before { withdraw.save }
|
||||
it do
|
||||
withdraw.update(created_at: 2.day.ago)
|
||||
withdraw = build(:btc_withdraw, :with_deposit_liability, member: member, sum: 0.6.to_d)
|
||||
expect(withdraw.verify_limits).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'zero limits' do
|
||||
before { WithdrawLimit.last.update!(limit_24_hour: 0, limit_1_month: 0) }
|
||||
|
||||
it { expect(withdraw.valid?).to be_truthy }
|
||||
end
|
||||
|
||||
context 'there are no WLs in DB' do
|
||||
before { WithdrawLimit.delete_all }
|
||||
|
||||
it { expect(withdraw.valid?).to be_truthy }
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user