Files
nairobi/spec/models/poll/answer_spec.rb
Javi Martín 5033691666 Avoid duplicate records in poll answers
Until now, we've stored the text of the answer somebody replied to. The
idea was to handle the scenarios where the user voters for an option but
then that option is deleted and restored, or the texts of the options
are accidentally edited and so the option "Yes" is now "Now" and vice
versa.

However, since commit 3a6e99cb8, options can no longer be edited once
the poll starts, so there's no risk of the option changing once somebody
has voted.

This means we can now store the ID of the option that has been voted.
That'll also help us deal with a bug introduced int 673ec075e, since
answers in different locales are not counted as the same answer. Note we
aren't dealing with this bug right now.

We're still keeping (and storing) the answer as well. There are two
reasons for that.

First, we might add an "open answer" type of questions in the future and
use this column for it.

Second, we've still got logic depending on the answer, and we need to be
careful when changing it because there are existing installations where
the answer is present but the option_id is not.

Note that we're using `dependent: nullify`. The reasoning is that, since
we're storing both the option_id and the answer text, we can still use
the answer text when removing the option. In practice, this won't matter
much, though, since we've got a validation rule that makes it impossible
to destroy options once the poll has started.

Also note we're still allowing duplicate records when the option is nil.
We need to do that until we've removed every duplicate record in the
database.
2024-06-26 20:20:24 +02:00

239 lines
8.7 KiB
Ruby

require "rails_helper"
describe Poll::Answer do
describe "validations" do
let(:answer) { build(:poll_answer) }
it "is valid" do
expect(answer).to be_valid
end
it "is not valid wihout a question" do
answer.question = nil
expect(answer).not_to be_valid
end
it "is not valid without an author" do
answer.author = nil
expect(answer).not_to be_valid
end
it "is not valid without an author when multiple answers are allowed" do
answer.author = nil
answer.question = create(:poll_question_multiple)
expect(answer).not_to be_valid
end
it "is not valid without an answer" do
answer.answer = nil
expect(answer).not_to be_valid
end
it "is not valid if there's already an answer to that question" do
author = create(:user)
question = create(:poll_question, :yes_no)
create(:poll_answer, author: author, question: question)
answer = build(:poll_answer, author: author, question: question)
expect(answer).not_to be_valid
end
it "is not valid when user already reached multiple answers question max votes" do
author = create(:user)
question = create(:poll_question_multiple, :abc, max_votes: 2)
create(:poll_answer, author: author, question: question, answer: "Answer A")
create(:poll_answer, author: author, question: question, answer: "Answer B")
answer = build(:poll_answer, author: author, question: question, answer: "Answer C")
expect(answer).not_to be_valid
end
it "is not valid when there are two identical answers" do
author = create(:user)
question = create(:poll_question_multiple, :abc)
option = question.question_options.first
create(:poll_answer, author: author, question: question, option: option, answer: "Answer A")
answer = build(:poll_answer, author: author, question: question, option: option, answer: "Answer A")
expect(answer).not_to be_valid
expect { answer.save(validate: false) }.to raise_error ActiveRecord::RecordNotUnique
end
it "is not valid when there are two answers with the same option and different answer" do
author = create(:user)
question = create(:poll_question_multiple, :abc)
option = question.question_options.first
create(:poll_answer, author: author, question: question, option: option, answer: "Answer A")
answer = build(:poll_answer, author: author, question: question, option: option, answer: "Answer B")
expect(answer).not_to be_valid
expect { answer.save(validate: false) }.to raise_error ActiveRecord::RecordNotUnique
end
it "is valid when there are two identical answers and the option is nil" do
author = create(:user)
question = create(:poll_question_multiple, :abc)
create(:poll_answer, author: author, question: question, option: nil, answer: "Answer A")
answer = build(:poll_answer, author: author, question: question, option: nil, answer: "Answer A")
expect(answer).to be_valid
expect { answer.save }.not_to raise_error
end
it "is valid for answers included in the Poll::Question's question_options list" do
question = create(:poll_question)
create(:poll_question_option, title: "One", question: question)
create(:poll_question_option, title: "Two", question: question)
create(:poll_question_option, title: "Three", question: question)
expect(build(:poll_answer, question: question, answer: "One")).to be_valid
expect(build(:poll_answer, question: question, answer: "Two")).to be_valid
expect(build(:poll_answer, question: question, answer: "Three")).to be_valid
expect(build(:poll_answer, question: question, answer: "Four")).not_to be_valid
end
context "creating answers at the same time", :race_condition do
it "validates max votes on single-answer questions" do
author = create(:user)
question = create(:poll_question, :yes_no)
answer = build(:poll_answer, author: author, question: question, answer: "Yes")
other_answer = build(:poll_answer, author: author, question: question, answer: "No")
[answer, other_answer].map do |poll_answer|
Thread.new { poll_answer.save }
end.each(&:join)
expect(Poll::Answer.count).to be 1
end
it "validates max votes on multiple-answer questions" do
author = create(:user, :level_two)
question = create(:poll_question_multiple, :abc, max_votes: 2)
create(:poll_answer, question: question, answer: "Answer A", author: author)
answer = build(:poll_answer, question: question, answer: "Answer B", author: author)
other_answer = build(:poll_answer, question: question, answer: "Answer C", author: author)
[answer, other_answer].map do |poll_answer|
Thread.new { poll_answer.save }
end.each(&:join)
expect(Poll::Answer.count).to be 2
end
end
end
describe "#save_and_record_voter_participation" do
let(:author) { create(:user, :level_two) }
let(:poll) { create(:poll) }
let(:question) { create(:poll_question, :yes_no, poll: poll) }
it "creates a poll_voter with user and poll data" do
answer = create(:poll_answer, question: question, author: author, answer: "Yes")
expect(answer.poll.voters).to be_blank
answer.save_and_record_voter_participation
expect(poll.reload.voters.size).to eq(1)
voter = poll.voters.first
expect(voter.document_number).to eq(answer.author.document_number)
expect(voter.poll_id).to eq(answer.poll.id)
expect(voter.officer_id).to be nil
end
it "updates a poll_voter with user and poll data" do
answer = create(:poll_answer, question: question, author: author, answer: "Yes")
answer.save_and_record_voter_participation
expect(poll.reload.voters.size).to eq(1)
updated_answer = answer.question.find_or_initialize_user_answer(
answer.author,
answer.question.question_options.excluding(answer.option).sample.id
)
updated_answer.save_and_record_voter_participation
expect(poll.reload.voters.size).to eq(1)
voter = poll.voters.first
expect(voter.document_number).to eq(updated_answer.author.document_number)
expect(voter.poll_id).to eq(updated_answer.poll.id)
end
it "does not save the answer if the voter is invalid" do
allow_any_instance_of(Poll::Voter).to receive(:valid?).and_return(false)
answer = build(:poll_answer)
expect do
answer.save_and_record_voter_participation
end.to raise_error(ActiveRecord::RecordInvalid)
expect(answer).not_to be_persisted
end
it "does not create two voters when creating two answers at the same time", :race_condition do
answer = build(:poll_answer, question: question, author: author, answer: "Yes")
other_answer = build(:poll_answer, question: question, author: author, answer: "No")
[answer, other_answer].map do |poll_answer|
Thread.new do
begin
poll_answer.save_and_record_voter_participation
rescue ActiveRecord::RecordInvalid
end
end
end.each(&:join)
expect(Poll::Voter.count).to be 1
end
it "does not create two voters when calling the method twice at the same time", :race_condition do
answer = create(:poll_answer, question: question, author: author, answer: "Yes")
2.times.map do
Thread.new { answer.save_and_record_voter_participation }
end.each(&:join)
expect(Poll::Voter.count).to be 1
end
end
describe "#destroy_and_remove_voter_participation" do
let(:poll) { create(:poll) }
let(:question) { create(:poll_question, :yes_no, poll: poll) }
it "destroys voter record and answer when it was the only user's answer" do
answer = build(:poll_answer, question: question)
answer.save_and_record_voter_participation
expect { answer.destroy_and_remove_voter_participation }
.to change { Poll::Answer.count }.by(-1)
.and change { Poll::Voter.count }.by(-1)
end
it "destroys the answer but does not destroy the voter record when the user
has answered other poll questions" do
answer = build(:poll_answer, question: question)
answer.save_and_record_voter_participation
other_question = create(:poll_question, :yes_no, poll: poll)
other_answer = build(:poll_answer, question: other_question, author: answer.author)
other_answer.save_and_record_voter_participation
expect(other_answer).to be_persisted
expect { answer.destroy_and_remove_voter_participation }
.to change { Poll::Answer.count }.by(-1)
.and change { Poll::Voter.count }.by(0)
end
end
end