Merge pull request #4061 from consul/ballot_race_condition

Fix race condition with ballot lines
This commit is contained in:
Javier Martín
2020-07-27 13:01:37 +02:00
committed by GitHub
5 changed files with 27 additions and 3 deletions

View File

@@ -68,6 +68,7 @@ end
group :development, :test do group :development, :test do
gem "bullet", "~> 5.9.0" gem "bullet", "~> 5.9.0"
gem "byebug", "~> 11.1.1" gem "byebug", "~> 11.1.1"
gem "database_cleaner", "~> 1.7.0"
gem "factory_bot_rails", "~> 4.8.2" gem "factory_bot_rails", "~> 4.8.2"
gem "faker", "~> 1.8.7" gem "faker", "~> 1.8.7"
gem "i18n-tasks", "~> 0.9.29" gem "i18n-tasks", "~> 0.9.29"
@@ -94,7 +95,6 @@ group :development do
gem "capistrano-rails", "~> 1.4.0", require: false gem "capistrano-rails", "~> 1.4.0", require: false
gem "capistrano3-delayed-job", "~> 1.7.3" gem "capistrano3-delayed-job", "~> 1.7.3"
gem "capistrano3-puma", "~> 4.0.0" gem "capistrano3-puma", "~> 4.0.0"
gem "database_cleaner", "~> 1.7.0"
gem "erb_lint", require: false gem "erb_lint", require: false
gem "github_changelog_generator", "~> 1.15.0" gem "github_changelog_generator", "~> 1.15.0"
gem "mdl", "~> 0.5.0", require: false gem "mdl", "~> 0.5.0", require: false

View File

@@ -19,6 +19,7 @@ class Budget
after_save :store_user_heading after_save :store_user_heading
def check_sufficient_funds def check_sufficient_funds
ballot.lock!
errors.add(:money, "insufficient funds") if ballot.amount_available(investment.heading) < investment.price.to_i errors.add(:money, "insufficient funds") if ballot.amount_available(investment.heading) < investment.price.to_i
end end

View File

@@ -26,6 +26,18 @@ describe Budget::Ballot::Line do
investment.update!(price: heading.price - 1) investment.update!(price: heading.price - 1)
expect(ballot_line).to be_valid expect(ballot_line).to be_valid
end end
it "validates sufficient funds when creating lines at the same time", :race_condition do
investment.update!(price: heading.price)
other_investment = create(:budget_investment, :selected, price: heading.price, heading: heading)
other_line = build(:budget_ballot_line, ballot: ballot, investment: other_investment)
[ballot_line, other_line].map do |line|
Thread.new { line.save }
end.each(&:join)
expect(Budget::Ballot::Line.count).to be 1
end
end end
describe "Selectibility" do describe "Selectibility" do
@@ -49,7 +61,7 @@ describe Budget::Ballot::Line do
create(:budget_ballot_line, ballot: ballot, investment: investment) create(:budget_ballot_line, ballot: ballot, investment: investment)
expect(user.balloted_heading_id).to eq(investment.heading.id) expect(user.reload.balloted_heading_id).to eq(investment.heading.id)
end end
end end

View File

@@ -10,11 +10,13 @@ describe Budget::Stats do
let!(:author_and_voter) { create(:user, :hidden, votables: [investment]) } let!(:author_and_voter) { create(:user, :hidden, votables: [investment]) }
let!(:voter) { create(:user, votables: [investment]) } let!(:voter) { create(:user, votables: [investment]) }
let!(:voter_and_balloter) { create(:user, votables: [investment], ballot_lines: [investment]) } let!(:voter_and_balloter) { create(:user, votables: [investment], ballot_lines: [investment]) }
let!(:balloter) { create(:user, :hidden, ballot_lines: [investment]) } let!(:balloter) { create(:user, ballot_lines: [investment]) }
let!(:poll_balloter) { create(:user, :level_two) } let!(:poll_balloter) { create(:user, :level_two) }
let!(:non_participant) { create(:user, :level_two) } let!(:non_participant) { create(:user, :level_two) }
before do before do
balloter.hide
create(:budget_investment, :selected, budget: budget, author: author_and_voter) create(:budget_investment, :selected, budget: budget, author: author_and_voter)
create(:poll_voter, :from_booth, user: poll_balloter, budget: budget) create(:poll_voter, :from_booth, user: poll_balloter, budget: budget)

View File

@@ -30,6 +30,15 @@ RSpec.configure do |config|
Setting["feature.user.skip_verification"] = nil Setting["feature.user.skip_verification"] = nil
end end
config.around(:each, :race_condition) do |example|
self.use_transactional_tests = false
example.run
self.use_transactional_tests = true
DatabaseCleaner.clean_with(:truncation)
Rails.application.load_seed
end
config.before(:each, type: :system) do config.before(:each, type: :system) do
Capybara::Webmock.start Capybara::Webmock.start
end end