spec/grape/validations_spec.rb in grape-0.9.0 vs spec/grape/validations_spec.rb in grape-0.10.0

- old
+ new

@@ -1,9 +1,8 @@ require 'spec_helper' describe Grape::Validations do - subject { Class.new(Grape::API) } def app subject end @@ -42,22 +41,21 @@ it 'adds to declared parameters' do subject.params do optional :some_param end - expect(subject.settings[:declared_params]).to eq([:some_param]) + expect(subject.route_setting(:declared_params)).to eq([:some_param]) end end context 'required' do before do subject.params do - requires :key + requires :key, type: String end - subject.get '/required' do - 'required works' - end + subject.get('/required') { 'required works' } + subject.put('/required') { { key: params[:key] }.to_json } end it 'errors when param not present' do get '/required' expect(last_response.status).to eq(400) @@ -72,12 +70,18 @@ it 'adds to declared parameters' do subject.params do requires :some_param end - expect(subject.settings[:declared_params]).to eq([:some_param]) + expect(subject.route_setting(:declared_params)).to eq([:some_param]) end + + it 'works when required field is present but nil' do + put '/required', { key: nil }.to_json, 'CONTENT_TYPE' => 'application/json' + expect(last_response.status).to eq(200) + expect(JSON.parse(last_response.body)).to eq('key' => nil) + end end context 'requires :all using Grape::Entity documentation' do def define_requires_all documentation = { @@ -95,11 +99,11 @@ end end it 'adds entity documentation to declared params' do define_requires_all - expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field]) + expect(subject.route_setting(:declared_params)).to eq([:required_field, :optional_field]) end it 'errors when required_field is not present' do get '/required' expect(last_response.status).to eq(400) @@ -130,11 +134,11 @@ end end it 'adds entity documentation to declared params' do define_requires_none - expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field]) + expect(subject.route_setting(:declared_params)).to eq([:required_field, :optional_field]) end it 'errors when required_field is not present' do get '/required' expect(last_response.status).to eq(400) @@ -160,11 +164,11 @@ end end it 'adds only the entity documentation to declared params, nothing more' do define_requires_all - expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field]) + expect(subject.route_setting(:declared_params)).to eq([:required_field, :optional_field]) end end context 'requires :none' do def define_requires_none @@ -188,23 +192,22 @@ subject.params do requires :items, type: Array do requires :key end end - subject.get '/required' do - 'required works' - end + subject.get('/required') { 'required works' } + subject.put('/required') { { items: params[:items] }.to_json } end it 'errors when param not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is missing') end - it "errors when param is not an Array" do - get '/required', items: "hello" + it 'errors when param is not an Array' do + get '/required', items: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid, items[key] is missing') get '/required', items: { key: 'foo' } expect(last_response.status).to eq(400) @@ -215,27 +218,23 @@ get '/required', items: [{ key: 'hello' }, { key: 'world' }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end - it "doesn't allow any key in the options hash other than type" do - expect { - subject.params do - requires(:items, desc: 'Foo') do - requires :key - end - end - }.to raise_error ArgumentError + it "doesn't throw a missing param when param is present but empty" do + put '/required', { items: [] }.to_json, 'CONTENT_TYPE' => 'application/json' + expect(last_response.status).to eq(200) + expect(JSON.parse(last_response.body)).to eq('items' => []) end it 'adds to declared parameters' do subject.params do requires :items do requires :key end end - expect(subject.settings[:declared_params]).to eq([items: [:key]]) + expect(subject.route_setting(:declared_params)).to eq([items: [:key]]) end end context 'required with a Hash block' do before do @@ -253,12 +252,12 @@ get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is missing, items[key] is missing') end - it "errors when param is not a Hash" do - get '/required', items: "hello" + it 'errors when param is not a Hash' do + get '/required', items: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid, items[key] is missing') get '/required', items: [{ key: 'foo' }] expect(last_response.status).to eq(400) @@ -269,27 +268,17 @@ get '/required', items: { key: 'hello' } expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end - it "doesn't allow any key in the options hash other than type" do - expect { - subject.params do - requires(:items, desc: 'Foo') do - requires :key - end - end - }.to raise_error ArgumentError - end - it 'adds to declared parameters' do subject.params do requires :items do requires :key end end - expect(subject.settings[:declared_params]).to eq([items: [:key]]) + expect(subject.route_setting(:declared_params)).to eq([items: [:key]]) end end context 'group' do before do @@ -319,14 +308,92 @@ subject.params do group :items do requires :key end end - expect(subject.settings[:declared_params]).to eq([items: [:key]]) + expect(subject.route_setting(:declared_params)).to eq([items: [:key]]) end end + context 'group params with nested params which has a type' do + let(:invalid_items){ { items: '' } } + + before do + subject.params do + optional :items do + optional :key1, type: String + optional :key2, type: String + end + end + subject.post '/group_with_nested' do + 'group with nested works' + end + end + + it 'errors when group param is invalid'do + post '/group_with_nested', items: invalid_items + expect(last_response.status).to eq(400) + end + end + + context 'custom validator for a Hash' do + module DateRangeValidations + class DateRangeValidator < Grape::Validations::Base + def validate_param!(attr_name, params) + unless params[attr_name][:from] <= params[attr_name][:to] + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'" + end + end + end + end + + before do + subject.params do + optional :date_range, date_range: true, type: Hash do + requires :from, type: Integer + requires :to, type: Integer + end + end + subject.get('/optional') do + 'optional works' + end + subject.params do + requires :date_range, date_range: true, type: Hash do + requires :from, type: Integer + requires :to, type: Integer + end + end + subject.get('/required') do + 'required works' + end + end + + context 'which is optional' do + it "doesn't throw an error if the validation passes" do + get '/optional', date_range: { from: 1, to: 2 } + expect(last_response.status).to eq(200) + end + + it 'errors if the validation fails' do + get '/optional', date_range: { from: 2, to: 1 } + expect(last_response.status).to eq(400) + end + end + + context 'which is required' do + it "doesn't throw an error if the validation passes" do + get '/required', date_range: { from: 1, to: 2 } + expect(last_response.status).to eq(200) + end + + it 'errors if the validation fails' do + get '/required', date_range: { from: 2, to: 1 } + expect(last_response.status).to eq(400) + end + end + end + context 'validation within arrays' do before do subject.params do group :children do requires :name @@ -369,13 +436,13 @@ get '/within_array', children: [name: 'Jay'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[parents] is missing') end - it "errors when param is not an Array" do + it 'errors when param is not an Array' do # NOTE: would be nicer if these just returned 'children is invalid' - get '/within_array', children: "hello" + get '/within_array', children: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('children is invalid, children[name] is missing, children[parents] is missing, children[parents] is invalid, children[parents][name] is missing') get '/within_array', children: { name: 'foo' } expect(last_response.status).to eq(400) @@ -426,11 +493,11 @@ '' end end it 'requires defaults to Array type' do - get '/req', planets: "Jupiter, Saturn" + get '/req', planets: 'Jupiter, Saturn' expect(last_response.status).to eq(400) expect(last_response.body).to eq('planets is invalid, planets[name] is missing') get '/req', planets: { name: 'Jupiter' } expect(last_response.status).to eq(400) @@ -442,30 +509,30 @@ put_with_json '/req', planets: [] expect(last_response.status).to eq(200) end it 'optional defaults to Array type' do - get '/opt', name: "Jupiter", moons: "Europa, Ganymede" + get '/opt', name: 'Jupiter', moons: 'Europa, Ganymede' expect(last_response.status).to eq(400) expect(last_response.body).to eq('moons is invalid, moons[name] is missing') - get '/opt', name: "Jupiter", moons: { name: 'Ganymede' } + get '/opt', name: 'Jupiter', moons: { name: 'Ganymede' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('moons is invalid') - get '/opt', name: "Jupiter", moons: [{ name: 'Io' }, { name: 'Callisto' }] + get '/opt', name: 'Jupiter', moons: [{ name: 'Io' }, { name: 'Callisto' }] expect(last_response.status).to eq(200) - put_with_json '/opt', name: "Venus" + put_with_json '/opt', name: 'Venus' expect(last_response.status).to eq(200) - put_with_json '/opt', name: "Mercury", moons: [] + put_with_json '/opt', name: 'Mercury', moons: [] expect(last_response.status).to eq(200) end it 'group defaults to Array type' do - get '/grp', stars: "Sun" + get '/grp', stars: 'Sun' expect(last_response.status).to eq(400) expect(last_response.body).to eq('stars is invalid, stars[name] is missing') get '/grp', stars: { name: 'Sun' } expect(last_response.status).to eq(400) @@ -543,18 +610,18 @@ get '/optional_group', items: [{ key: 'foo' }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional group works') end - it "errors when group is present, but required param is not" do + it 'errors when group is present, but required param is not' do get '/optional_group', items: [{ not_key: 'foo' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[key] is missing') end it "errors when param is present but isn't an Array" do - get '/optional_group', items: "hello" + get '/optional_group', items: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid, items[key] is missing') get '/optional_group', items: { key: 'foo' } expect(last_response.status).to eq(400) @@ -565,11 +632,11 @@ subject.params do optional :items do requires :key end end - expect(subject.settings[:declared_params]).to eq([items: [:key]]) + expect(subject.route_setting(:declared_params)).to eq([items: [:key]]) end end context 'nested optional Array blocks' do before do @@ -629,11 +696,11 @@ requires :key optional(:optional_subitems) { requires :value } requires(:required_subitems) { requires :value } end end - expect(subject.settings[:declared_params]).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]]) + expect(subject.route_setting(:declared_params)).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]]) end end context 'multiple validation errors' do before do @@ -654,14 +721,14 @@ end end context 'custom validation' do module CustomValidations - class Customvalidator < Grape::Validations::Validator + class Customvalidator < Grape::Validations::Base def validate_param!(attr_name, params) unless params[attr_name] == 'im custom' - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "is not custom!" + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: 'is not custom!' end end end end @@ -774,17 +841,17 @@ get '/nested/one', custom: 'im wrong, validate me' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') end - specify 'the nested namesapce inherits the custom validator' do + specify 'the nested namespace inherits the custom validator' do get '/nested/nested/two', custom: 'im wrong, validate me' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') end - specify 'peer namesapces does not have the validator' do + specify 'peer namespaces does not have the validator' do get '/peer/one', custom: 'im not validated' expect(last_response.status).to eq(200) expect(last_response.body).to eq('no validation required') end @@ -803,22 +870,22 @@ end end context 'when using options on param' do module CustomValidations - class CustomvalidatorWithOptions < Grape::Validations::SingleOptionValidator + class CustomvalidatorWithOptions < Grape::Validations::Base def validate_param!(attr_name, params) unless params[attr_name] == @option[:text] - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: @option[:error_message] + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: @option[:error_message] end end end end before do subject.params do - optional :custom, customvalidator_with_options: { text: 'im custom with options', error_message: "is not custom with options!" } + optional :custom, customvalidator_with_options: { text: 'im custom with options', error_message: 'is not custom with options!' } end subject.get '/optional_custom' do 'optional with custom works!' end end @@ -875,20 +942,19 @@ it 'by #use' do subject.params do use :pagination end - expect(subject.settings[:declared_params]).to eq [:page, :per_page] + expect(subject.route_setting(:declared_params)).to eq [:page, :per_page] end it 'by #use with multiple params' do subject.params do use :pagination, :period end - expect(subject.settings[:declared_params]).to eq [:page, :per_page, :start_date, :end_date] + expect(subject.route_setting(:declared_params)).to eq [:page, :per_page, :start_date, :end_date] end - end context 'with block' do before do subject.helpers do @@ -958,33 +1024,78 @@ 'mutually_exclusive works!' end get '/mutually_exclusive', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) - expect(last_response.body).to eq "beer, wine are mutually exclusive" + expect(last_response.body).to eq 'beer, wine are mutually exclusive' end end context 'more than one set of mutually exclusive params' do it 'errors for all sets' do subject.params do optional :beer optional :wine mutually_exclusive :beer, :wine - optional :scotch - optional :aquavit - mutually_exclusive :scotch, :aquavit + optional :nested, type: Hash do + optional :scotch + optional :aquavit + mutually_exclusive :scotch, :aquavit + end + optional :nested2, type: Array do + optional :scotch2 + optional :aquavit2 + mutually_exclusive :scotch2, :aquavit2 + end end subject.get '/mutually_exclusive' do 'mutually_exclusive works!' end - get '/mutually_exclusive', beer: 'true', wine: 'true', scotch: 'true', aquavit: 'true' + get '/mutually_exclusive', beer: 'true', wine: 'true', nested: { scotch: 'true', aquavit: 'true' }, nested2: [{ scotch2: 'true' }, { scotch2: 'true', aquavit2: 'true' }] expect(last_response.status).to eq(400) - expect(last_response.body).to eq "beer, wine are mutually exclusive, scotch, aquavit are mutually exclusive" + expect(last_response.body).to eq 'beer, wine are mutually exclusive, scotch, aquavit are mutually exclusive, scotch2, aquavit2 are mutually exclusive' end end + + context 'in a group' do + it 'works when only one from the set is present' do + subject.params do + group :drink, type: Hash do + optional :wine + optional :beer + optional :juice + + mutually_exclusive :beer, :wine, :juice + end + end + subject.get '/mutually_exclusive_group' do + 'mutually_exclusive_group works!' + end + + get '/mutually_exclusive_group', drink: { beer: 'true' } + expect(last_response.status).to eq(200) + end + + it 'errors when more than one from the set is present' do + subject.params do + group :drink, type: Hash do + optional :wine + optional :beer + optional :juice + + mutually_exclusive :beer, :wine, :juice + end + end + subject.get '/mutually_exclusive_group' do + 'mutually_exclusive_group works!' + end + + get '/mutually_exclusive_group', drink: { beer: 'true', juice: 'true', wine: 'true' } + expect(last_response.status).to eq(400) + end + end end context 'exactly one of' do context 'params' do before :each do @@ -1000,11 +1111,11 @@ end it 'errors when none are present' do get '/exactly_one_of' expect(last_response.status).to eq(400) - expect(last_response.body).to eq "beer, wine, juice are missing, exactly one parameter must be provided" + expect(last_response.body).to eq 'beer, wine, juice are missing, exactly one parameter must be provided' end it 'succeeds when one is present' do get '/exactly_one_of', beer: 'string' expect(last_response.status).to eq(200) @@ -1012,13 +1123,53 @@ end it 'errors when two or more are present' do get '/exactly_one_of', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) - expect(last_response.body).to eq "beer, wine are mutually exclusive" + expect(last_response.body).to eq 'beer, wine are mutually exclusive' end end + + context 'nested params' do + before :each do + subject.params do + requires :nested, type: Hash do + optional :beer_nested + optional :wine_nested + optional :juice_nested + exactly_one_of :beer_nested, :wine_nested, :juice_nested + end + optional :nested2, type: Array do + optional :beer_nested2 + optional :wine_nested2 + optional :juice_nested2 + exactly_one_of :beer_nested2, :wine_nested2, :juice_nested2 + end + end + subject.get '/exactly_one_of_nested' do + 'exactly_one_of works!' + end + end + + it 'errors when none are present' do + get '/exactly_one_of_nested' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq 'nested is missing, beer_nested, wine_nested, juice_nested are missing, exactly one parameter must be provided' + end + + it 'succeeds when one is present' do + get '/exactly_one_of_nested', nested: { beer_nested: 'string' } + expect(last_response.status).to eq(200) + expect(last_response.body).to eq 'exactly_one_of works!' + end + + it 'errors when two or more are present' do + get '/exactly_one_of_nested', nested: { beer_nested: 'string' }, nested2: [{ beer_nested2: 'string', wine_nested2: 'anotherstring' }] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq 'beer_nested2, wine_nested2 are mutually exclusive' + end + end end context 'at least one of' do context 'params' do before :each do @@ -1034,11 +1185,11 @@ end it 'errors when none are present' do get '/at_least_one_of' expect(last_response.status).to eq(400) - expect(last_response.body).to eq "beer, wine, juice are missing, at least one parameter must be provided" + expect(last_response.body).to eq 'beer, wine, juice are missing, at least one parameter must be provided' end it 'does not error when one is present' do get '/at_least_one_of', beer: 'string' expect(last_response.status).to eq(200) @@ -1048,9 +1199,123 @@ it 'does not error when two are present' do get '/at_least_one_of', beer: 'string', wine: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end + end + + context 'nested params' do + before :each do + subject.params do + requires :nested, type: Hash do + optional :beer_nested + optional :wine_nested + optional :juice_nested + at_least_one_of :beer_nested, :wine_nested, :juice_nested + end + optional :nested2, type: Array do + optional :beer_nested2 + optional :wine_nested2 + optional :juice_nested2 + at_least_one_of :beer_nested2, :wine_nested2, :juice_nested2 + end + end + subject.get '/at_least_one_of_nested' do + 'at_least_one_of works!' + end + end + + it 'errors when none are present' do + get '/at_least_one_of_nested' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq 'nested is missing, beer_nested, wine_nested, juice_nested are missing, at least one parameter must be provided' + end + + it 'does not error when one is present' do + get '/at_least_one_of_nested', nested: { beer_nested: 'string' }, nested2: [{ beer_nested2: 'string' }] + expect(last_response.status).to eq(200) + expect(last_response.body).to eq 'at_least_one_of works!' + end + + it 'does not error when two are present' do + get '/at_least_one_of_nested', nested: { beer_nested: 'string', wine_nested: 'string' }, nested2: [{ beer_nested2: 'string', wine_nested2: 'string' }] + expect(last_response.status).to eq(200) + expect(last_response.body).to eq 'at_least_one_of works!' + end + end + end + + context 'in a group' do + it 'works when only one from the set is present' do + subject.params do + group :drink, type: Hash do + optional :wine + optional :beer + optional :juice + + exactly_one_of :beer, :wine, :juice + end + end + subject.get '/exactly_one_of_group' do + 'exactly_one_of_group works!' + end + + get '/exactly_one_of_group', drink: { beer: 'true' } + expect(last_response.status).to eq(200) + end + + it 'errors when no parameter from the set is present' do + subject.params do + group :drink, type: Hash do + optional :wine + optional :beer + optional :juice + + exactly_one_of :beer, :wine, :juice + end + end + subject.get '/exactly_one_of_group' do + 'exactly_one_of_group works!' + end + + get '/exactly_one_of_group', drink: {} + expect(last_response.status).to eq(400) + end + + it 'errors when more than one from the set is present' do + subject.params do + group :drink, type: Hash do + optional :wine + optional :beer + optional :juice + + exactly_one_of :beer, :wine, :juice + end + end + subject.get '/exactly_one_of_group' do + 'exactly_one_of_group works!' + end + + get '/exactly_one_of_group', drink: { beer: 'true', juice: 'true', wine: 'true' } + expect(last_response.status).to eq(400) + end + + it 'does not falsely think the param is there if it is provided outside the block' do + subject.params do + group :drink, type: Hash do + optional :wine + optional :beer + optional :juice + + exactly_one_of :beer, :wine, :juice + end + end + subject.get '/exactly_one_of_group' do + 'exactly_one_of_group works!' + end + + get '/exactly_one_of_group', drink: { foo: 'bar' }, beer: 'true' + expect(last_response.status).to eq(400) end end end end