diff --git a/CHANGELOG.md b/CHANGELOG.md index bd88843..b4f2e4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### 4.1.1 (Next) +* [#239](https://github.com/mongoid/mongoid-rspec/pull/239): Add support for if/unless validator options - [@knovoselic](https://github.com/knovoselic). * Your contribution here. ### 4.1.0 (6/12/2020) diff --git a/README.md b/README.md index ffd5514..d7b9c5d 100644 --- a/README.md +++ b/README.md @@ -315,6 +315,22 @@ RSpec.describe Person do # should redefine the kind method to return :custom, i.e. "def self.kind() :custom end" it { is_expected.to custom_validate(:ssn).with_validator(SsnValidator) } end + +# If you're using validators with if/unless conditionals, spec subject must be object instance +# This is supported on Mongoid 4 and newer +Rspec.describe User do + context 'when user has `admin` role' do + subject { User.new(role: 'admin') } + + it { is_expected.to validate_length_of(:password).greater_than(20) } + end + + context 'when user does not have `admin` role' do + subject { User.new(role: 'member') } + + it { is_expected.not_to validate_length_of(:password) } + end +end ``` ### Mass Assignment Matcher diff --git a/lib/matchers/validations.rb b/lib/matchers/validations.rb index e3959fb..9a0a334 100644 --- a/lib/matchers/validations.rb +++ b/lib/matchers/validations.rb @@ -12,7 +12,8 @@ def matches?(actual) @klass = actual.is_a?(Class) ? actual : actual.class @validator = @klass.validators_on(@field).detect do |v| - (v.kind.to_s == @type) && (!v.options[:on] || on_options_matches?(v)) + (v.kind.to_s == @type) && (!v.options[:on] || on_options_matches?(v)) && + if_condition_matches?(actual, v) && unless_condition_matches?(actual, v) end if @validator @@ -59,6 +60,31 @@ def with_message(message) private + def if_condition_matches?(actual, validator) + return true unless validator.options[:if] + + check_condition actual, validator.options[:if] + end + + def unless_condition_matches?(actual, validator) + return true unless validator.options[:unless] + + !check_condition actual, validator.options[:unless] + end + + def check_condition(actual, filter) + raise ArgumentError, 'Spec subject must be object instance when testing validators with if/unless condition.' if actual.is_a?(Class) + + case filter + when Symbol + actual.send filter + when ::Proc + actual.instance_exec(&filter) + else + raise ArgumentError, "Unexpected filter: #{filter.inspect}" + end + end + def check_on validator_on_methods = [@validator.options[:on]].flatten diff --git a/spec/models/article.rb b/spec/models/article.rb index b2959ac..ee64dd1 100644 --- a/spec/models/article.rb +++ b/spec/models/article.rb @@ -9,6 +9,7 @@ class Article field :number_of_comments, type: Integer field :status, type: Symbol field :deletion_date, type: DateTime, default: nil + field :reviewer, type: String, default: nil embeds_many :comments, cascade_callbacks: true, inverse_of: :article embeds_one :permalink, inverse_of: :linkable @@ -24,6 +25,10 @@ class Article validates_absence_of :deletion_date if Mongoid::Compatibility::Version.mongoid4_or_newer? + validates_presence_of :reviewer, unless: -> { status == :pending } + + validates_absence_of :comments, unless: :allow_comments if Mongoid::Compatibility::Version.mongoid4_or_newer? + index({ title: 1 }, unique: true, background: true, drop_dups: true) index(published: 1) index('permalink._id' => 1) diff --git a/spec/models/user.rb b/spec/models/user.rb index 101991e..670c261 100644 --- a/spec/models/user.rb +++ b/spec/models/user.rb @@ -30,9 +30,17 @@ class User validates :provider_uid, presence: true validates :locale, inclusion: { in: ->(_user) { %i[en ru] } } + with_options if: :admin? do + validates :password, length: { minimum: 20 } + end + + with_options if: -> { role == 'moderator' } do + validates :password, length: { minimum: 10 } + end + accepts_nested_attributes_for :articles, :comments def admin? - false + role == 'admin' end end diff --git a/spec/unit/validations_spec.rb b/spec/unit/validations_spec.rb index 79f2cd3..e0648ea 100644 --- a/spec/unit/validations_spec.rb +++ b/spec/unit/validations_spec.rb @@ -59,3 +59,63 @@ it { is_expected.to validate_format_of(:to).with_message('format') } end end + +if Mongoid::Compatibility::Version.mongoid4_or_newer? + RSpec.describe 'Conditional validations' do + describe 'validations with if condition using symbol' do + context 'when the condition is met' do + subject { User.new(role: 'admin') } + + it { is_expected.to validate_length_of(:password).greater_than(20) } + end + + context 'when the condition is not met' do + subject { User.new(role: 'member') } + + it { is_expected.not_to validate_length_of(:password) } + end + end + + describe 'validations with if condition using lambda' do + context 'when the condition is met' do + subject { User.new(role: 'moderator') } + + it { is_expected.to validate_length_of(:password).greater_than(10) } + end + + context 'when the condition is not met' do + subject { User.new(role: 'member') } + + it { is_expected.not_to validate_length_of(:password) } + end + end + + describe 'validations with unless condition using symbol' do + context 'when the condition is met' do + subject { Article.new(allow_comments: false) } + + it { is_expected.to validate_absence_of(:comments) } + end + + context 'when the condition is not met' do + subject { Article.new(allow_comments: true) } + + it { is_expected.not_to validate_absence_of(:comments) } + end + end + + describe 'validations with unless condition using lambda' do + context 'when the condition is met' do + subject { Article.new(status: :rejected) } + + it { is_expected.to validate_presence_of(:reviewer) } + end + + context 'when the condition is not met' do + subject { Article.new(status: :pending) } + + it { is_expected.not_to validate_presence_of(:reviewer) } + end + end + end +end