diff --git a/CHANGELOG.md b/CHANGELOG.md index 8886a7009..88a1a27b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [#2656](https://github.com/ruby-grape/grape/pull/2656): Remove useless instance_variable_defined? checks - [@ericproulx](https://github.com/ericproulx). * [#2619](https://github.com/ruby-grape/grape/pull/2619): Remove TOC from README.md and danger-toc check - [@alexanderadam](https://github.com/alexanderadam). * [#2663](https://github.com/ruby-grape/grape/pull/2663): Refactor `ParamsScope` and `Parameters` DSL to use named kwargs - [@ericproulx](https://github.com/ericproulx). +* [#2664](https://github.com/ruby-grape/grape/pull/2664): Drop `test-prof` dependency - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index d7f25adf8..763727412 100644 --- a/Gemfile +++ b/Gemfile @@ -31,7 +31,6 @@ group :test do gem 'rspec', '~> 3.13' gem 'simplecov', '~> 0.21', require: false gem 'simplecov-lcov', '~> 0.8', require: false - gem 'test-prof', require: false end platforms :jruby do diff --git a/spec/config/spec_test_prof.rb b/spec/config/spec_test_prof.rb deleted file mode 100644 index e5259e110..000000000 --- a/spec/config/spec_test_prof.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'test_prof/recipes/rspec/let_it_be' - -TestProf::BeforeAll.adapter = Class.new do - def begin_transaction; end - - def rollback_transaction; end -end.new diff --git a/spec/grape/validations/validators/all_or_none_validator_spec.rb b/spec/grape/validations/validators/all_or_none_validator_spec.rb index 9c8fe78b1..df491f706 100644 --- a/spec/grape/validations/validators/all_or_none_validator_spec.rb +++ b/spec/grape/validations/validators/all_or_none_validator_spec.rb @@ -1,101 +1,100 @@ # frozen_string_literal: true describe Grape::Validations::Validators::AllOrNoneOfValidator do - let_it_be(:app) do - Class.new(Grape::API) do - rescue_from Grape::Exceptions::ValidationErrors do |e| - error!(e.errors.transform_keys! { |key| key.join(',') }, 400) - end + describe '#validate!' do + subject(:validate) { post path, params } - params do - optional :beer, :wine, type: Grape::API::Boolean - all_or_none_of :beer, :wine - end - post do - end + describe '/' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - optional :beer, :wine, :other, type: Grape::API::Boolean - all_or_none_of :beer, :wine - end - post 'mixed-params' do + params do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine + end + post do + end + end end - params do - optional :beer, :wine, type: Grape::API::Boolean - all_or_none_of :beer, :wine, message: 'choose all or none' - end - post '/custom-message' do - end + context 'when all restricted params are present' do + let(:path) { '/' } + let(:params) { { beer: true, wine: true } } - params do - requires :item, type: Hash do - optional :beer, :wine, type: Grape::API::Boolean - all_or_none_of :beer, :wine + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end - post '/nested-hash' do - end - params do - requires :items, type: Array do - optional :beer, :wine, type: Grape::API::Boolean - all_or_none_of :beer, :wine + context 'when a subset of restricted params are present' do + let(:path) { '/' } + let(:params) { { beer: true } } + + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,wine' => ['provide all or none of parameters'] + ) end end - post '/nested-array' do + + context 'when no restricted params are present' do + let(:path) { '/' } + let(:params) { { somethingelse: true } } + + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 + end end + end - params do - requires :items, type: Array do - requires :nested_items, type: Array do - optional :beer, :wine, type: Grape::API::Boolean + describe '/mixed-params' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :beer, :wine, :other, type: Grape::API::Boolean all_or_none_of :beer, :wine end + post 'mixed-params' do + end end end - post '/deeply-nested-array' do - end - end - end - describe '#validate!' do - subject(:validate) { post path, params } - - context 'when all restricted params are present' do - let(:path) { '/' } - let(:params) { { beer: true, wine: true } } + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, other: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end - - context 'mixed with other params' do - let(:path) { '/mixed-params' } - let(:params) { { beer: true, wine: true, other: true } } - - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 - end - end end - context 'when a subset of restricted params are present' do - let(:path) { '/' } - let(:params) { { beer: true } } + describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,wine' => ['provide all or none of parameters'] - ) + params do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine, message: 'choose all or none' + end + post '/custom-message' do + end + end end - end - context 'when custom message is specified' do let(:path) { '/custom-message' } let(:params) { { beer: true } } @@ -108,17 +107,24 @@ end end - context 'when no restricted params are present' do - let(:path) { '/' } - let(:params) { { somethingelse: true } } + describe '/nested-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 + params do + requires :item, type: Hash do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine + end + end + post '/nested-hash' do + end + end end - end - context 'when restricted params are nested inside required hash' do let(:path) { '/nested-hash' } let(:params) { { item: { beer: true } } } @@ -131,7 +137,24 @@ end end - context 'when mutually exclusive params are nested inside array' do + describe '/nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine + end + end + post '/nested-array' do + end + end + end + let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true }] } } @@ -144,7 +167,26 @@ end end - context 'when mutually exclusive params are deeply nested' do + describe '/deeply-nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine + end + end + end + post '/deeply-nested-array' do + end + end + end + let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true }] }] } } diff --git a/spec/grape/validations/validators/allow_blank_validator_spec.rb b/spec/grape/validations/validators/allow_blank_validator_spec.rb index 88c527508..68eb7782f 100644 --- a/spec/grape/validations/validators/allow_blank_validator_spec.rb +++ b/spec/grape/validations/validators/allow_blank_validator_spec.rb @@ -1,145 +1,109 @@ # frozen_string_literal: true describe Grape::Validations::Validators::AllowBlankValidator do - let_it_be(:app) do - Class.new(Grape::API) do - default_format :json - - params do - requires :name, allow_blank: false - end - get '/disallow_blank' - - params do - optional :name, type: String, allow_blank: false - end - get '/opt_disallow_string_blank' - - params do - optional :name, allow_blank: false - end - get '/disallow_blank_optional_param' - - params do - requires :name, allow_blank: true - end - get '/allow_blank' - - params do - requires :val, type: DateTime, allow_blank: true - end - get '/allow_datetime_blank' - - params do - requires :val, type: DateTime, allow_blank: false - end - get '/disallow_datetime_blank' - - params do - requires :val, type: DateTime - end - get '/default_allow_datetime_blank' - - params do - requires :val, type: Date, allow_blank: true - end - get '/allow_date_blank' + describe 'bad encoding' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - requires :val, type: Integer, allow_blank: true + params do + requires :name, type: String, allow_blank: false + end + get '/bad_encoding' end - get '/allow_integer_blank' + end - params do - requires :val, type: Float, allow_blank: true + context 'when value has bad encoding' do + it 'does not raise an error' do + expect { get('/bad_encoding', { name: "Hello \x80" }) }.not_to raise_error end - get '/allow_float_blank' + end + end - params do - requires :val, type: Integer, allow_blank: true - end - get '/allow_integer_blank' + describe '/disallow_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - requires :val, type: Symbol, allow_blank: true + params do + requires :name, allow_blank: false + end + get '/disallow_blank' end - get '/allow_symbol_blank' + end - params do - requires :val, type: Grape::API::Boolean, allow_blank: true - end - get '/allow_boolean_blank' + it 'refuses empty string' do + get '/disallow_blank', name: '' + expect(last_response.status).to eq(400) + end - params do - requires :val, type: Grape::API::Boolean, allow_blank: false - end - get '/disallow_boolean_blank' + it 'refuses only whitespaces' do + get '/disallow_blank', name: ' ' + expect(last_response.status).to eq(400) - params do - optional :user, type: Hash do - requires :name, allow_blank: false - end - end - get '/disallow_blank_required_param_in_an_optional_group' + get '/disallow_blank', name: " \n " + expect(last_response.status).to eq(400) - params do - optional :user, type: Hash do - requires :name, type: Date, allow_blank: true - end - end - get '/allow_blank_date_param_in_an_optional_group' + get '/disallow_blank', name: "\n" + expect(last_response.status).to eq(400) + end - params do - optional :user, type: Hash do - optional :name, allow_blank: false - requires :age - end - end - get '/disallow_blank_optional_param_in_an_optional_group' + it 'refuses nil' do + get '/disallow_blank', name: nil + expect(last_response.status).to eq(400) + end - params do - requires :user, type: Hash do - requires :name, allow_blank: false - end - end - get '/disallow_blank_required_param_in_a_required_group' + it 'refuses missing' do + get '/disallow_blank' + expect(last_response.status).to eq(400) + end - params do - requires :user, type: Hash do - requires :name, allow_blank: false - end - end - get '/disallow_string_value_in_a_required_hash_group' + it 'accepts valid input' do + get '/disallow_blank', name: 'bob' + expect(last_response.status).to eq(200) + end + end - params do - requires :user, type: Hash do - optional :name, allow_blank: false - end - end - get '/disallow_blank_optional_param_in_a_required_group' + describe '/opt_disallow_string_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :user, type: Hash do - optional :name, allow_blank: false + params do + optional :name, type: String, allow_blank: false end + get '/opt_disallow_string_blank' end - get '/disallow_string_value_in_an_optional_hash_group' + end - resources :custom_message do - params do - requires :name, allow_blank: { value: false, message: 'has no value' } - end - get + it 'allows missing optional strings' do + get 'opt_disallow_string_blank' + expect(last_response.status).to eq(200) + end + end - params do - optional :name, allow_blank: { value: false, message: 'has no value' } - end - get '/disallow_blank_optional_param' + describe '/allow_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do requires :name, allow_blank: true end get '/allow_blank' + end + end + + it 'accepts empty input when allow_blank is true' do + get '/allow_blank', name: '' + expect(last_response.status).to eq(200) + end + end + + describe 'type-specific blanks' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do requires :val, type: DateTime, allow_blank: true @@ -147,7 +111,7 @@ get '/allow_datetime_blank' params do - requires :val, type: DateTime, allow_blank: { value: false, message: 'has no value' } + requires :val, type: DateTime, allow_blank: false end get '/disallow_datetime_blank' @@ -171,11 +135,6 @@ end get '/allow_float_blank' - params do - requires :val, type: Integer, allow_blank: true - end - get '/allow_integer_blank' - params do requires :val, type: Symbol, allow_blank: true end @@ -187,13 +146,73 @@ get '/allow_boolean_blank' params do - requires :val, type: Grape::API::Boolean, allow_blank: { value: false, message: 'has no value' } + requires :val, type: Grape::API::Boolean, allow_blank: false end get '/disallow_boolean_blank' + end + end + + it 'refuses empty string for disallow_datetime_blank' do + get '/disallow_datetime_blank', val: '' + expect(last_response.status).to eq(400) + end + + it 'accepts value when time allow_blank' do + get '/disallow_datetime_blank', val: Time.now + expect(last_response.status).to eq(200) + end + + it 'accepts empty when datetime allow_blank' do + get '/allow_datetime_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty input' do + get '/default_allow_datetime_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when date allow_blank' do + get '/allow_date_blank', val: '' + expect(last_response.status).to eq(200) + end + + context 'allow_blank when Numeric' do + it 'accepts empty when integer allow_blank' do + get '/allow_integer_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when float allow_blank' do + get '/allow_float_blank', val: '' + expect(last_response.status).to eq(200) + end + end + + it 'accepts empty when symbol allow_blank' do + get '/allow_symbol_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when boolean allow_blank' do + get '/allow_boolean_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts false when boolean allow_blank' do + get '/disallow_boolean_blank', val: false + expect(last_response.status).to eq(200) + end + end + + describe 'in an optional group' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do optional :user, type: Hash do - requires :name, allow_blank: { value: false, message: 'has no value' } + requires :name, allow_blank: false end end get '/disallow_blank_required_param_in_an_optional_group' @@ -207,102 +226,129 @@ params do optional :user, type: Hash do - optional :name, allow_blank: { value: false, message: 'has no value' } + optional :name, allow_blank: false requires :age end end get '/disallow_blank_optional_param_in_an_optional_group' + end + end + + context 'as a required param' do + it 'accepts a missing group, even with a disallwed blank param' do + get '/disallow_blank_required_param_in_an_optional_group' + expect(last_response.status).to eq(200) + end + + it 'accepts a nested missing date value' do + get '/allow_blank_date_param_in_an_optional_group', user: { name: '' } + expect(last_response.status).to eq(200) + end + + it 'refuses a blank value in an existing group' do + get '/disallow_blank_required_param_in_an_optional_group', user: { name: '' } + expect(last_response.status).to eq(400) + end + end + + context 'as an optional param' do + it 'accepts a missing group, even with a disallwed blank param' do + get '/disallow_blank_optional_param_in_an_optional_group' + expect(last_response.status).to eq(200) + end + + it 'accepts a nested missing optional value' do + get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' } + expect(last_response.status).to eq(200) + end + + it 'refuses a blank existing value in an existing scope' do + get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' } + expect(last_response.status).to eq(400) + end + end + end + + describe 'in a required group' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do requires :user, type: Hash do - requires :name, allow_blank: { value: false, message: 'has no value' } + requires :name, allow_blank: false end end get '/disallow_blank_required_param_in_a_required_group' params do requires :user, type: Hash do - requires :name, allow_blank: { value: false, message: 'has no value' } + requires :name, allow_blank: false end end get '/disallow_string_value_in_a_required_hash_group' params do requires :user, type: Hash do - optional :name, allow_blank: { value: false, message: 'has no value' } + optional :name, allow_blank: false end end get '/disallow_blank_optional_param_in_a_required_group' params do optional :user, type: Hash do - optional :name, allow_blank: { value: false, message: 'has no value' } + optional :name, allow_blank: false end end get '/disallow_string_value_in_an_optional_hash_group' end end - end - describe 'bad encoding' do - let(:app) do - Class.new(Grape::API) do - default_format :json - - params do - requires :name, type: String, allow_blank: false - end - get '/bad_encoding' + context 'as a required param' do + it 'refuses a blank value in a required existing group' do + get '/disallow_blank_required_param_in_a_required_group', user: { name: '' } + expect(last_response.status).to eq(400) end - end - context 'when value has bad encoding' do - it 'does not raise an error' do - expect { get('/bad_encoding', { name: "Hello \x80" }) }.not_to raise_error + it 'refuses a string value in a required hash group' do + get '/disallow_string_value_in_a_required_hash_group', user: '' + expect(last_response.status).to eq(400) end end - end - context 'invalid input' do - it 'refuses empty string' do - get '/disallow_blank', name: '' - expect(last_response.status).to eq(400) - - get '/disallow_datetime_blank', val: '' - expect(last_response.status).to eq(400) - end - - it 'refuses only whitespaces' do - get '/disallow_blank', name: ' ' - expect(last_response.status).to eq(400) - - get '/disallow_blank', name: " \n " - expect(last_response.status).to eq(400) - - get '/disallow_blank', name: "\n" - expect(last_response.status).to eq(400) - end + context 'as an optional param' do + it 'accepts a nested missing value' do + get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29' } + expect(last_response.status).to eq(200) + end - it 'refuses nil' do - get '/disallow_blank', name: nil - expect(last_response.status).to eq(400) - end + it 'refuses a blank existing value in an existing scope' do + get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' } + expect(last_response.status).to eq(400) + end - it 'refuses missing' do - get '/disallow_blank' - expect(last_response.status).to eq(400) + it 'refuses a string value in an optional hash group' do + get '/disallow_string_value_in_an_optional_hash_group', user: '' + expect(last_response.status).to eq(400) + end end end - context 'custom validation message' do - context 'with invalid input' do - it 'refuses empty string' do - get '/custom_message', name: '' - expect(last_response.body).to eq('{"error":"name has no value"}') + describe 'custom message' do + context 'GET /custom_message' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :name, allow_blank: { value: false, message: 'has no value' } + end + get '/custom_message' + end end - it 'refuses empty string for an optional param' do - get '/custom_message/disallow_blank_optional_param', name: '' + it 'refuses empty string' do + get '/custom_message', name: '' expect(last_response.body).to eq('{"error":"name has no value"}') end @@ -321,48 +367,150 @@ get '/custom_message', name: nil expect(last_response.body).to eq('{"error":"name has no value"}') end - end - context 'with valid input' do it 'accepts valid input' do get '/custom_message', name: 'bob' expect(last_response.status).to eq(200) end + end + + context 'GET /custom_message/disallow_blank_optional_param' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :name, allow_blank: { value: false, message: 'has no value' } + end + get '/custom_message/disallow_blank_optional_param' + end + end + + it 'refuses empty string for an optional param' do + get '/custom_message/disallow_blank_optional_param', name: '' + expect(last_response.body).to eq('{"error":"name has no value"}') + end + end + + context 'GET /custom_message/allow_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :name, allow_blank: true + end + get '/custom_message/allow_blank' + end + end - it 'accepts empty input when allow_blank is false' do + it 'accepts empty input when allow_blank is true' do get '/custom_message/allow_blank', name: '' expect(last_response.status).to eq(200) end + end - it 'accepts empty input' do - get '/custom_message/default_allow_datetime_blank', val: '' - expect(last_response.status).to eq(200) + context 'GET /custom_message/allow_datetime_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :val, type: DateTime, allow_blank: true + end + get '/custom_message/allow_datetime_blank' + end end it 'accepts empty when datetime allow_blank' do get '/custom_message/allow_datetime_blank', val: '' expect(last_response.status).to eq(200) end + end + + context 'GET /custom_message/default_allow_datetime_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :val, type: DateTime + end + get '/custom_message/default_allow_datetime_blank' + end + end + + it 'accepts empty input' do + get '/custom_message/default_allow_datetime_blank', val: '' + expect(last_response.status).to eq(200) + end + end + + context 'GET /custom_message/allow_date_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :val, type: Date, allow_blank: true + end + get '/custom_message/allow_date_blank' + end + end it 'accepts empty when date allow_blank' do get '/custom_message/allow_date_blank', val: '' expect(last_response.status).to eq(200) end + end + + context 'allow_blank when Numeric' do + context 'GET /custom_message/allow_integer_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :val, type: Integer, allow_blank: true + end + get '/custom_message/allow_integer_blank' + end + end - context 'allow_blank when Numeric' do it 'accepts empty when integer allow_blank' do get '/custom_message/allow_integer_blank', val: '' expect(last_response.status).to eq(200) end + end + + context 'GET /custom_message/allow_float_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :val, type: Float, allow_blank: true + end + get '/custom_message/allow_float_blank' + end + end it 'accepts empty when float allow_blank' do get '/custom_message/allow_float_blank', val: '' expect(last_response.status).to eq(200) end + end + end - it 'accepts empty when integer allow_blank' do - get '/custom_message/allow_integer_blank', val: '' - expect(last_response.status).to eq(200) + context 'GET /custom_message/allow_symbol_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :val, type: Symbol, allow_blank: true + end + get '/custom_message/allow_symbol_blank' end end @@ -370,27 +518,61 @@ get '/custom_message/allow_symbol_blank', val: '' expect(last_response.status).to eq(200) end + end + + context 'GET /custom_message/allow_boolean_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :val, type: Grape::API::Boolean, allow_blank: true + end + get '/custom_message/allow_boolean_blank' + end + end it 'accepts empty when boolean allow_blank' do get '/custom_message/allow_boolean_blank', val: '' expect(last_response.status).to eq(200) end + end + + context 'GET /custom_message/disallow_boolean_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :val, type: Grape::API::Boolean, allow_blank: { value: false, message: 'has no value' } + end + get '/custom_message/disallow_boolean_blank' + end + end - it 'accepts false when boolean allow_blank' do + it 'accepts false when boolean disallow_blank' do get '/custom_message/disallow_boolean_blank', val: false expect(last_response.status).to eq(200) end end context 'in an optional group' do - context 'as a required param' do - it 'accepts a missing group, even with a disallwed blank param' do - get '/custom_message/disallow_blank_required_param_in_an_optional_group' - expect(last_response.status).to eq(200) + context 'GET /custom_message/disallow_blank_required_param_in_an_optional_group' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :user, type: Hash do + requires :name, allow_blank: { value: false, message: 'has no value' } + end + end + get '/custom_message/disallow_blank_required_param_in_an_optional_group' + end end - it 'accepts a nested missing date value' do - get '/custom_message/allow_blank_date_param_in_an_optional_group', user: { name: '' } + it 'accepts a missing group, even with a disallwed blank param' do + get '/custom_message/disallow_blank_required_param_in_an_optional_group' expect(last_response.status).to eq(200) end @@ -401,7 +583,41 @@ end end - context 'as an optional param' do + context 'GET /custom_message/allow_blank_date_param_in_an_optional_group' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :user, type: Hash do + requires :name, type: Date, allow_blank: true + end + end + get '/custom_message/allow_blank_date_param_in_an_optional_group' + end + end + + it 'accepts a nested missing date value' do + get '/custom_message/allow_blank_date_param_in_an_optional_group', user: { name: '' } + expect(last_response.status).to eq(200) + end + end + + context 'GET /custom_message/disallow_blank_optional_param_in_an_optional_group' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :user, type: Hash do + optional :name, allow_blank: { value: false, message: 'has no value' } + requires :age + end + end + get '/custom_message/disallow_blank_optional_param_in_an_optional_group' + end + end + it 'accepts a missing group, even with a disallwed blank param' do get '/custom_message/disallow_blank_optional_param_in_an_optional_group' expect(last_response.status).to eq(200) @@ -421,12 +637,40 @@ end context 'in a required group' do - context 'as a required param' do + context 'GET /custom_message/disallow_blank_required_param_in_a_required_group' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :user, type: Hash do + requires :name, allow_blank: { value: false, message: 'has no value' } + end + end + get '/custom_message/disallow_blank_required_param_in_a_required_group' + end + end + it 'refuses a blank value in a required existing group' do get '/custom_message/disallow_blank_required_param_in_a_required_group', user: { name: '' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[name] has no value"}') end + end + + context 'GET /custom_message/disallow_string_value_in_a_required_hash_group' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :user, type: Hash do + requires :name, allow_blank: { value: false, message: 'has no value' } + end + end + get '/custom_message/disallow_string_value_in_a_required_hash_group' + end + end it 'refuses a string value in a required hash group' do get '/custom_message/disallow_string_value_in_a_required_hash_group', user: '' @@ -435,7 +679,20 @@ end end - context 'as an optional param' do + context 'GET /custom_message/disallow_blank_optional_param_in_a_required_group' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :user, type: Hash do + optional :name, allow_blank: { value: false, message: 'has no value' } + end + end + get '/custom_message/disallow_blank_optional_param_in_a_required_group' + end + end + it 'accepts a nested missing value' do get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29' } expect(last_response.status).to eq(200) @@ -446,6 +703,21 @@ expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[name] has no value"}') end + end + + context 'GET /custom_message/disallow_string_value_in_an_optional_hash_group' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :user, type: Hash do + optional :name, allow_blank: { value: false, message: 'has no value' } + end + end + get '/custom_message/disallow_string_value_in_an_optional_hash_group' + end + end it 'refuses a string value in an optional hash group' do get '/custom_message/disallow_string_value_in_an_optional_hash_group', user: '' @@ -455,140 +727,4 @@ end end end - - context 'valid input' do - it 'allows missing optional strings' do - get 'opt_disallow_string_blank' - expect(last_response.status).to eq(200) - end - - it 'accepts valid input' do - get '/disallow_blank', name: 'bob' - expect(last_response.status).to eq(200) - end - - it 'accepts empty input when allow_blank is false' do - get '/allow_blank', name: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty input' do - get '/default_allow_datetime_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty when datetime allow_blank' do - get '/allow_datetime_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty when date allow_blank' do - get '/allow_date_blank', val: '' - expect(last_response.status).to eq(200) - end - - context 'allow_blank when Numeric' do - it 'accepts empty when integer allow_blank' do - get '/allow_integer_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty when float allow_blank' do - get '/allow_float_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty when integer allow_blank' do - get '/allow_integer_blank', val: '' - expect(last_response.status).to eq(200) - end - end - - it 'accepts empty when symbol allow_blank' do - get '/allow_symbol_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty when boolean allow_blank' do - get '/allow_boolean_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts false when boolean allow_blank' do - get '/disallow_boolean_blank', val: false - expect(last_response.status).to eq(200) - end - - it 'accepts value when time allow_blank' do - get '/disallow_datetime_blank', val: Time.now - expect(last_response.status).to eq(200) - end - end - - context 'in an optional group' do - context 'as a required param' do - it 'accepts a missing group, even with a disallwed blank param' do - get '/disallow_blank_required_param_in_an_optional_group' - expect(last_response.status).to eq(200) - end - - it 'accepts a nested missing date value' do - get '/allow_blank_date_param_in_an_optional_group', user: { name: '' } - expect(last_response.status).to eq(200) - end - - it 'refuses a blank value in an existing group' do - get '/disallow_blank_required_param_in_an_optional_group', user: { name: '' } - expect(last_response.status).to eq(400) - end - end - - context 'as an optional param' do - it 'accepts a missing group, even with a disallwed blank param' do - get '/disallow_blank_optional_param_in_an_optional_group' - expect(last_response.status).to eq(200) - end - - it 'accepts a nested missing optional value' do - get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' } - expect(last_response.status).to eq(200) - end - - it 'refuses a blank existing value in an existing scope' do - get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' } - expect(last_response.status).to eq(400) - end - end - end - - context 'in a required group' do - context 'as a required param' do - it 'refuses a blank value in a required existing group' do - get '/disallow_blank_required_param_in_a_required_group', user: { name: '' } - expect(last_response.status).to eq(400) - end - - it 'refuses a string value in a required hash group' do - get '/disallow_string_value_in_a_required_hash_group', user: '' - expect(last_response.status).to eq(400) - end - end - - context 'as an optional param' do - it 'accepts a nested missing value' do - get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29' } - expect(last_response.status).to eq(200) - end - - it 'refuses a blank existing value in an existing scope' do - get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' } - expect(last_response.status).to eq(400) - end - - it 'refuses a string value in an optional hash group' do - get '/disallow_string_value_in_an_optional_hash_group', user: '' - expect(last_response.status).to eq(400) - end - end - end end diff --git a/spec/grape/validations/validators/at_least_one_of_validator_spec.rb b/spec/grape/validations/validators/at_least_one_of_validator_spec.rb index 4b0e3b0c7..a75ecae25 100644 --- a/spec/grape/validations/validators/at_least_one_of_validator_spec.rb +++ b/spec/grape/validations/validators/at_least_one_of_validator_spec.rb @@ -1,79 +1,61 @@ # frozen_string_literal: true describe Grape::Validations::Validators::AtLeastOneOfValidator do - let_it_be(:app) do - Class.new(Grape::API) do - rescue_from Grape::Exceptions::ValidationErrors do |e| - error!(e.errors.transform_keys! { |key| key.join(',') }, 400) - end + describe '#validate!' do + subject(:validate) { post path, params } - params do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit - end - post do - end + describe '/' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - optional :beer, :wine, :grapefruit, :other - at_least_one_of :beer, :wine, :grapefruit - end - post 'mixed-params' do + params do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit + end + post do + end + end end - params do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit, message: 'you should choose something' - end - post '/custom-message' do - end + context 'when all restricted params are present' do + let(:path) { '/' } + let(:params) { { beer: true, wine: true, grapefruit: true } } - params do - requires :item, type: Hash do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit, message: 'fail' + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end - post '/nested-hash' do - end - params do - requires :items, type: Array do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit, message: 'fail' - end - end - post '/nested-array' do - end + context 'when a subset of restricted params are present' do + let(:path) { '/' } + let(:params) { { beer: true, grapefruit: true } } - params do - requires :items, type: Array do - requires :nested_items, type: Array do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit, message: 'fail' - end + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end - post '/deeply-nested-array' do - end - end - end - describe '#validate!' do - subject(:validate) { post path, params } - - context 'when all restricted params are present' do - let(:path) { '/' } - let(:params) { { beer: true, wine: true, grapefruit: true } } + context 'when none of the restricted params is selected' do + let(:path) { '/' } + let(:params) { { other: true } } - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,wine,grapefruit' => ['are missing, at least one parameter must be provided'] + ) + end end - context 'mixed with other params' do - let(:path) { '/mixed-params' } - let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } + context 'when exactly one of the restricted params is selected' do + let(:path) { '/' } + let(:params) { { beer: true } } it 'does not return a validation error' do validate @@ -82,9 +64,24 @@ end end - context 'when a subset of restricted params are present' do - let(:path) { '/' } - let(:params) { { beer: true, grapefruit: true } } + describe '/mixed-params' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :beer, :wine, :grapefruit, :other + at_least_one_of :beer, :wine, :grapefruit + end + post 'mixed-params' do + end + end + end + + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'does not return a validation error' do validate @@ -92,42 +89,52 @@ end end - context 'when none of the restricted params is selected' do - let(:path) { '/' } + describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit, message: 'you should choose something' + end + post '/custom-message' do + end + end + end + + let(:path) { '/custom-message' } let(:params) { { other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['are missing, at least one parameter must be provided'] + 'beer,wine,grapefruit' => ['you should choose something'] ) end - - context 'when custom message is specified' do - let(:path) { '/custom-message' } - - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['you should choose something'] - ) - end - end end - context 'when exactly one of the restricted params is selected' do - let(:path) { '/' } - let(:params) { { beer: true } } + describe '/nested-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 + params do + requires :item, type: Hash do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit, message: 'fail' + end + end + post '/nested-hash' do + end + end end - end - context 'when restricted params are nested inside hash' do let(:path) { '/nested-hash' } context 'when at least one of them is present' do @@ -152,7 +159,24 @@ end end - context 'when restricted params are nested inside array' do + describe '/nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit, message: 'fail' + end + end + post '/nested-array' do + end + end + end + let(:path) { '/nested-array' } context 'when at least one of them is present' do @@ -177,7 +201,26 @@ end end - context 'when restricted params are deeply nested' do + describe '/deeply-nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit, message: 'fail' + end + end + end + post '/deeply-nested-array' do + end + end + end + let(:path) { '/deeply-nested-array' } context 'when at least one of them is present' do diff --git a/spec/grape/validations/validators/default_validator_spec.rb b/spec/grape/validations/validators/default_validator_spec.rb index df84f06ee..92e9066b7 100644 --- a/spec/grape/validations/validators/default_validator_spec.rb +++ b/spec/grape/validations/validators/default_validator_spec.rb @@ -1,191 +1,270 @@ # frozen_string_literal: true describe Grape::Validations::Validators::DefaultValidator do - let_it_be(:app) do - Class.new(Grape::API) do - default_format :json + describe '/' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :id - optional :type, default: 'default-type' - end - get '/' do - { id: params[:id], type: params[:type] } + params do + optional :id + optional :type, default: 'default-type' + end + get '/' do + { id: params[:id], type: params[:type] } + end end + end - params do - optional :type1, default: 'default-type1' - optional :type2, default: 'default-type2' - end - get '/user' do - { type1: params[:type1], type2: params[:type2] } - end + it 'set default value for optional param' do + get('/') + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ id: nil, type: 'default-type' }.to_json) + end + end - params do - requires :id - optional :type1, default: 'default-type1' - optional :type2, default: 'default-type2' - end + describe '/user' do + let(:app) do + Class.new(Grape::API) do + default_format :json - get '/message' do - { id: params[:id], type1: params[:type1], type2: params[:type2] } + params do + optional :type1, default: 'default-type1' + optional :type2, default: 'default-type2' + end + get '/user' do + { type1: params[:type1], type2: params[:type2] } + end end + end - params do - optional :random, default: -> { Random.rand } - optional :not_random, default: Random.rand - end - get '/numbers' do - { random_number: params[:random], non_random_number: params[:non_random_number] } - end + it 'set default values for optional params' do + get('/user') + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ type1: 'default-type1', type2: 'default-type2' }.to_json) + end + + it 'set default values for missing params in the request' do + get('/user?type2=value2') + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ type1: 'default-type1', type2: 'value2' }.to_json) + end + end + + describe '/message' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :array, type: Array do - requires :name - optional :with_default, default: 'default' + params do + requires :id + optional :type1, default: 'default-type1' + optional :type2, default: 'default-type2' end - end - get '/array' do - { array: params[:array] } - end - params do - requires :thing1 - optional :more_things, type: Array do - requires :nested_thing - requires :other_thing, default: 1 + get '/message' do + { id: params[:id], type1: params[:type1], type2: params[:type2] } end end - get '/optional_array' do - { thing1: params[:thing1] } + end + + it 'set default values for optional params and allow to use required fields in the same time' do + get('/message?id=1') + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ id: '1', type1: 'default-type1', type2: 'default-type2' }.to_json) + end + end + + describe '/numbers' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :random, default: -> { Random.rand } + optional :not_random, default: Random.rand + end + get '/numbers' do + { random_number: params[:random], non_random_number: params[:non_random_number] } + end end + end - params do - requires :root, type: Hash do - optional :some_things, type: Array do - requires :foo - optional :options, type: Array do - requires :name, type: String - requires :value, type: String - end + it 'sets lambda based defaults at the time of call' do + get('/numbers') + expect(last_response.status).to eq(200) + before = JSON.parse(last_response.body) + get('/numbers') + expect(last_response.status).to eq(200) + after = JSON.parse(last_response.body) + + expect(before['non_random_number']).to eq(after['non_random_number']) + expect(before['random_number']).not_to eq(after['random_number']) + end + end + + describe '/array' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :array, type: Array do + requires :name + optional :with_default, default: 'default' end end + get '/array' do + { array: params[:array] } + end end - get '/nested_optional_array' do - { root: params[:root] } - end + end - params do - requires :root, type: Hash do - optional :some_things, type: Array do - requires :foo - optional :options, type: Array do - optional :name, type: String - optional :value, type: String - end + it 'sets default values for grouped arrays' do + get('/array?array[][name]=name&array[][name]=name2&array[][with_default]=bar2') + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ array: [{ name: 'name', with_default: 'default' }, { name: 'name2', with_default: 'bar2' }] }.to_json) + end + end + + describe '/optional_array' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :thing1 + optional :more_things, type: Array do + requires :nested_thing + requires :other_thing, default: 1 end end + get '/optional_array' do + { thing1: params[:thing1] } + end end - get '/another_nested_optional_array' do - { root: params[:root] } - end + end - params do - requires :foo - optional :bar, default: ->(params) { params[:foo] } - optional :qux, default: ->(params) { params[:bar] } - end - get '/default_values_from_other_params' do - { - foo: params[:foo], - bar: params[:bar], - qux: params[:qux] - } - end + it 'lets you leave required values nested inside an optional blank' do + get '/optional_array', thing1: 'stuff' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ thing1: 'stuff' }.to_json) end end - it 'lets you leave required values nested inside an optional blank' do - get '/optional_array', thing1: 'stuff' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ thing1: 'stuff' }.to_json) - end + describe '/nested_optional_array' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'allows optional arrays to be omitted' do - params = { some_things: - [{ foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, - { foo: 'two' }, - { foo: 'three', options: [{ name: 'wooop', value: 'yap' }] }] } - get '/nested_optional_array', root: params - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ root: params }.to_json) - end + params do + requires :root, type: Hash do + optional :some_things, type: Array do + requires :foo + optional :options, type: Array do + requires :name, type: String + requires :value, type: String + end + end + end + end + get '/nested_optional_array' do + { root: params[:root] } + end + end + end - it 'does not allows faulty optional arrays' do - params = { some_things: - [ - { foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, - { foo: 'two', options: [{ name: 'wat' }] }, - { foo: 'three' } - ] } - error = { error: 'root[some_things][1][options][0][value] is missing' } - get '/nested_optional_array', root: params - expect(last_response.status).to eq(400) - expect(last_response.body).to eq(error.to_json) - end + it 'allows optional arrays to be omitted' do + params = { some_things: + [{ foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, + { foo: 'two' }, + { foo: 'three', options: [{ name: 'wooop', value: 'yap' }] }] } + get '/nested_optional_array', root: params + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ root: params }.to_json) + end - it 'allows optional arrays with optional params' do - params = { some_things: - [ - { foo: 'one', options: [{ value: 'nope' }] }, - { foo: 'two', options: [{ name: 'wat' }] }, - { foo: 'three' } - ] } - get '/another_nested_optional_array', root: params - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ root: params }.to_json) + it 'does not allows faulty optional arrays' do + params = { some_things: + [ + { foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, + { foo: 'two', options: [{ name: 'wat' }] }, + { foo: 'three' } + ] } + error = { error: 'root[some_things][1][options][0][value] is missing' } + get '/nested_optional_array', root: params + expect(last_response.status).to eq(400) + expect(last_response.body).to eq(error.to_json) + end end - it 'set default value for optional param' do - get('/') - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ id: nil, type: 'default-type' }.to_json) - end + describe '/another_nested_optional_array' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'set default values for optional params' do - get('/user') - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ type1: 'default-type1', type2: 'default-type2' }.to_json) - end + params do + requires :root, type: Hash do + optional :some_things, type: Array do + requires :foo + optional :options, type: Array do + optional :name, type: String + optional :value, type: String + end + end + end + end + get '/another_nested_optional_array' do + { root: params[:root] } + end + end + end - it 'set default values for missing params in the request' do - get('/user?type2=value2') - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ type1: 'default-type1', type2: 'value2' }.to_json) + it 'allows optional arrays with optional params' do + params = { some_things: + [ + { foo: 'one', options: [{ value: 'nope' }] }, + { foo: 'two', options: [{ name: 'wat' }] }, + { foo: 'three' } + ] } + get '/another_nested_optional_array', root: params + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ root: params }.to_json) + end end - it 'set default values for optional params and allow to use required fields in the same time' do - get('/message?id=1') - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ id: '1', type1: 'default-type1', type2: 'default-type2' }.to_json) - end + describe '/default_values_from_other_params' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'sets lambda based defaults at the time of call' do - get('/numbers') - expect(last_response.status).to eq(200) - before = JSON.parse(last_response.body) - get('/numbers') - expect(last_response.status).to eq(200) - after = JSON.parse(last_response.body) + params do + requires :foo + optional :bar, default: ->(params) { params[:foo] } + optional :qux, default: ->(params) { params[:bar] } + end + get '/default_values_from_other_params' do + { + foo: params[:foo], + bar: params[:bar], + qux: params[:qux] + } + end + end + end - expect(before['non_random_number']).to eq(after['non_random_number']) - expect(before['random_number']).not_to eq(after['random_number']) - end + it 'sets default value for optional params using other params values' do + expected_foo_value = 'foo-value' - it 'sets default values for grouped arrays' do - get('/array?array[][name]=name&array[][name]=name2&array[][with_default]=bar2') - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ array: [{ name: 'name', with_default: 'default' }, { name: 'name2', with_default: 'bar2' }] }.to_json) + get("/default_values_from_other_params?foo=#{expected_foo_value}") + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ + foo: expected_foo_value, + bar: expected_foo_value, + qux: expected_foo_value + }.to_json) + end end context 'optional group with defaults' do @@ -473,17 +552,4 @@ def app expect(JSON.parse(last_response.body)).to eq(expected) end end - - it 'sets default value for optional params using other params values' do - expected_foo_value = 'foo-value' - - get("/default_values_from_other_params?foo=#{expected_foo_value}") - - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ - foo: expected_foo_value, - bar: expected_foo_value, - qux: expected_foo_value - }.to_json) - end end diff --git a/spec/grape/validations/validators/exactly_one_of_validator_spec.rb b/spec/grape/validations/validators/exactly_one_of_validator_spec.rb index 21e177e93..90c058690 100644 --- a/spec/grape/validations/validators/exactly_one_of_validator_spec.rb +++ b/spec/grape/validations/validators/exactly_one_of_validator_spec.rb @@ -1,165 +1,158 @@ # frozen_string_literal: true describe Grape::Validations::Validators::ExactlyOneOfValidator do - let_it_be(:app) do - Class.new(Grape::API) do - rescue_from Grape::Exceptions::ValidationErrors do |e| - error!(e.errors.transform_keys! { |key| key.join(',') }, 400) - end - - params do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit - end - post do - end - - params do - optional :beer - optional :wine - optional :grapefruit - optional :other - exactly_one_of :beer, :wine, :grapefruit - end - post 'mixed-params' do - end + describe '#validate!' do + subject(:validate) { post path, params } - params do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit, message: 'you should choose one' - end - post '/custom-message' do - end + describe '/' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - requires :item, type: Hash do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit + params do + optional :beer + optional :wine + optional :grapefruit + exactly_one_of :beer, :wine, :grapefruit + end + post do + end end end - post '/nested-hash' do - end - params do - optional :item, type: Hash do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit - end - end - post '/nested-optional-hash' do - end + context 'when all params are present' do + let(:path) { '/' } + let(:params) { { beer: true, wine: true, grapefruit: true } } - params do - requires :items, type: Array do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,wine,grapefruit' => ['are mutually exclusive'] + ) end end - post '/nested-array' do - end - params do - requires :items, type: Array do - requires :nested_items, type: Array do - optional :beer, :wine, :grapefruit, type: Grape::API::Boolean - exactly_one_of :beer, :wine, :grapefruit - end + context 'when a subset of params are present' do + let(:path) { '/' } + let(:params) { { beer: true, grapefruit: true } } + + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,grapefruit' => ['are mutually exclusive'] + ) end end - post '/deeply-nested-array' do - end - end - end - - describe '#validate!' do - subject(:validate) { post path, params } - context 'when all params are present' do - let(:path) { '/' } - let(:params) { { beer: true, wine: true, grapefruit: true } } + context 'when exacly one param is present' do + let(:path) { '/' } + let(:params) { { beer: true, somethingelse: true } } - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['are mutually exclusive'] - ) + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 + end end - context 'mixed with other params' do - let(:path) { '/mixed-params' } - let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } + context 'when none of the params are present' do + let(:path) { '/' } + let(:params) { { somethingelse: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['are mutually exclusive'] + 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided'] ) end end end - context 'when a subset of params are present' do - let(:path) { '/' } - let(:params) { { beer: true, grapefruit: true } } + describe '/mixed-params' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,grapefruit' => ['are mutually exclusive'] - ) + params do + optional :beer + optional :wine + optional :grapefruit + optional :other + exactly_one_of :beer, :wine, :grapefruit + end + post 'mixed-params' do + end + end end - end - context 'when custom message is specified' do - let(:path) { '/custom-message' } - let(:params) { { beer: true, wine: true } } + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( - 'beer,wine' => ['you should choose one'] + 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end end - context 'when exacly one param is present' do - let(:path) { '/' } - let(:params) { { beer: true, somethingelse: true } } + describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 + params do + optional :beer + optional :wine + optional :grapefruit + exactly_one_of :beer, :wine, :grapefruit, message: 'you should choose one' + end + post '/custom-message' do + end + end end - end - context 'when none of the params are present' do - let(:path) { '/' } - let(:params) { { somethingelse: true } } + let(:path) { '/custom-message' } + let(:params) { { beer: true, wine: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided'] + 'beer,wine' => ['you should choose one'] ) end end - context 'when params are nested inside required hash' do + describe '/nested-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :item, type: Hash do + optional :beer + optional :wine + optional :grapefruit + exactly_one_of :beer, :wine, :grapefruit + end + end + post '/nested-hash' do + end + end + end + let(:path) { '/nested-hash' } let(:params) { { item: { beer: true, wine: true } } } @@ -172,7 +165,26 @@ end end - context 'when params are nested inside optional hash' do + describe '/nested-optional-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :item, type: Hash do + optional :beer + optional :wine + optional :grapefruit + exactly_one_of :beer, :wine, :grapefruit + end + end + post '/nested-optional-hash' do + end + end + end + let(:path) { '/nested-optional-hash' } context 'when params are passed' do @@ -197,7 +209,26 @@ end end - context 'when params are nested inside array' do + describe '/nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + optional :beer + optional :wine + optional :grapefruit + exactly_one_of :beer, :wine, :grapefruit + end + end + post '/nested-array' do + end + end + end + let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } } @@ -215,7 +246,26 @@ end end - context 'when params are deeply nested' do + describe '/deeply-nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, :grapefruit, type: Grape::API::Boolean + exactly_one_of :beer, :wine, :grapefruit + end + end + end + post '/deeply-nested-array' do + end + end + end + let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } } diff --git a/spec/grape/validations/validators/length_validator_spec.rb b/spec/grape/validations/validators/length_validator_spec.rb index 7e85b4dd8..6987cb8c7 100644 --- a/spec/grape/validations/validators/length_validator_spec.rb +++ b/spec/grape/validations/validators/length_validator_spec.rb @@ -1,113 +1,17 @@ # frozen_string_literal: true describe Grape::Validations::Validators::LengthValidator do - let_it_be(:app) do - Class.new(Grape::API) do - params do - requires :list, length: { min: 2, max: 3 } - end - post 'with_min_max' do - end - - params do - requires :list, type: [Integer], length: { min: 2 } - end - post 'with_min_only' do - end - - params do - requires :list, type: [Integer], length: { max: 3 } - end - post 'with_max_only' do - end - - params do - requires :list, type: Integer, length: { max: 3 } - end - post 'type_is_not_array' do - end - - params do - requires :list, type: Hash, length: { max: 3 } - end - post 'type_supports_length' do - end - - params do - requires :list, type: [Integer], length: { min: -3 } - end - post 'negative_min' do - end - - params do - requires :list, type: [Integer], length: { max: -3 } - end - post 'negative_max' do - end - - params do - requires :list, type: [Integer], length: { min: 2.5 } - end - post 'float_min' do - end - - params do - requires :list, type: [Integer], length: { max: 2.5 } - end - post 'float_max' do - end - - params do - requires :list, type: [Integer], length: { min: 15, max: 3 } - end - post 'min_greater_than_max' do - end - - params do - requires :list, type: [Integer], length: { min: 3, max: 3 } - end - post 'min_equal_to_max' do - end - - params do - requires :list, type: [JSON], length: { min: 0 } - end - post 'zero_min' do - end - - params do - requires :list, type: [JSON], length: { max: 0 } - end - post 'zero_max' do - end - - params do - requires :list, type: [Integer], length: { min: 2, message: 'not match' } - end - post '/custom-message' do - end - - params do - requires :code, length: { is: 2 } - end - post 'is' do - end - - params do - requires :code, length: { is: -2 } - end - post 'negative_is' do - end - - params do - requires :code, length: { is: 2, max: 10 } - end - post 'is_with_max' do + describe '/with_min_max' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, length: { min: 2, max: 3 } + end + post 'with_min_max' do + end end end - end - describe '/with_min_max' do context 'when length is within limits' do it do post '/with_min_max', list: [1, 2] @@ -134,6 +38,16 @@ end describe '/with_max_only' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { max: 3 } + end + post 'with_max_only' do + end + end + end + context 'when length is less than limits' do it do post '/with_max_only', list: [1, 2] @@ -152,6 +66,16 @@ end describe '/with_min_only' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: 2 } + end + post 'with_min_only' do + end + end + end + context 'when length is greater than limit' do it do post '/with_min_only', list: [1, 2] @@ -170,6 +94,16 @@ end describe '/zero_min' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [JSON], length: { min: 0 } + end + post 'zero_min' do + end + end + end + context 'when length is equal to the limit' do it do post '/zero_min', list: '[]' @@ -188,6 +122,16 @@ end describe '/zero_max' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [JSON], length: { max: 0 } + end + post 'zero_max' do + end + end + end + context 'when length is within the limit' do it do post '/zero_max', list: '[]' @@ -206,6 +150,16 @@ end describe '/type_is_not_array' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: Integer, length: { max: 3 } + end + post 'type_is_not_array' do + end + end + end + context 'does not raise an error' do it do expect do @@ -216,6 +170,16 @@ end describe '/type_supports_length' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: Hash, length: { max: 3 } + end + post 'type_supports_length' do + end + end + end + context 'when length is within limits' do it do post 'type_supports_length', list: { key: 'value' } @@ -235,7 +199,17 @@ describe '/negative_min' do context 'when min is negative' do - it do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: -3 } + end + post 'negative_min' do + end + end + end + + it 'raises an error' do expect { post 'negative_min', list: [12] }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero') end end @@ -243,6 +217,16 @@ describe '/negative_max' do context 'it raises an error' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { max: -3 } + end + post 'negative_max' do + end + end + end + it do expect { post 'negative_max', list: [12] }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero') end @@ -251,6 +235,16 @@ describe '/float_min' do context 'when min is not an integer' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: 2.5 } + end + post 'float_min' do + end + end + end + it do expect { post 'float_min', list: [12] }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero') end @@ -259,6 +253,16 @@ describe '/float_max' do context 'when max is not an integer' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { max: 2.5 } + end + post 'float_max' do + end + end + end + it do expect { post 'float_max', list: [12] }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero') end @@ -267,13 +271,33 @@ describe '/min_greater_than_max' do context 'raises an error' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: 15, max: 3 } + end + post 'min_greater_than_max' do + end + end + end + it do - expect { post 'min_greater_than_max', list: [1, 2] }.to raise_error(ArgumentError, 'min 15 cannot be greater than max 3') + expect { post 'min_greater_than_max', list: [12] }.to raise_error(ArgumentError, 'min 15 cannot be greater than max 3') end end end describe '/min_equal_to_max' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: 3, max: 3 } + end + post 'min_equal_to_max' do + end + end + end + context 'when array meets expectations' do it do post 'min_equal_to_max', list: [1, 2, 3] @@ -300,6 +324,16 @@ end describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: 2, message: 'not match' } + end + post '/custom-message' do + end + end + end + context 'is within limits' do it do post '/custom-message', list: [1, 2, 3] @@ -318,6 +352,16 @@ end describe '/is' do + let(:app) do + Class.new(Grape::API) do + params do + requires :code, length: { is: 2 } + end + post 'is' do + end + end + end + context 'when length is exact' do it do post 'is', code: 'ZZ' @@ -352,6 +396,16 @@ end describe '/negative_is' do + let(:app) do + Class.new(Grape::API) do + params do + requires :code, length: { is: -2 } + end + post 'negative_is' do + end + end + end + context 'when `is` is negative' do it do expect { post 'negative_is', code: 'ZZ' }.to raise_error(ArgumentError, 'is must be an integer greater than zero') @@ -361,6 +415,16 @@ describe '/is_with_max' do context 'when `is` is combined with max' do + let(:app) do + Class.new(Grape::API) do + params do + requires :code, length: { is: 2, max: 10 } + end + post 'is_with_max' do + end + end + end + it do expect { post 'is_with_max', code: 'ZZ' }.to raise_error(ArgumentError, 'is cannot be combined with min or max') end diff --git a/spec/grape/validations/validators/mutually_exclusive_spec.rb b/spec/grape/validations/validators/mutually_exclusive_spec.rb index a2c82e332..e217cb365 100644 --- a/spec/grape/validations/validators/mutually_exclusive_spec.rb +++ b/spec/grape/validations/validators/mutually_exclusive_spec.rb @@ -1,92 +1,85 @@ # frozen_string_literal: true describe Grape::Validations::Validators::MutuallyExclusiveValidator do - let_it_be(:app) do - Class.new(Grape::API) do - rescue_from Grape::Exceptions::ValidationErrors do |e| - error!(e.errors.transform_keys! { |key| key.join(',') }, 400) - end + describe '#validate!' do + subject(:validate) { post path, params } - params do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit - end - post do - end + describe '/' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - optional :beer - optional :wine - optional :grapefruit - optional :other - mutually_exclusive :beer, :wine, :grapefruit - end - post 'mixed-params' do + params do + optional :beer + optional :wine + optional :grapefruit + mutually_exclusive :beer, :wine, :grapefruit + end + post do + end + end end - params do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit, message: 'you should not mix beer and wine' - end - post '/custom-message' do - end + context 'when all mutually exclusive params are present' do + let(:path) { '/' } + let(:params) { { beer: true, wine: true, grapefruit: true } } - params do - requires :item, type: Hash do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,wine,grapefruit' => ['are mutually exclusive'] + ) end end - post '/nested-hash' do - end - params do - optional :item, type: Hash do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit + context 'when a subset of mutually exclusive params are present' do + let(:path) { '/' } + let(:params) { { beer: true, grapefruit: true } } + + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,grapefruit' => ['are mutually exclusive'] + ) end end - post '/nested-optional-hash' do - end - params do - requires :items, type: Array do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit + context 'when no mutually exclusive params are present' do + let(:path) { '/' } + let(:params) { { beer: true, somethingelse: true } } + + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end - post '/nested-array' do - end + end + + describe '/mixed-params' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - requires :items, type: Array do - requires :nested_items, type: Array do - optional :beer, :wine, :grapefruit, type: Grape::API::Boolean + params do + optional :beer + optional :wine + optional :grapefruit + optional :other mutually_exclusive :beer, :wine, :grapefruit end + post 'mixed-params' do + end end end - post '/deeply-nested-array' do - end - end - end - describe '#validate!' do - subject(:validate) { post path, params } - - context 'when all mutually exclusive params are present' do - let(:path) { '/' } - let(:params) { { beer: true, wine: true, grapefruit: true } } + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'returns a validation error' do validate @@ -95,35 +88,26 @@ 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end - - context 'mixed with other params' do - let(:path) { '/mixed-params' } - let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } - - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['are mutually exclusive'] - ) - end - end end - context 'when a subset of mutually exclusive params are present' do - let(:path) { '/' } - let(:params) { { beer: true, grapefruit: true } } + describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,grapefruit' => ['are mutually exclusive'] - ) + params do + optional :beer + optional :wine + optional :grapefruit + mutually_exclusive :beer, :wine, :grapefruit, message: 'you should not mix beer and wine' + end + post '/custom-message' do + end + end end - end - context 'when custom message is specified' do let(:path) { '/custom-message' } let(:params) { { beer: true, wine: true } } @@ -136,17 +120,26 @@ end end - context 'when no mutually exclusive params are present' do - let(:path) { '/' } - let(:params) { { beer: true, somethingelse: true } } + describe '/nested-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 + params do + requires :item, type: Hash do + optional :beer + optional :wine + optional :grapefruit + mutually_exclusive :beer, :wine, :grapefruit + end + end + post '/nested-hash' do + end + end end - end - context 'when mutually exclusive params are nested inside required hash' do let(:path) { '/nested-hash' } let(:params) { { item: { beer: true, wine: true } } } @@ -159,7 +152,26 @@ end end - context 'when mutually exclusive params are nested inside optional hash' do + describe '/nested-optional-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :item, type: Hash do + optional :beer + optional :wine + optional :grapefruit + mutually_exclusive :beer, :wine, :grapefruit + end + end + post '/nested-optional-hash' do + end + end + end + let(:path) { '/nested-optional-hash' } context 'when params are passed' do @@ -184,7 +196,26 @@ end end - context 'when mutually exclusive params are nested inside array' do + describe '/nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + optional :beer + optional :wine + optional :grapefruit + mutually_exclusive :beer, :wine, :grapefruit + end + end + post '/nested-array' do + end + end + end + let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } } @@ -198,7 +229,26 @@ end end - context 'when mutually exclusive params are deeply nested' do + describe '/deeply-nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, :grapefruit, type: Grape::API::Boolean + mutually_exclusive :beer, :wine, :grapefruit + end + end + end + post '/deeply-nested-array' do + end + end + end + let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } } diff --git a/spec/grape/validations/validators/regexp_validator_spec.rb b/spec/grape/validations/validators/regexp_validator_spec.rb index c12e8a60a..1976f289c 100644 --- a/spec/grape/validations/validators/regexp_validator_spec.rb +++ b/spec/grape/validations/validators/regexp_validator_spec.rb @@ -1,66 +1,148 @@ # frozen_string_literal: true describe Grape::Validations::Validators::RegexpValidator do - let_it_be(:app) do - Class.new(Grape::API) do - default_format :json + describe '#bad encoding' do + let(:app) do + Class.new(Grape::API) do + default_format :json - resources :custom_message do params do - requires :name, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } - end - get do + requires :name, regexp: { value: /^[a-z]+$/ } end + get '/bad_encoding' + end + end + + context 'when value as bad encoding' do + it 'does not raise an error' do + expect { get '/bad_encoding', name: "Hello \x80" }.not_to raise_error + end + end + end + + describe '/' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do - requires :names, type: { value: Array[String], message: 'can\'t be nil' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } + requires :name, regexp: /^[a-z]+$/ end - get 'regexp_with_array' do + get do end end + end - params do - requires :name, regexp: /^[a-z]+$/ - end - get do + context 'invalid input' do + it 'refuses inapppopriate' do + get '/', name: 'invalid name' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"name is invalid"}') end - params do - requires :names, type: Array[String], regexp: /^[a-z]+$/ - end - get 'regexp_with_array' do + it 'refuses empty' do + get '/', name: '' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"name is invalid"}') end + end + + it 'accepts nil' do + get '/', name: nil + expect(last_response.status).to eq(200) + end + + it 'accepts valid input' do + get '/', name: 'bob' + expect(last_response.status).to eq(200) + end + end - params do - requires :people, type: Hash do + describe '/regexp_with_array' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do requires :names, type: Array[String], regexp: /^[a-z]+$/ end + get 'regexp_with_array' do + end end - get 'nested_regexp_with_array' do - end + end + + it 'refuses inapppopriate items' do + get '/regexp_with_array', names: ['invalid name', 'abc'] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"names is invalid"}') + end + + it 'refuses empty items' do + get '/regexp_with_array', names: ['', 'abc'] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"names is invalid"}') + end + + it 'refuses nil items' do + get '/regexp_with_array', names: [nil, 'abc'] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"names is invalid"}') + end + + it 'accepts valid items' do + get '/regexp_with_array', names: ['bob'] + expect(last_response.status).to eq(200) + end + + it 'accepts nil instead of array' do + get '/regexp_with_array', names: nil + expect(last_response.status).to eq(200) end end - describe '#bad encoding' do + describe '/nested_regexp_with_array' do let(:app) do Class.new(Grape::API) do default_format :json params do - requires :name, regexp: { value: /^[a-z]+$/ } + requires :people, type: Hash do + requires :names, type: Array[String], regexp: /^[a-z]+$/ + end + end + get 'nested_regexp_with_array' do end - get '/bad_encoding' end end - context 'when value as bad encoding' do - it 'does not raise an error' do - expect { get '/bad_encoding', name: "Hello \x80" }.not_to raise_error - end + it 'refuses inapppopriate' do + get '/nested_regexp_with_array', people: 'invalid name' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"people is invalid, people[names] is missing, people[names] is invalid"}') end end - context 'custom validation message' do + describe '/custom_message' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + resources :custom_message do + params do + requires :name, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } + end + get do + end + + params do + requires :names, type: { value: Array[String], message: 'can\'t be nil' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } + end + get 'regexp_with_array' do + end + end + end + end + context 'with invalid input' do it 'refuses inapppopriate' do get '/custom_message', name: 'invalid name' @@ -115,66 +197,4 @@ end end end - - context 'invalid input' do - it 'refuses inapppopriate' do - get '/', name: 'invalid name' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('{"error":"name is invalid"}') - end - - it 'refuses empty' do - get '/', name: '' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('{"error":"name is invalid"}') - end - end - - it 'accepts nil' do - get '/', name: nil - expect(last_response.status).to eq(200) - end - - it 'accepts valid input' do - get '/', name: 'bob' - expect(last_response.status).to eq(200) - end - - context 'regexp with array' do - it 'refuses inapppopriate items' do - get '/regexp_with_array', names: ['invalid name', 'abc'] - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('{"error":"names is invalid"}') - end - - it 'refuses empty items' do - get '/regexp_with_array', names: ['', 'abc'] - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('{"error":"names is invalid"}') - end - - it 'refuses nil items' do - get '/regexp_with_array', names: [nil, 'abc'] - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('{"error":"names is invalid"}') - end - - it 'accepts valid items' do - get '/regexp_with_array', names: ['bob'] - expect(last_response.status).to eq(200) - end - - it 'accepts nil instead of array' do - get '/regexp_with_array', names: nil - expect(last_response.status).to eq(200) - end - end - - context 'nested regexp with array' do - it 'refuses inapppopriate' do - get '/nested_regexp_with_array', people: 'invalid name' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('{"error":"people is invalid, people[names] is missing, people[names] is invalid"}') - end - end end diff --git a/spec/grape/validations/validators/same_as_validator_spec.rb b/spec/grape/validations/validators/same_as_validator_spec.rb index cc1ac984e..62016e5fa 100644 --- a/spec/grape/validations/validators/same_as_validator_spec.rb +++ b/spec/grape/validations/validators/same_as_validator_spec.rb @@ -1,25 +1,18 @@ # frozen_string_literal: true describe Grape::Validations::Validators::SameAsValidator do - let_it_be(:app) do - Class.new(Grape::API) do - params do - requires :password - requires :password_confirmation, same_as: :password - end - post do - end - - params do - requires :password - requires :password_confirmation, same_as: { value: :password, message: 'not match' } - end - post '/custom-message' do + describe '/' do + let(:app) do + Class.new(Grape::API) do + params do + requires :password + requires :password_confirmation, same_as: :password + end + post do + end end end - end - describe '/' do context 'is the same' do it do post '/', password: '987654', password_confirmation: '987654' @@ -38,6 +31,17 @@ end describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + params do + requires :password + requires :password_confirmation, same_as: { value: :password, message: 'not match' } + end + post '/custom-message' do + end + end + end + context 'is the same' do it do post '/custom-message', password: '987654', password_confirmation: '987654' diff --git a/spec/grape/validations/validators/values_validator_spec.rb b/spec/grape/validations/validators/values_validator_spec.rb index 437202ac8..5ee07b46c 100644 --- a/spec/grape/validations/validators/values_validator_spec.rb +++ b/spec/grape/validations/validators/values_validator_spec.rb @@ -45,384 +45,461 @@ def default_excepts end end - let(:app) do - Class.new(Grape::API) do - default_format :json + before do + stub_const('ValuesModel', values_model) + end - resources :custom_message do - params do - requires :type, values: { value: ValuesModel.values, message: 'value does not include in values' } - end - get '/' do - { type: params[:type] } - end + describe '#bad encoding' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do - optional :type, values: { value: -> { ValuesModel.values }, message: 'value does not include in values' }, default: 'valid-type2' - end - get '/lambda' do - { type: params[:type] } + requires :type, type: String, values: %w[a b] end + get '/bad_encoding' end + end - params do - requires :type, values: ValuesModel.values - end - get '/' do - { type: params[:type] } + context 'when value as bad encoding' do + it 'does not raise an error' do + expect { get '/bad_encoding', type: "Hello \x80" }.not_to raise_error end + end + end - params do - requires :type, values: [] - end - get '/empty' + describe '/custom_message' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :type, values: { value: ValuesModel.values }, default: 'valid-type2' - end - get '/default/hash/valid' do - { type: params[:type] } + resources :custom_message do + params do + requires :type, values: { value: ValuesModel.values, message: 'value does not include in values' } + end + get '/' do + { type: params[:type] } + end + + params do + optional :type, values: { value: -> { ValuesModel.values }, message: 'value does not include in values' }, default: 'valid-type2' + end + get '/lambda' do + { type: params[:type] } + end + end end + end - params do - optional :type, values: ValuesModel.values, default: 'valid-type2' - end - get '/default/valid' do - { type: params[:type] } - end + it 'allows a valid value for a parameter' do + get('/custom_message', type: 'valid-type1') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) + end - params do - optional :type, values: -> { ValuesModel.values }, default: 'valid-type2' - end - get '/lambda' do - { type: params[:type] } - end + it 'does not allow an invalid value for a parameter' do + get('/custom_message', type: 'invalid-type') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json) + end - params do - optional :type, type: Integer, values: 1.. - end - get '/endless' do - { type: params[:type] } - end + it 'validates against values in a proc' do + ValuesModel.add_value('valid-type4') - params do - requires :type, values: ->(v) { ValuesModel.include? v } - end - get '/lambda_val' do - { type: params[:type] } - end + get('/custom_message/lambda', type: 'valid-type4') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) + end - params do - requires :number, type: Integer, values: ->(v) { v > 0 } - end - get '/lambda_int_val' do - { number: params[:number] } - end + it 'does not allow an invalid value for a parameter using lambda' do + get('/custom_message/lambda', type: 'invalid-type') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json) + end + end - params do - requires :type, values: -> { [] } - end - get '/empty_lambda' + describe '/' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample } - end - get '/default_lambda' do - { type: params[:type] } + params do + requires :type, values: ValuesModel.values + end + get '/' do + { type: params[:type] } + end end + end - params do - optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample } - end - get '/default_and_values_lambda' do - { type: params[:type] } - end + it 'allows a valid value for a parameter' do + get('/', type: 'valid-type1') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) + end - params do - optional :type, type: Grape::API::Boolean, desc: 'A boolean', values: [true] - end - get '/values/optional_boolean' do - { type: params[:type] } - end + it 'does not allow an invalid value for a parameter' do + get('/', type: 'invalid-type') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end - params do - requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10 - end - get '/values/coercion' do - { type: params[:type] } + context 'nil value for a parameter' do + it 'does not allow for root params scope' do + get('/', type: nil) + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end + end - params do - requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10 - end - get '/values/array_coercion' do - { type: params[:type] } - end + it 'does not validate updated values without proc' do + app # Instantiate with the existing values. + ValuesModel.add_value('valid-type4') + get('/', type: 'valid-type4') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end + end + + describe '/empty' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :optional, type: Array do - requires :type, values: %w[a b] + params do + requires :type, values: [] end + get '/empty' end - get '/optional_with_required_values' + end - params do - requires :type, type: Integer, values: 1..5, except_values: [3] - end - get '/mixed/value/except' do - { type: params[:type] } - end + it 'rejects all values if values is an empty array' do + get('/empty', type: 'invalid-type') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end + end - params do - optional :optional, type: Array[String], values: %w[a b c] - end - put '/optional_with_array_of_string_values' + describe '/optional_with_required_values' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - requires :type, values: ->(v) { ValuesModel.include? v } - end - get '/proc' do - { type: params[:type] } + params do + optional :optional, type: Array do + requires :type, values: %w[a b] + end + end + get '/optional_with_required_values' end + end - params do - requires :type, values: { value: ->(v) { ValuesModel.include? v }, message: 'failed check' } - end - get '/proc/message' + it 'allows nil value for a required param in child scope' do + get('/optional_with_required_values') + expect(last_response.status).to eq 200 + end + end - params do - requires :number, values: { value: ->(v) { ValuesModel.even? v }, message: 'must be even' } - end - get '/proc/custom_message' do - { message: 'success' } - end + describe '/optional_with_array_of_string_values' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - requires :input_one, :input_two, values: { value: ->(v1, v2) { v1 + v2 > 10 } } + params do + optional :optional, type: Array[String], values: %w[a b c] + end + put '/optional_with_array_of_string_values' end - get '/proc/arity2' + end - params do - optional :name, type: String, values: %w[a b], allow_blank: true - end - get '/allow_blank' + it 'accepts nil for an optional param with a list of values' do + put('/optional_with_array_of_string_values', optional: nil) + expect(last_response.status).to eq 200 + end + end - params do - with(type: String) do - requires :type, values: ValuesModel.values + describe '/default/valid' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :type, values: ValuesModel.values, default: 'valid-type2' + end + get '/default/valid' do + { type: params[:type] } end end - get 'values_wrapped_by_with_block' end - end - before do - stub_const('ValuesModel', values_model) + it 'allows a valid default value' do + get('/default/valid') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) + end end - describe '#bad encoding' do + describe '/default/hash/valid' do let(:app) do Class.new(Grape::API) do default_format :json params do - requires :type, type: String, values: %w[a b] + optional :type, values: { value: ValuesModel.values }, default: 'valid-type2' + end + get '/default/hash/valid' do + { type: params[:type] } end - get '/bad_encoding' end end - context 'when value as bad encoding' do - it 'does not raise an error' do - expect { get '/bad_encoding', type: "Hello \x80" }.not_to raise_error - end + it 'allows a valid default value' do + get('/default/hash/valid') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) end end - context 'with a custom validation message' do - it 'allows a valid value for a parameter' do - get('/custom_message', type: 'valid-type1') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) + describe '/lambda' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :type, values: -> { ValuesModel.values }, default: 'valid-type2' + end + get '/lambda' do + { type: params[:type] } + end + end end - it 'does not allow an invalid value for a parameter' do - get('/custom_message', type: 'invalid-type') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json) + it 'allows a proc for values' do + get('/lambda', type: 'valid-type1') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'validates against values in a proc' do ValuesModel.add_value('valid-type4') - get('/custom_message/lambda', type: 'valid-type4') + get('/lambda', type: 'valid-type4') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) end it 'does not allow an invalid value for a parameter using lambda' do - get('/custom_message/lambda', type: 'invalid-type') + get('/lambda', type: 'invalid-type') expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json) + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end - end - it 'allows a valid value for a parameter' do - get('/', type: 'valid-type1') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) + it 'evaluates the proc per-request, not at definition time (e.g. for DB-backed values)' do + app # instantiate at definition time, before the new value is added + ValuesModel.add_value('valid-type4') + get('/lambda', type: 'valid-type4') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) + end end - it 'does not allow an invalid value for a parameter' do - get('/', type: 'invalid-type') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) - end + describe '/endless' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'rejects all values if values is an empty array' do - get('/empty', type: 'invalid-type') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) - end + params do + optional :type, type: Integer, values: 1.. + end + get '/endless' do + { type: params[:type] } + end + end + end - context 'nil value for a parameter' do - it 'does not allow for root params scope' do - get('/', type: nil) + it 'validates against values in an endless range' do + get('/endless', type: 10) + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 10 }.to_json) + end + + it 'does not allow an invalid value for a parameter using an endless range' do + get('/endless', type: 0) expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end + end - it 'allows for a required param in child scope' do - get('/optional_with_required_values') - expect(last_response.status).to eq 200 + describe '/lambda_val' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, values: ->(v) { ValuesModel.include? v } + end + get '/lambda_val' do + { type: params[:type] } + end + end end - it 'accepts for an optional param with a list of values' do - put('/optional_with_array_of_string_values', optional: nil) + it 'allows value using lambda' do + get('/lambda_val', type: 'valid-type1') expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end - end - it 'allows a valid default value' do - get('/default/valid') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) + it 'does not allow invalid value using lambda' do + get('/lambda_val', type: 'invalid-type') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end end - it 'allows a valid default value' do - get('/default/hash/valid') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) - end + describe '/lambda_int_val' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'allows a proc for values' do - get('/lambda', type: 'valid-type1') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) - end + params do + requires :number, type: Integer, values: ->(v) { v > 0 } + end + get '/lambda_int_val' do + { number: params[:number] } + end + end + end - it 'does not validate updated values without proc' do - app # Instantiate with the existing values. - ValuesModel.add_value('valid-type4') - get('/', type: 'valid-type4') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) - end + it 'does not allow non-numeric string value for int value using lambda' do + get('/lambda_int_val', number: 'foo') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'number is invalid, number does not have a valid value' }.to_json) + end - it 'validates against values in a proc' do - ValuesModel.add_value('valid-type4') + it 'does not allow nil for int value using lambda' do + get('/lambda_int_val', number: nil) + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'number does not have a valid value' }.to_json) + end - get('/lambda', type: 'valid-type4') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) + it 'allows numeric string for int value using lambda' do + get('/lambda_int_val', number: '3') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ number: 3 }.to_json) + end end - it 'does not allow an invalid value for a parameter using lambda' do - get('/lambda', type: 'invalid-type') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) - end + describe '/empty_lambda' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'validates against values in an endless range' do - get('/endless', type: 10) - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 10 }.to_json) - end + params do + requires :type, values: -> { [] } + end + get '/empty_lambda' + end + end - it 'does not allow an invalid value for a parameter using an endless range' do - get('/endless', type: 0) - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + it 'validates against an empty array in a proc' do + get('/empty_lambda', type: 'any') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end end - it 'does not allow non-numeric string value for int value using lambda' do - get('/lambda_int_val', number: 'foo') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'number is invalid, number does not have a valid value' }.to_json) - end + describe '/default_lambda' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'does not allow nil for int value using lambda' do - get('/lambda_int_val', number: nil) - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'number does not have a valid value' }.to_json) - end + params do + optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample } + end + get '/default_lambda' do + { type: params[:type] } + end + end + end - it 'allows numeric string for int value using lambda' do - get('/lambda_int_val', number: '3') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ number: 3 }.to_json) + it 'validates default value from proc' do + get('/default_lambda') + expect(last_response.status).to eq 200 + end end - it 'allows value using lambda' do - get('/lambda_val', type: 'valid-type1') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) - end + describe '/default_and_values_lambda' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'does not allow invalid value using lambda' do - get('/lambda_val', type: 'invalid-type') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) - end + params do + optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample } + end + get '/default_and_values_lambda' do + { type: params[:type] } + end + end + end - it 'validates against an empty array in a proc' do - get('/empty_lambda', type: 'any') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + it 'validates default value from proc against values in a proc' do + get('/default_and_values_lambda') + expect(last_response.status).to eq 200 + end end - it 'validates default value from proc' do - get('/default_lambda') - expect(last_response.status).to eq 200 - end + context 'IncompatibleOptionValues' do + it 'raises on an invalid default value from proc' do + subject = Class.new(Grape::API) + expect do + subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: "#{ValuesModel.values.sample}_invalid" } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end - it 'validates default value from proc against values in a proc' do - get('/default_and_values_lambda') - expect(last_response.status).to eq 200 - end + it 'raises on an invalid default value' do + subject = Class.new(Grape::API) + expect do + subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: 'invalid-type' } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end - it 'raises IncompatibleOptionValues on an invalid default value from proc' do - subject = Class.new(Grape::API) - expect do - subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: "#{ValuesModel.values.sample}_invalid" } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues - end + it 'raises when type is incompatible with values array' do + subject = Class.new(Grape::API) + expect do + subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], type: Symbol } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end - it 'raises IncompatibleOptionValues on an invalid default value' do - subject = Class.new(Grape::API) - expect do - subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: 'invalid-type' } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues - end + it 'raises when values contains a value that is not a kind of the type' do + subject = Class.new(Grape::API) + expect do + subject.params { requires :type, values: [10.5, 11], type: Integer } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end - it 'raises IncompatibleOptionValues when type is incompatible with values array' do - subject = Class.new(Grape::API) - expect do - subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], type: Symbol } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues + it 'raises when except contains a value that is not a kind of the type' do + subject = Class.new(Grape::API) + expect do + subject.params { requires :type, except_values: [10.5, 11], type: Integer } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end end - context 'boolean values' do + describe '/values/optional_boolean' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :type, type: Grape::API::Boolean, desc: 'A boolean', values: [true] + end + get '/values/optional_boolean' do + { type: params[:type] } + end + end + end + it 'allows a value from the list' do get('/values/optional_boolean', type: true) @@ -437,38 +514,67 @@ def default_excepts end end - it 'allows values to be a kind of the coerced type not just an instance of it' do - get('/values/coercion', type: 10) - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 10 }.to_json) - end + describe '/values/coercion' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'allows values to be a kind of the coerced type in an array' do - get('/values/array_coercion', type: [10]) - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: [10] }.to_json) - end + params do + requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10 + end + get '/values/coercion' do + { type: params[:type] } + end + end + end - it 'raises IncompatibleOptionValues when values contains a value that is not a kind of the type' do - subject = Class.new(Grape::API) - expect do - subject.params { requires :type, values: [10.5, 11], type: Integer } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues + it 'allows values to be a kind of the coerced type not just an instance of it' do + get('/values/coercion', type: 10) + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 10 }.to_json) + end end - it 'raises IncompatibleOptionValues when except contains a value that is not a kind of the type' do - subject = Class.new(Grape::API) - expect do - subject.params { requires :type, except_values: [10.5, 11], type: Integer } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues + describe '/values/array_coercion' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10 + end + get '/values/array_coercion' do + { type: params[:type] } + end + end + end + + it 'allows values to be a kind of the coerced type in an array' do + get('/values/array_coercion', type: [10]) + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: [10] }.to_json) + end end - it 'allows a blank value when the allow_blank option is true' do - get 'allow_blank', name: nil - expect(last_response.status).to eq(200) + describe '/allow_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :name, type: String, values: %w[a b], allow_blank: true + end + get '/allow_blank' + end + end - get 'allow_blank', name: '' - expect(last_response.status).to eq(200) + it 'allows a blank value when the allow_blank option is true' do + get 'allow_blank', name: nil + expect(last_response.status).to eq(200) + + get 'allow_blank', name: '' + expect(last_response.status).to eq(200) + end end context 'with a lambda values' do @@ -544,7 +650,20 @@ def app end end - context 'with mixed values and excepts' do + describe '/mixed/value/except' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, type: Integer, values: 1..5, except_values: [3] + end + get '/mixed/value/except' do + { type: params[:type] } + end + end + end + it 'allows value, but not in except' do get '/mixed/value/except', type: 2 expect(last_response.status).to eq 200 @@ -564,7 +683,20 @@ def app end end - context 'custom validation using proc' do + describe '/proc' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, values: ->(v) { ValuesModel.include? v } + end + get '/proc' do + { type: params[:type] } + end + end + end + it 'accepts a single valid value' do get '/proc', type: 'valid-type1' expect(last_response.status).to eq 200 @@ -588,36 +720,86 @@ def app expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end + end + + describe '/proc/message' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, values: { value: ->(v) { ValuesModel.include? v }, message: 'failed check' } + end + get '/proc/message' + end + end it 'uses supplied message' do get '/proc/message', type: 'invalid-type1' expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type failed check' }.to_json) end + end - context 'when proc has an arity of 1' do - it 'accepts a valid value' do - get '/proc/custom_message', number: 4 - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ message: 'success' }.to_json) - end + describe '/proc/custom_message' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'rejects an invalid value' do - get '/proc/custom_message', number: 5 - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'number must be even' }.to_json) + params do + requires :number, values: { value: ->(v) { ValuesModel.even? v }, message: 'must be even' } + end + get '/proc/custom_message' do + { message: 'success' } + end end end - context 'when arity is > 1' do - it 'returns an error status code' do - get '/proc/arity2', input_one: 2, input_two: 3 - expect(last_response.status).to eq 400 + it 'accepts a valid value' do + get '/proc/custom_message', number: 4 + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ message: 'success' }.to_json) + end + + it 'rejects an invalid value' do + get '/proc/custom_message', number: 5 + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'number must be even' }.to_json) + end + end + + describe '/proc/arity2' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :input_one, :input_two, values: { value: ->(v1, v2) { v1 + v2 > 10 } } + end + get '/proc/arity2' end end + + it 'returns an error status code' do + get '/proc/arity2', input_one: 2, input_two: 3 + expect(last_response.status).to eq 400 + end end - context 'when wrapped by with block' do + describe '/values_wrapped_by_with_block' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + with(type: String) do + requires :type, values: ValuesModel.values + end + end + get 'values_wrapped_by_with_block' + end + end + it 'rejects an invalid value' do get 'values_wrapped_by_with_block' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 358f002b1..abcaa2146 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,7 +7,7 @@ Grape.deprecator.behavior = :raise -%w[config support].each do |dir| +%w[support].each do |dir| Dir["#{File.dirname(__FILE__)}/#{dir}/**/*.rb"].each do |file| require file end