Note that Rails 7.1 changes `find_or_create_by!` so it calls `create_or_find_by!` when no record is found, meaning we'll rarely get `RecordNotUnique` exceptions when using this method during a race condition. Adding this index means we need to remove the uniqueness validation. According to the `create_or_find_by` documentation [1]: > Columns with unique database constraints should not have uniqueness > validations defined, otherwise create will fail due to validation > errors and find_by will never be called. We're adding a test that checks what happens when using `create_or_find_by!`. Note that we're creating voters combining `create_with` with `find_or_create_by!`. Using `find_or_create_by!(...)` with all attributes (including non-key ones like `origin`) fails when a voter already exists with different values, e.g. an existing `origin: "web"` and an incoming `"booth"`. In this situation the existing record is not matched and the unique index raises an exception. `create_with(...).find_or_create_by!(user: ..., poll: ...)` searches by the unique key only and applies the extra attributes only on creation. Existing voters are returned unchanged, which is the intended behavior. For the `take_votes_from` method, we're handling a (highly unlikely, but theoretically possible) scenario where a user votes at the same time as taking voters from another user. For that, we're doing something similar to what `create_or_find_by!` does: we're updating the `user_id` column inside a new transaction (using a new transactions avoids a `PG::InFailedSqlTransaction` exception when there are duplicate records), and deleting the existing voter when we get a `RecordNotUnique` exception. On `Poll::WebVote` we're simply raising an exception when there's already a user who's voted via booth, because the `Poll::WebVote#update` method should never be called in this case. We still need to use `with_lock` in `Poll::WebVote`, but not due to duplicate voters (`find_or_create_by!` method will now handle the unique record scenario, even in the case of simultaneous transactions), but because we use a uniqueness validation in `Poll::Answer`; this validation would cause an error in simultaneous transactions. [1] https://api.rubyonrails.org/v7.1/classes/ActiveRecord/Relation.html#method-i-create_or_find_by
78 lines
2.5 KiB
Ruby
78 lines
2.5 KiB
Ruby
require "rails_helper"
|
|
|
|
describe Officing::VotersController do
|
|
describe "POST create" do
|
|
let(:officer) { create(:poll_officer) }
|
|
before { sign_in(officer.user) }
|
|
|
|
it "does not create two records with two simultaneous requests", :race_condition do
|
|
poll = create(:poll, officers: [officer])
|
|
user = create(:user, :level_two)
|
|
|
|
2.times.map do
|
|
Thread.new do
|
|
post :create, params: {
|
|
voter: { poll_id: poll.id, user_id: user.id },
|
|
format: :js
|
|
}
|
|
end
|
|
end.each(&:join)
|
|
|
|
expect(Poll::Voter.count).to eq 1
|
|
expect(Poll::Voter.last.officer_id).to eq(officer.id)
|
|
end
|
|
|
|
it "stores officer and booth information" do
|
|
user = create(:user, :in_census)
|
|
poll1 = create(:poll, name: "Would you be interested in XYZ?")
|
|
poll2 = create(:poll, name: "Testing polls")
|
|
|
|
booth = create(:poll_booth)
|
|
|
|
assignment1 = create(:poll_booth_assignment, poll: poll1, booth: booth)
|
|
assignment2 = create(:poll_booth_assignment, poll: poll2, booth: booth)
|
|
create(:poll_shift, officer: officer, booth: booth, date: Date.current, task: :vote_collection)
|
|
|
|
validate_officer
|
|
set_officing_booth(booth)
|
|
|
|
post :create, params: {
|
|
voter: { poll_id: poll1.id, user_id: user.id },
|
|
format: :js
|
|
}
|
|
expect(response).to be_successful
|
|
|
|
post :create, params: {
|
|
voter: { poll_id: poll2.id, user_id: user.id },
|
|
format: :js
|
|
}
|
|
expect(response).to be_successful
|
|
|
|
expect(Poll::Voter.count).to eq(2)
|
|
|
|
voter1 = Poll::Voter.first
|
|
expect(voter1.booth_assignment).to eq(assignment1)
|
|
expect(voter1.officer_assignment).to eq(assignment1.officer_assignments.first)
|
|
|
|
voter2 = Poll::Voter.last
|
|
expect(voter2.booth_assignment).to eq(assignment2)
|
|
expect(voter2.officer_assignment).to eq(assignment2.officer_assignments.first)
|
|
end
|
|
|
|
it "does not overwrite non key attributes when a web voter already exists" do
|
|
user = create(:user, :level_two, document_number: "11223344Z")
|
|
poll = create(:poll, officers: [officer])
|
|
existing = create(:poll_voter, poll: poll, user: user, origin: "web")
|
|
|
|
expect do
|
|
post :create, params: { voter: { poll_id: poll.id, user_id: user.id }, format: :js }
|
|
expect(response).to be_successful
|
|
end.not_to change { Poll::Voter.count }
|
|
|
|
existing.reload
|
|
expect(existing.origin).to eq "web"
|
|
expect(existing.document_number).to eq "11223344Z"
|
|
end
|
|
end
|
|
end
|