Initial commit

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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