Fix polls not being validated on edition (#33755)

stable-4.3
Claire 2025-01-28 15:38:18 +01:00
parent 227d48dbd5
commit 2b148d3e88
8 changed files with 82 additions and 21 deletions

View File

@ -37,7 +37,8 @@ class Poll < ApplicationRecord
validates :options, presence: true validates :options, presence: true
validates :expires_at, presence: true, if: :local? validates :expires_at, presence: true, if: :local?
validates_with PollValidator, on: :create, if: :local? validates_with PollOptionsValidator, if: :local?
validates_with PollExpirationValidator, if: -> { local? && expires_at_changed? }
scope :attached, -> { where.not(status_id: nil) } scope :attached, -> { where.not(status_id: nil) }
scope :unattached, -> { where(status_id: nil) } scope :unattached, -> { where(status_id: nil) }

View File

@ -86,10 +86,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
}, },
polls: { polls: {
max_options: PollValidator::MAX_OPTIONS, max_options: PollOptionsValidator::MAX_OPTIONS,
max_characters_per_option: PollValidator::MAX_OPTION_CHARS, max_characters_per_option: PollOptionsValidator::MAX_OPTION_CHARS,
min_expiration: PollValidator::MIN_EXPIRATION, min_expiration: PollExpirationValidator::MIN_EXPIRATION,
max_expiration: PollValidator::MAX_EXPIRATION, max_expiration: PollExpirationValidator::MAX_EXPIRATION,
}, },
translation: { translation: {

View File

@ -78,10 +78,10 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
}, },
polls: { polls: {
max_options: PollValidator::MAX_OPTIONS, max_options: PollOptionsValidator::MAX_OPTIONS,
max_characters_per_option: PollValidator::MAX_OPTION_CHARS, max_characters_per_option: PollOptionsValidator::MAX_OPTION_CHARS,
min_expiration: PollValidator::MIN_EXPIRATION, min_expiration: PollExpirationValidator::MIN_EXPIRATION,
max_expiration: PollValidator::MAX_EXPIRATION, max_expiration: PollExpirationValidator::MAX_EXPIRATION,
}, },
} }
end end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class PollExpirationValidator < ActiveModel::Validator
MAX_EXPIRATION = 1.month.freeze
MIN_EXPIRATION = 5.minutes.freeze
def validate(poll)
current_time = Time.now.utc
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && (poll.expires_at - current_time).ceil < MIN_EXPIRATION
end
end

View File

@ -1,19 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class PollValidator < ActiveModel::Validator class PollOptionsValidator < ActiveModel::Validator
MAX_OPTIONS = 4 MAX_OPTIONS = 4
MAX_OPTION_CHARS = 50 MAX_OPTION_CHARS = 50
MAX_EXPIRATION = 1.month.freeze
MIN_EXPIRATION = 5.minutes.freeze
def validate(poll) def validate(poll)
current_time = Time.now.utc
poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1 poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1
poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS
poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS } poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS }
poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && (poll.expires_at - current_time).ceil < MIN_EXPIRATION
end end
end end

View File

@ -56,7 +56,7 @@ RSpec.describe 'Instances' do
max_media_attachments: Status::MEDIA_ATTACHMENTS_LIMIT max_media_attachments: Status::MEDIA_ATTACHMENTS_LIMIT
), ),
polls: include( polls: include(
max_options: PollValidator::MAX_OPTIONS max_options: PollOptionsValidator::MAX_OPTIONS
) )
) )
) )

View File

@ -2,7 +2,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe PollValidator do RSpec.describe PollExpirationValidator do
describe '#validate' do describe '#validate' do
before do before do
validator.validate(poll) validator.validate(poll)
@ -14,16 +14,24 @@ RSpec.describe PollValidator do
let(:options) { %w(foo bar) } let(:options) { %w(foo bar) }
let(:expires_at) { 1.day.from_now } let(:expires_at) { 1.day.from_now }
it 'have no errors' do it 'has no errors' do
expect(errors).to_not have_received(:add) expect(errors).to_not have_received(:add)
end end
context 'when expires is just 5 min ago' do context 'when the poll expires in 5 min from now' do
let(:expires_at) { 5.minutes.from_now } let(:expires_at) { 5.minutes.from_now }
it 'not calls errors add' do it 'has no errors' do
expect(errors).to_not have_received(:add) expect(errors).to_not have_received(:add)
end end
end end
context 'when the poll expires in the past' do
let(:expires_at) { 5.minutes.ago }
it 'has errors' do
expect(errors).to have_received(:add)
end
end
end end
end end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe PollOptionsValidator do
describe '#validate' do
before do
validator.validate(poll)
end
let(:validator) { described_class.new }
let(:poll) { instance_double(Poll, options: options, expires_at: expires_at, errors: errors) }
let(:errors) { instance_double(ActiveModel::Errors, add: nil) }
let(:options) { %w(foo bar) }
let(:expires_at) { 1.day.from_now }
it 'has no errors' do
expect(errors).to_not have_received(:add)
end
context 'when the poll has duplicate options' do
let(:options) { %w(foo foo) }
it 'adds errors' do
expect(errors).to have_received(:add)
end
end
context 'when the poll has no options' do
let(:options) { [] }
it 'adds errors' do
expect(errors).to have_received(:add)
end
end
context 'when the poll has too many options' do
let(:options) { Array.new(described_class::MAX_OPTIONS + 1) { |i| "option #{i}" } }
it 'adds errors' do
expect(errors).to have_received(:add)
end
end
end
end