double_entry 1.0.1 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +432 -0
- data/README.md +36 -9
- data/double_entry.gemspec +20 -48
- data/lib/active_record/locking_extensions.rb +3 -3
- data/lib/active_record/locking_extensions/log_subscriber.rb +1 -1
- data/lib/double_entry/account.rb +38 -45
- data/lib/double_entry/account_balance.rb +18 -1
- data/lib/double_entry/errors.rb +13 -13
- data/lib/double_entry/line.rb +3 -2
- data/lib/double_entry/reporting.rb +26 -38
- data/lib/double_entry/reporting/aggregate.rb +43 -23
- data/lib/double_entry/reporting/aggregate_array.rb +16 -13
- data/lib/double_entry/reporting/line_aggregate.rb +3 -2
- data/lib/double_entry/reporting/line_aggregate_filter.rb +8 -10
- data/lib/double_entry/reporting/line_metadata_filter.rb +33 -0
- data/lib/double_entry/transfer.rb +33 -27
- data/lib/double_entry/validation.rb +1 -0
- data/lib/double_entry/validation/account_fixer.rb +36 -0
- data/lib/double_entry/validation/line_check.rb +22 -40
- data/lib/double_entry/version.rb +1 -1
- data/lib/generators/double_entry/install/install_generator.rb +7 -1
- data/lib/generators/double_entry/install/templates/migration.rb +27 -25
- metadata +33 -243
- data/.gitignore +0 -32
- data/.rspec +0 -2
- data/.travis.yml +0 -29
- data/.yardopts +0 -2
- data/Gemfile +0 -2
- data/Rakefile +0 -15
- data/script/jack_hammer +0 -210
- data/script/setup.sh +0 -8
- data/spec/active_record/locking_extensions_spec.rb +0 -110
- data/spec/double_entry/account_balance_spec.rb +0 -7
- data/spec/double_entry/account_spec.rb +0 -130
- data/spec/double_entry/balance_calculator_spec.rb +0 -88
- data/spec/double_entry/configuration_spec.rb +0 -50
- data/spec/double_entry/line_spec.rb +0 -80
- data/spec/double_entry/locking_spec.rb +0 -214
- data/spec/double_entry/performance/double_entry_performance_spec.rb +0 -32
- data/spec/double_entry/performance/reporting/aggregate_performance_spec.rb +0 -50
- data/spec/double_entry/reporting/aggregate_array_spec.rb +0 -123
- data/spec/double_entry/reporting/aggregate_spec.rb +0 -205
- data/spec/double_entry/reporting/line_aggregate_filter_spec.rb +0 -90
- data/spec/double_entry/reporting/line_aggregate_spec.rb +0 -39
- data/spec/double_entry/reporting/month_range_spec.rb +0 -139
- data/spec/double_entry/reporting/time_range_array_spec.rb +0 -169
- data/spec/double_entry/reporting/time_range_spec.rb +0 -45
- data/spec/double_entry/reporting/week_range_spec.rb +0 -103
- data/spec/double_entry/reporting_spec.rb +0 -181
- data/spec/double_entry/transfer_spec.rb +0 -93
- data/spec/double_entry/validation/line_check_spec.rb +0 -99
- data/spec/double_entry_spec.rb +0 -428
- data/spec/generators/double_entry/install/install_generator_spec.rb +0 -30
- data/spec/spec_helper.rb +0 -118
- data/spec/support/accounts.rb +0 -21
- data/spec/support/blueprints.rb +0 -43
- data/spec/support/database.example.yml +0 -21
- data/spec/support/database.travis.yml +0 -24
- data/spec/support/double_entry_spec_helper.rb +0 -27
- data/spec/support/gemfiles/Gemfile.rails-3.2.x +0 -8
- data/spec/support/gemfiles/Gemfile.rails-4.1.x +0 -6
- data/spec/support/gemfiles/Gemfile.rails-4.2.x +0 -5
- data/spec/support/gemfiles/Gemfile.rails-5.0.x +0 -5
- data/spec/support/performance_helper.rb +0 -26
- data/spec/support/reporting_configuration.rb +0 -6
- data/spec/support/schema.rb +0 -74
data/.gitignore
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
*.gem
|
2
|
-
*.rbc
|
3
|
-
/.config
|
4
|
-
/coverage/
|
5
|
-
/InstalledFiles
|
6
|
-
/pkg/
|
7
|
-
/spec/reports/
|
8
|
-
/test/tmp/
|
9
|
-
/test/version_tmp/
|
10
|
-
/tmp/
|
11
|
-
|
12
|
-
## Performance profiling
|
13
|
-
/profiles/
|
14
|
-
|
15
|
-
## Documentation cache and generated files:
|
16
|
-
/.yardoc/
|
17
|
-
/_yardoc/
|
18
|
-
/doc/
|
19
|
-
/rdoc/
|
20
|
-
|
21
|
-
## Environment normalisation:
|
22
|
-
/.bundle/
|
23
|
-
/lib/bundler/man/
|
24
|
-
/Gemfile.lock
|
25
|
-
/.ruby-version
|
26
|
-
/.ruby-gemset
|
27
|
-
|
28
|
-
# DoubleEntry specific
|
29
|
-
/bin/
|
30
|
-
/log/
|
31
|
-
/spec/reports/
|
32
|
-
/spec/support/database.yml
|
data/.rspec
DELETED
data/.travis.yml
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
sudo: false
|
3
|
-
before_script:
|
4
|
-
- cp spec/support/database.travis.yml spec/support/database.yml
|
5
|
-
- mysql -e 'create database double_entry_test;'
|
6
|
-
- psql -c 'create database double_entry_test;' -U postgres
|
7
|
-
script:
|
8
|
-
- bundle exec rake spec
|
9
|
-
- ruby script/jack_hammer -t 2000
|
10
|
-
matrix:
|
11
|
-
include:
|
12
|
-
- rvm: 2.1
|
13
|
-
gemfile: spec/support/gemfiles/Gemfile.rails-3.2.x
|
14
|
-
env: DB=mysql
|
15
|
-
- rvm: 2.2
|
16
|
-
gemfile: spec/support/gemfiles/Gemfile.rails-4.1.x
|
17
|
-
env: DB=mysql
|
18
|
-
- rvm: 2.2
|
19
|
-
gemfile: spec/support/gemfiles/Gemfile.rails-4.2.x
|
20
|
-
env: DB=mysql
|
21
|
-
- rvm: 2.3
|
22
|
-
gemfile: spec/support/gemfiles/Gemfile.rails-5.0.x
|
23
|
-
env: DB=mysql
|
24
|
-
- rvm: 2.3
|
25
|
-
gemfile: spec/support/gemfiles/Gemfile.rails-5.0.x
|
26
|
-
env: DB=sqlite
|
27
|
-
- rvm: 2.3
|
28
|
-
gemfile: spec/support/gemfiles/Gemfile.rails-5.0.x
|
29
|
-
env: DB=postgres
|
data/.yardopts
DELETED
data/Gemfile
DELETED
data/Rakefile
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
require 'rspec/core/rake_task'
|
2
|
-
require 'bundler/gem_tasks'
|
3
|
-
|
4
|
-
RSpec::Core::RakeTask.new(:spec) do |t|
|
5
|
-
t.verbose = false
|
6
|
-
t.ruby_opts = '-w'
|
7
|
-
end
|
8
|
-
|
9
|
-
task :default do
|
10
|
-
%w(mysql postgres sqlite).each do |db|
|
11
|
-
puts "Running tests with `DB=#{db}`"
|
12
|
-
ENV['DB'] = db
|
13
|
-
Rake::Task['spec'].execute
|
14
|
-
end
|
15
|
-
end
|
data/script/jack_hammer
DELETED
@@ -1,210 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
# Run a concurrency test on the double_entry code.
|
4
|
-
#
|
5
|
-
# This spawns a bunch of processes, and does random transactions between a set
|
6
|
-
# of accounts, then validates that all the numbers add up at the end.
|
7
|
-
#
|
8
|
-
# You can also tell it to flush our the account balances table at regular
|
9
|
-
# intervals, to validate that new account balances records get created with the
|
10
|
-
# correct balances from the lines table.
|
11
|
-
#
|
12
|
-
# Run it without arguments to get the usage.
|
13
|
-
|
14
|
-
require 'optparse'
|
15
|
-
require 'bundler/setup'
|
16
|
-
require 'active_record'
|
17
|
-
require 'database_cleaner'
|
18
|
-
|
19
|
-
support = File.expand_path("../../spec/support/", __FILE__)
|
20
|
-
|
21
|
-
db_engine = ENV['DB'] || 'mysql'
|
22
|
-
|
23
|
-
if db_engine == 'sqlite'
|
24
|
-
puts "Skipping jackhammer for SQLITE..."
|
25
|
-
exit 0
|
26
|
-
end
|
27
|
-
|
28
|
-
ActiveRecord::Base.establish_connection YAML.load_file(File.join(support, "database.yml"))[db_engine]
|
29
|
-
require "#{support}/schema"
|
30
|
-
|
31
|
-
lib = File.expand_path('../../lib', __FILE__)
|
32
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
33
|
-
require 'double_entry'
|
34
|
-
|
35
|
-
def parse_options
|
36
|
-
$account_count = 5
|
37
|
-
$process_count = 20
|
38
|
-
$transfer_count = 20000
|
39
|
-
$balance_flush_count = 1
|
40
|
-
$use_threads = false
|
41
|
-
|
42
|
-
options = OptionParser.new
|
43
|
-
|
44
|
-
options.on("-a", "--accounts=COUNT", Integer, "Number of accounts (default: #{$account_count})") do |value|
|
45
|
-
$account_count = value
|
46
|
-
end
|
47
|
-
|
48
|
-
options.on("-p", "--processes=COUNT", Integer, "Number of processes (default: #{$process_count})") do |value|
|
49
|
-
$process_count = value
|
50
|
-
end
|
51
|
-
|
52
|
-
options.on("-t", "--transfers=COUNT", Integer, "Number of transfers (default: #{$transfer_count})") do |value|
|
53
|
-
$transfer_count = value
|
54
|
-
end
|
55
|
-
|
56
|
-
options.on("-f", "--flush-balances=COUNT", Integer, "Flush account balances table COUNT times") do |value|
|
57
|
-
$balance_flush_count = value
|
58
|
-
end
|
59
|
-
|
60
|
-
options.on("-z", "--threads", "Use threads instead of processes") do |value|
|
61
|
-
$use_threads = !!value
|
62
|
-
end
|
63
|
-
|
64
|
-
options.parse(*ARGV)
|
65
|
-
end
|
66
|
-
|
67
|
-
|
68
|
-
def clean_out_database
|
69
|
-
puts "Cleaning out the database..."
|
70
|
-
|
71
|
-
DatabaseCleaner.clean_with(:truncation)
|
72
|
-
end
|
73
|
-
|
74
|
-
def create_accounts_and_transfers
|
75
|
-
puts "Setting up #{$account_count} accounts..."
|
76
|
-
|
77
|
-
DoubleEntry.configure do |config|
|
78
|
-
|
79
|
-
# Create the accounts.
|
80
|
-
config.define_accounts do |accounts|
|
81
|
-
scope = ->(x) { x }
|
82
|
-
$account_count.times do |i|
|
83
|
-
accounts.define(:identifier => :"account-#{i}", :scope_identifier => scope)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
# Create all the possible transfers.
|
88
|
-
config.define_transfers do |transfers|
|
89
|
-
config.accounts.each do |from|
|
90
|
-
config.accounts.each do |to|
|
91
|
-
transfers.define(:from => from.identifier, :to => to.identifier, :code => :test)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
# Find account instances so we have something to work with.
|
97
|
-
$accounts = config.accounts.map do |account|
|
98
|
-
DoubleEntry.account(account.identifier, :scope => 1)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
|
104
|
-
def run_tests
|
105
|
-
puts "Spawning #{$process_count} processes..."
|
106
|
-
|
107
|
-
iterations_per_process = [ ($transfer_count / $process_count / $balance_flush_count), 1 ].max
|
108
|
-
|
109
|
-
$balance_flush_count.times do
|
110
|
-
puts "Flushing balances"
|
111
|
-
DoubleEntry::AccountBalance.delete_all
|
112
|
-
ActiveRecord::Base.connection_pool.disconnect!
|
113
|
-
|
114
|
-
if $use_threads
|
115
|
-
puts "Using threads as workers"
|
116
|
-
threads = []
|
117
|
-
$process_count.times do |process_num|
|
118
|
-
threads << Thread.new { run_process(iterations_per_process, process_num) }
|
119
|
-
end
|
120
|
-
|
121
|
-
threads.each(&:join)
|
122
|
-
else
|
123
|
-
puts "Using processes as workers"
|
124
|
-
pids = []
|
125
|
-
$process_count.times do |process_num|
|
126
|
-
pids << fork { run_process(iterations_per_process, process_num) }
|
127
|
-
end
|
128
|
-
|
129
|
-
pids.each {|pid| Process.wait2(pid) }
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
|
135
|
-
def run_process(iterations, process_num)
|
136
|
-
srand # Seed the random number generator separately for each process.
|
137
|
-
|
138
|
-
puts "Process #{process_num} running #{iterations} transfers..."
|
139
|
-
|
140
|
-
iterations.times do |i|
|
141
|
-
account_a = $accounts.sample
|
142
|
-
account_b = ($accounts - [account_a]).sample
|
143
|
-
amount = rand(1000) + 1
|
144
|
-
|
145
|
-
DoubleEntry.transfer(Money.new(amount), :from => account_a, :to => account_b, :code => :test)
|
146
|
-
|
147
|
-
puts "Process #{process_num} completed #{i+1} transfers" if (i+1) % 100 == 0
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
|
152
|
-
def reconcile
|
153
|
-
error_count = 0
|
154
|
-
puts "Reconciling..."
|
155
|
-
|
156
|
-
if DoubleEntry::Line.count == $transfer_count * 2
|
157
|
-
puts "All the Line records were written, FTW!"
|
158
|
-
else
|
159
|
-
puts "Not enough Line records written. :("
|
160
|
-
error_count += 1
|
161
|
-
end
|
162
|
-
|
163
|
-
if $accounts.all? {|account| DoubleEntry::Reporting.reconciled?(account) }
|
164
|
-
puts "All accounts reconciled, FTW!"
|
165
|
-
else
|
166
|
-
$accounts.each do |account|
|
167
|
-
if !DoubleEntry::Reporting.reconciled?(account)
|
168
|
-
puts "Account #{account.identifier} failed to reconcile. :("
|
169
|
-
|
170
|
-
# See http://bugs.mysql.com/bug.php?id=51431
|
171
|
-
use_index = if DoubleEntry::Line.connection.adapter_name.match /mysql/i
|
172
|
-
"USE INDEX (lines_scope_account_id_idx)"
|
173
|
-
else
|
174
|
-
""
|
175
|
-
end
|
176
|
-
|
177
|
-
rows = ActiveRecord::Base.connection.select_all(<<-SQL)
|
178
|
-
SELECT id, amount, balance
|
179
|
-
FROM #{DoubleEntry::Line.quoted_table_name} #{use_index}
|
180
|
-
WHERE scope = '#{account.scope_identity}'
|
181
|
-
AND account = '#{account.identifier}'
|
182
|
-
ORDER BY id
|
183
|
-
SQL
|
184
|
-
|
185
|
-
rows.each_cons(2) do |a, b|
|
186
|
-
if a["balance"].to_i + b["amount"].to_i != b["balance"].to_i
|
187
|
-
puts "Bad lines entry id = #{b['id']}"
|
188
|
-
error_count += 1
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
error_count == 0
|
196
|
-
end
|
197
|
-
|
198
|
-
|
199
|
-
parse_options
|
200
|
-
clean_out_database
|
201
|
-
create_accounts_and_transfers
|
202
|
-
run_tests
|
203
|
-
|
204
|
-
if reconcile
|
205
|
-
puts "Done successfully :)"
|
206
|
-
exit 0
|
207
|
-
else
|
208
|
-
puts "Done with errors :("
|
209
|
-
exit 1
|
210
|
-
end
|
data/script/setup.sh
DELETED
@@ -1,110 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
RSpec.describe ActiveRecord::LockingExtensions do
|
4
|
-
PG_DEADLOCK = ActiveRecord::StatementInvalid.new('PG::Error: ERROR: deadlock detected')
|
5
|
-
MYSQL_DEADLOCK = ActiveRecord::StatementInvalid.new('Mysql::Error: Deadlock found when trying to get lock')
|
6
|
-
SQLITE3_LOCK = ActiveRecord::StatementInvalid.new('SQLite3::BusyException: database is locked: UPDATE...')
|
7
|
-
|
8
|
-
context '#restartable_transaction' do
|
9
|
-
it "keeps running the lock until a ActiveRecord::RestartTransaction isn't raised" do
|
10
|
-
expect(User).to receive(:create!).ordered.and_raise(ActiveRecord::RestartTransaction)
|
11
|
-
expect(User).to receive(:create!).ordered.and_raise(ActiveRecord::RestartTransaction)
|
12
|
-
expect(User).to receive(:create!).ordered.and_return(true)
|
13
|
-
|
14
|
-
expect { User.restartable_transaction { User.create! } }.to_not raise_error
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
context '#with_restart_on_deadlock' do
|
19
|
-
shared_examples 'abstract adapter' do
|
20
|
-
it 'raises a ActiveRecord::RestartTransaction error if a deadlock occurs' do
|
21
|
-
expect { User.with_restart_on_deadlock { fail exception } }.
|
22
|
-
to raise_error(ActiveRecord::RestartTransaction)
|
23
|
-
end
|
24
|
-
|
25
|
-
it 'publishes a notification' do
|
26
|
-
expect(ActiveSupport::Notifications).
|
27
|
-
to receive(:publish).
|
28
|
-
with('deadlock_restart.active_record', hash_including(:exception => exception))
|
29
|
-
expect { User.with_restart_on_deadlock { fail exception } }.to raise_error
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
context 'mysql' do
|
34
|
-
let(:exception) { MYSQL_DEADLOCK }
|
35
|
-
|
36
|
-
it_behaves_like 'abstract adapter'
|
37
|
-
end
|
38
|
-
|
39
|
-
context 'postgres' do
|
40
|
-
let(:exception) { PG_DEADLOCK }
|
41
|
-
|
42
|
-
it_behaves_like 'abstract adapter'
|
43
|
-
end
|
44
|
-
|
45
|
-
context 'sqlite' do
|
46
|
-
let(:exception) { SQLITE3_LOCK }
|
47
|
-
|
48
|
-
it_behaves_like 'abstract adapter'
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
context '#create_ignoring_duplicates' do
|
53
|
-
it 'does not raise an error if a duplicate index error is raised in the database' do
|
54
|
-
User.make! :username => 'keith'
|
55
|
-
|
56
|
-
expect { User.make! :username => 'keith' }.to raise_error
|
57
|
-
expect { User.create_ignoring_duplicates! :username => 'keith' }.to_not raise_error
|
58
|
-
end
|
59
|
-
|
60
|
-
it 'publishes a notification when a duplicate is encountered' do
|
61
|
-
User.make! :username => 'keith'
|
62
|
-
|
63
|
-
expect(ActiveSupport::Notifications).
|
64
|
-
to receive(:publish).
|
65
|
-
with('duplicate_ignore.active_record', hash_including(:exception => kind_of(ActiveRecord::RecordNotUnique)))
|
66
|
-
|
67
|
-
expect { User.create_ignoring_duplicates! :username => 'keith' }.to_not raise_error
|
68
|
-
end
|
69
|
-
|
70
|
-
shared_examples 'abstract adapter' do
|
71
|
-
it 'retries the creation if a deadlock error is raised from the database' do
|
72
|
-
expect(User).to receive(:create!).ordered.and_raise(exception)
|
73
|
-
expect(User).to receive(:create!).ordered.and_return(true)
|
74
|
-
|
75
|
-
expect { User.create_ignoring_duplicates! }.to_not raise_error
|
76
|
-
end
|
77
|
-
|
78
|
-
it 'publishes a notification on each retry' do
|
79
|
-
expect(User).to receive(:create!).ordered.and_raise(exception)
|
80
|
-
expect(User).to receive(:create!).ordered.and_raise(exception)
|
81
|
-
expect(User).to receive(:create!).ordered.and_return(true)
|
82
|
-
|
83
|
-
expect(ActiveSupport::Notifications).
|
84
|
-
to receive(:publish).
|
85
|
-
with('deadlock_retry.active_record', hash_including(:exception => exception)).
|
86
|
-
twice
|
87
|
-
|
88
|
-
expect { User.create_ignoring_duplicates! }.to_not raise_error
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
context 'mysql' do
|
93
|
-
let(:exception) { MYSQL_DEADLOCK }
|
94
|
-
|
95
|
-
it_behaves_like 'abstract adapter'
|
96
|
-
end
|
97
|
-
|
98
|
-
context 'postgres' do
|
99
|
-
let(:exception) { PG_DEADLOCK }
|
100
|
-
|
101
|
-
it_behaves_like 'abstract adapter'
|
102
|
-
end
|
103
|
-
|
104
|
-
context 'sqlite' do
|
105
|
-
let(:exception) { SQLITE3_LOCK }
|
106
|
-
|
107
|
-
it_behaves_like 'abstract adapter'
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
@@ -1,130 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module DoubleEntry
|
3
|
-
RSpec.describe Account do
|
4
|
-
let(:identity_scope) { ->(value) { value } }
|
5
|
-
|
6
|
-
describe '::new' do
|
7
|
-
context 'given an identifier 31 characters in length' do
|
8
|
-
let(:identifier) { 'xxxxxxxx 31 characters xxxxxxxx' }
|
9
|
-
specify do
|
10
|
-
expect { Account.new(:identifier => identifier) }.to_not raise_error
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
context 'given an identifier 32 characters in length' do
|
15
|
-
let(:identifier) { 'xxxxxxxx 32 characters xxxxxxxxx' }
|
16
|
-
specify do
|
17
|
-
expect { Account.new(:identifier => identifier) }.to raise_error AccountIdentifierTooLongError, /'#{identifier}'/
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
describe Account::Instance do
|
23
|
-
it 'is sortable' do
|
24
|
-
account = Account.new(:identifier => 'savings', :scope_identifier => identity_scope)
|
25
|
-
a = Account::Instance.new(:account => account, :scope => '123')
|
26
|
-
b = Account::Instance.new(:account => account, :scope => '456')
|
27
|
-
expect([b, a].sort).to eq [a, b]
|
28
|
-
end
|
29
|
-
|
30
|
-
it 'is hashable' do
|
31
|
-
account = Account.new(:identifier => 'savings', :scope_identifier => identity_scope)
|
32
|
-
a1 = Account::Instance.new(:account => account, :scope => '123')
|
33
|
-
a2 = Account::Instance.new(:account => account, :scope => '123')
|
34
|
-
b = Account::Instance.new(:account => account, :scope => '456')
|
35
|
-
|
36
|
-
expect(a1.hash).to eq a2.hash
|
37
|
-
expect(a1.hash).to_not eq b.hash
|
38
|
-
end
|
39
|
-
|
40
|
-
describe '::new' do
|
41
|
-
let(:account) { Account.new(:identifier => 'x', :scope_identifier => identity_scope) }
|
42
|
-
subject(:initialize_account_instance) { Account::Instance.new(:account => account, :scope => scope) }
|
43
|
-
|
44
|
-
context 'given a scope identifier 23 characters in length' do
|
45
|
-
let(:scope) { 'xxxx 23 characters xxxx' }
|
46
|
-
specify { expect { initialize_account_instance }.to_not raise_error }
|
47
|
-
end
|
48
|
-
|
49
|
-
context 'given a scope identifier 24 characters in length' do
|
50
|
-
let(:scope) { 'xxxx 24 characters xxxxx' }
|
51
|
-
specify { expect { initialize_account_instance }.to raise_error ScopeIdentifierTooLongError, /'#{scope}'/ }
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
describe 'currency' do
|
57
|
-
it 'defaults to USD currency' do
|
58
|
-
account = DoubleEntry::Account.new(:identifier => 'savings', :scope_identifier => identity_scope)
|
59
|
-
expect(DoubleEntry::Account::Instance.new(:account => account).currency).to eq('USD')
|
60
|
-
end
|
61
|
-
|
62
|
-
it 'allows the currency to be set' do
|
63
|
-
account = DoubleEntry::Account.new(:identifier => 'savings', :scope_identifier => identity_scope, :currency => 'AUD')
|
64
|
-
expect(DoubleEntry::Account::Instance.new(:account => account).currency).to eq('AUD')
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
describe Account::Set do
|
69
|
-
describe '#define' do
|
70
|
-
context "given a 'savings' account is defined" do
|
71
|
-
before { subject.define(:identifier => 'savings') }
|
72
|
-
its(:first) { should be_an Account }
|
73
|
-
its('first.identifier') { should eq 'savings' }
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
describe '#active_record_scope_identifier' do
|
78
|
-
subject(:scope) { Account::Set.new.active_record_scope_identifier(ar_class) }
|
79
|
-
|
80
|
-
context 'given ActiveRecordScopeFactory is stubbed' do
|
81
|
-
let(:scope_identifier) { double(:scope_identifier) }
|
82
|
-
let(:scope_factory) { double(:scope_factory, :scope_identifier => scope_identifier) }
|
83
|
-
let(:ar_class) { double(:ar_class) }
|
84
|
-
before { allow(Account::ActiveRecordScopeFactory).to receive(:new).with(ar_class).and_return(scope_factory) }
|
85
|
-
|
86
|
-
it { should eq scope_identifier }
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
RSpec.describe Account::ActiveRecordScopeFactory do
|
93
|
-
context 'given the class User' do
|
94
|
-
subject(:factory) { Account::ActiveRecordScopeFactory.new(User) }
|
95
|
-
|
96
|
-
describe '#scope_identifier' do
|
97
|
-
subject(:scope_identifier) { factory.scope_identifier }
|
98
|
-
|
99
|
-
describe '#call' do
|
100
|
-
subject(:scope) { scope_identifier.call(value) }
|
101
|
-
|
102
|
-
context 'given a User instance with ID 32' do
|
103
|
-
let(:value) { User.make(:id => 32) }
|
104
|
-
|
105
|
-
it { should eq 32 }
|
106
|
-
end
|
107
|
-
|
108
|
-
context 'given differing model instance with ID 32' do
|
109
|
-
let(:value) { double(:id => 32) }
|
110
|
-
it 'raises an error' do
|
111
|
-
expect { scope_identifier.call(value) }.to raise_error DoubleEntry::AccountScopeMismatchError
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
context "given the String 'I am a bearded lady'" do
|
116
|
-
let(:value) { 'I am a bearded lady' }
|
117
|
-
|
118
|
-
it { should eq 'I am a bearded lady' }
|
119
|
-
end
|
120
|
-
|
121
|
-
context 'given the Integer 42' do
|
122
|
-
let(:value) { 42 }
|
123
|
-
|
124
|
-
it { should eq 42 }
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|