grape-security 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +45 -0
- data/.rspec +2 -0
- data/.rubocop.yml +70 -0
- data/.travis.yml +18 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +314 -0
- data/CONTRIBUTING.md +118 -0
- data/Gemfile +21 -0
- data/Guardfile +14 -0
- data/LICENSE +20 -0
- data/README.md +1777 -0
- data/RELEASING.md +105 -0
- data/Rakefile +69 -0
- data/UPGRADING.md +124 -0
- data/grape-security.gemspec +39 -0
- data/grape.png +0 -0
- data/lib/grape.rb +99 -0
- data/lib/grape/api.rb +646 -0
- data/lib/grape/cookies.rb +39 -0
- data/lib/grape/endpoint.rb +533 -0
- data/lib/grape/error_formatter/base.rb +31 -0
- data/lib/grape/error_formatter/json.rb +15 -0
- data/lib/grape/error_formatter/txt.rb +16 -0
- data/lib/grape/error_formatter/xml.rb +15 -0
- data/lib/grape/exceptions/base.rb +66 -0
- data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
- data/lib/grape/exceptions/invalid_formatter.rb +10 -0
- data/lib/grape/exceptions/invalid_versioner_option.rb +10 -0
- data/lib/grape/exceptions/invalid_with_option_for_represent.rb +10 -0
- data/lib/grape/exceptions/missing_mime_type.rb +10 -0
- data/lib/grape/exceptions/missing_option.rb +10 -0
- data/lib/grape/exceptions/missing_vendor_option.rb +10 -0
- data/lib/grape/exceptions/unknown_options.rb +10 -0
- data/lib/grape/exceptions/unknown_validator.rb +10 -0
- data/lib/grape/exceptions/validation.rb +26 -0
- data/lib/grape/exceptions/validation_errors.rb +43 -0
- data/lib/grape/formatter/base.rb +31 -0
- data/lib/grape/formatter/json.rb +12 -0
- data/lib/grape/formatter/serializable_hash.rb +35 -0
- data/lib/grape/formatter/txt.rb +11 -0
- data/lib/grape/formatter/xml.rb +12 -0
- data/lib/grape/http/request.rb +26 -0
- data/lib/grape/locale/en.yml +32 -0
- data/lib/grape/middleware/auth/base.rb +30 -0
- data/lib/grape/middleware/auth/basic.rb +13 -0
- data/lib/grape/middleware/auth/digest.rb +13 -0
- data/lib/grape/middleware/auth/oauth2.rb +83 -0
- data/lib/grape/middleware/base.rb +62 -0
- data/lib/grape/middleware/error.rb +89 -0
- data/lib/grape/middleware/filter.rb +17 -0
- data/lib/grape/middleware/formatter.rb +150 -0
- data/lib/grape/middleware/globals.rb +13 -0
- data/lib/grape/middleware/versioner.rb +32 -0
- data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
- data/lib/grape/middleware/versioner/header.rb +132 -0
- data/lib/grape/middleware/versioner/param.rb +42 -0
- data/lib/grape/middleware/versioner/path.rb +52 -0
- data/lib/grape/namespace.rb +23 -0
- data/lib/grape/parser/base.rb +29 -0
- data/lib/grape/parser/json.rb +11 -0
- data/lib/grape/parser/xml.rb +11 -0
- data/lib/grape/path.rb +70 -0
- data/lib/grape/route.rb +27 -0
- data/lib/grape/util/content_types.rb +18 -0
- data/lib/grape/util/deep_merge.rb +23 -0
- data/lib/grape/util/hash_stack.rb +120 -0
- data/lib/grape/validations.rb +322 -0
- data/lib/grape/validations/coerce.rb +63 -0
- data/lib/grape/validations/default.rb +25 -0
- data/lib/grape/validations/exactly_one_of.rb +26 -0
- data/lib/grape/validations/mutual_exclusion.rb +25 -0
- data/lib/grape/validations/presence.rb +16 -0
- data/lib/grape/validations/regexp.rb +12 -0
- data/lib/grape/validations/values.rb +23 -0
- data/lib/grape/version.rb +3 -0
- data/spec/grape/api_spec.rb +2571 -0
- data/spec/grape/endpoint_spec.rb +784 -0
- data/spec/grape/entity_spec.rb +324 -0
- data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
- data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
- data/spec/grape/exceptions/missing_mime_type_spec.rb +18 -0
- data/spec/grape/exceptions/missing_option_spec.rb +18 -0
- data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
- data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
- data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
- data/spec/grape/middleware/auth/basic_spec.rb +31 -0
- data/spec/grape/middleware/auth/digest_spec.rb +47 -0
- data/spec/grape/middleware/auth/oauth2_spec.rb +135 -0
- data/spec/grape/middleware/base_spec.rb +58 -0
- data/spec/grape/middleware/error_spec.rb +45 -0
- data/spec/grape/middleware/exception_spec.rb +184 -0
- data/spec/grape/middleware/formatter_spec.rb +258 -0
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
- data/spec/grape/middleware/versioner/header_spec.rb +302 -0
- data/spec/grape/middleware/versioner/param_spec.rb +58 -0
- data/spec/grape/middleware/versioner/path_spec.rb +44 -0
- data/spec/grape/middleware/versioner_spec.rb +22 -0
- data/spec/grape/path_spec.rb +229 -0
- data/spec/grape/util/hash_stack_spec.rb +132 -0
- data/spec/grape/validations/coerce_spec.rb +208 -0
- data/spec/grape/validations/default_spec.rb +123 -0
- data/spec/grape/validations/exactly_one_of_spec.rb +71 -0
- data/spec/grape/validations/mutual_exclusion_spec.rb +61 -0
- data/spec/grape/validations/presence_spec.rb +142 -0
- data/spec/grape/validations/regexp_spec.rb +40 -0
- data/spec/grape/validations/values_spec.rb +152 -0
- data/spec/grape/validations/zh-CN.yml +10 -0
- data/spec/grape/validations_spec.rb +994 -0
- data/spec/shared/versioning_examples.rb +121 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/basic_auth_encode_helpers.rb +3 -0
- data/spec/support/content_type_helpers.rb +11 -0
- data/spec/support/versioned_helpers.rb +50 -0
- metadata +421 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Validations::PresenceValidator do
|
4
|
+
|
5
|
+
module ValidationsSpec
|
6
|
+
module PresenceValidatorSpec
|
7
|
+
class API < Grape::API
|
8
|
+
default_format :json
|
9
|
+
|
10
|
+
resource :bacons do
|
11
|
+
get do
|
12
|
+
"All the bacon"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
params do
|
17
|
+
requires :id, regexp: /^[0-9]+$/
|
18
|
+
end
|
19
|
+
post do
|
20
|
+
{ ret: params[:id] }
|
21
|
+
end
|
22
|
+
|
23
|
+
params do
|
24
|
+
requires :name, :company
|
25
|
+
end
|
26
|
+
get do
|
27
|
+
"Hello"
|
28
|
+
end
|
29
|
+
|
30
|
+
params do
|
31
|
+
requires :user, type: Hash do
|
32
|
+
requires :first_name
|
33
|
+
requires :last_name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
get '/nested' do
|
37
|
+
"Nested"
|
38
|
+
end
|
39
|
+
|
40
|
+
params do
|
41
|
+
requires :admin, type: Hash do
|
42
|
+
requires :admin_name
|
43
|
+
requires :super, type: Hash do
|
44
|
+
requires :user, type: Hash do
|
45
|
+
requires :first_name
|
46
|
+
requires :last_name
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
get '/nested_triple' do
|
52
|
+
"Nested triple"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def app
|
59
|
+
ValidationsSpec::PresenceValidatorSpec::API
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'does not validate for any params' do
|
63
|
+
get "/bacons"
|
64
|
+
expect(last_response.status).to eq(200)
|
65
|
+
expect(last_response.body).to eq("All the bacon".to_json)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'validates id' do
|
69
|
+
post '/'
|
70
|
+
expect(last_response.status).to eq(400)
|
71
|
+
expect(last_response.body).to eq('{"error":"id is missing"}')
|
72
|
+
|
73
|
+
io = StringIO.new('{"id" : "a56b"}')
|
74
|
+
post '/', {}, 'rack.input' => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length
|
75
|
+
expect(last_response.body).to eq('{"error":"id is invalid"}')
|
76
|
+
expect(last_response.status).to eq(400)
|
77
|
+
|
78
|
+
io = StringIO.new('{"id" : 56}')
|
79
|
+
post '/', {}, 'rack.input' => io, 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => io.length
|
80
|
+
expect(last_response.body).to eq('{"ret":56}')
|
81
|
+
expect(last_response.status).to eq(201)
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'validates name, company' do
|
85
|
+
get '/'
|
86
|
+
expect(last_response.status).to eq(400)
|
87
|
+
expect(last_response.body).to eq('{"error":"name is missing"}')
|
88
|
+
|
89
|
+
get '/', name: "Bob"
|
90
|
+
expect(last_response.status).to eq(400)
|
91
|
+
expect(last_response.body).to eq('{"error":"company is missing"}')
|
92
|
+
|
93
|
+
get '/', name: "Bob", company: "TestCorp"
|
94
|
+
expect(last_response.status).to eq(200)
|
95
|
+
expect(last_response.body).to eq("Hello".to_json)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'validates nested parameters' do
|
99
|
+
get '/nested'
|
100
|
+
expect(last_response.status).to eq(400)
|
101
|
+
expect(last_response.body).to eq('{"error":"user is missing, user[first_name] is missing, user[last_name] is missing"}')
|
102
|
+
|
103
|
+
get '/nested', user: { first_name: "Billy" }
|
104
|
+
expect(last_response.status).to eq(400)
|
105
|
+
expect(last_response.body).to eq('{"error":"user[last_name] is missing"}')
|
106
|
+
|
107
|
+
get '/nested', user: { first_name: "Billy", last_name: "Bob" }
|
108
|
+
expect(last_response.status).to eq(200)
|
109
|
+
expect(last_response.body).to eq("Nested".to_json)
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'validates triple nested parameters' do
|
113
|
+
get '/nested_triple'
|
114
|
+
expect(last_response.status).to eq(400)
|
115
|
+
expect(last_response.body).to include '{"error":"admin is missing'
|
116
|
+
|
117
|
+
get '/nested_triple', user: { first_name: "Billy" }
|
118
|
+
expect(last_response.status).to eq(400)
|
119
|
+
expect(last_response.body).to include '{"error":"admin is missing'
|
120
|
+
|
121
|
+
get '/nested_triple', admin: { super: { first_name: "Billy" } }
|
122
|
+
expect(last_response.status).to eq(400)
|
123
|
+
expect(last_response.body).to eq('{"error":"admin[admin_name] is missing, admin[super][user] is missing, admin[super][user][first_name] is missing, admin[super][user][last_name] is missing"}')
|
124
|
+
|
125
|
+
get '/nested_triple', super: { user: { first_name: "Billy", last_name: "Bob" } }
|
126
|
+
expect(last_response.status).to eq(400)
|
127
|
+
expect(last_response.body).to include '{"error":"admin is missing'
|
128
|
+
|
129
|
+
get '/nested_triple', admin: { super: { user: { first_name: "Billy" } } }
|
130
|
+
expect(last_response.status).to eq(400)
|
131
|
+
expect(last_response.body).to eq('{"error":"admin[admin_name] is missing, admin[super][user][last_name] is missing"}')
|
132
|
+
|
133
|
+
get '/nested_triple', admin: { admin_name: 'admin', super: { user: { first_name: "Billy" } } }
|
134
|
+
expect(last_response.status).to eq(400)
|
135
|
+
expect(last_response.body).to eq('{"error":"admin[super][user][last_name] is missing"}')
|
136
|
+
|
137
|
+
get '/nested_triple', admin: { admin_name: 'admin', super: { user: { first_name: "Billy", last_name: "Bob" } } }
|
138
|
+
expect(last_response.status).to eq(200)
|
139
|
+
expect(last_response.body).to eq("Nested triple".to_json)
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Validations::RegexpValidator do
|
4
|
+
module ValidationsSpec
|
5
|
+
module RegexpValidatorSpec
|
6
|
+
class API < Grape::API
|
7
|
+
default_format :json
|
8
|
+
|
9
|
+
params do
|
10
|
+
requires :name, regexp: /^[a-z]+$/
|
11
|
+
end
|
12
|
+
get do
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def app
|
20
|
+
ValidationsSpec::RegexpValidatorSpec::API
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'invalid input' do
|
24
|
+
it 'refuses inapppopriate' do
|
25
|
+
get '/', name: "invalid name"
|
26
|
+
expect(last_response.status).to eq(400)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'refuses nil' do
|
30
|
+
get '/', name: nil
|
31
|
+
expect(last_response.status).to eq(400)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'accepts valid input' do
|
36
|
+
get '/', name: "bob"
|
37
|
+
expect(last_response.status).to eq(200)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Validations::ValuesValidator do
|
4
|
+
|
5
|
+
class ValuesModel
|
6
|
+
DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3']
|
7
|
+
class << self
|
8
|
+
def values
|
9
|
+
@values ||= []
|
10
|
+
[DEFAULT_VALUES + @values].flatten.uniq
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_value(value)
|
14
|
+
@values ||= []
|
15
|
+
@values << value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ValidationsSpec
|
21
|
+
module ValuesValidatorSpec
|
22
|
+
class API < Grape::API
|
23
|
+
default_format :json
|
24
|
+
|
25
|
+
params do
|
26
|
+
requires :type, values: ValuesModel.values
|
27
|
+
end
|
28
|
+
get '/' do
|
29
|
+
{ type: params[:type] }
|
30
|
+
end
|
31
|
+
|
32
|
+
params do
|
33
|
+
optional :type, values: ValuesModel.values, default: 'valid-type2'
|
34
|
+
end
|
35
|
+
get '/default/valid' do
|
36
|
+
{ type: params[:type] }
|
37
|
+
end
|
38
|
+
|
39
|
+
params do
|
40
|
+
optional :type, values: -> { ValuesModel.values }, default: 'valid-type2'
|
41
|
+
end
|
42
|
+
get '/lambda' do
|
43
|
+
{ type: params[:type] }
|
44
|
+
end
|
45
|
+
|
46
|
+
params do
|
47
|
+
requires :type, type: Integer, desc: "An integer", values: [10, 11], default: 10
|
48
|
+
end
|
49
|
+
get '/values/coercion' do
|
50
|
+
{ type: params[:type] }
|
51
|
+
end
|
52
|
+
|
53
|
+
params do
|
54
|
+
optional :optional do
|
55
|
+
requires :type, values: ["a", "b"]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
get '/optional_with_required_values'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def app
|
64
|
+
ValidationsSpec::ValuesValidatorSpec::API
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'allows a valid value for a parameter' do
|
68
|
+
get("/", type: 'valid-type1')
|
69
|
+
expect(last_response.status).to eq 200
|
70
|
+
expect(last_response.body).to eq({ type: "valid-type1" }.to_json)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'does not allow an invalid value for a parameter' do
|
74
|
+
get("/", type: 'invalid-type')
|
75
|
+
expect(last_response.status).to eq 400
|
76
|
+
expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'nil value for a parameter' do
|
80
|
+
it 'does not allow for root params scope' do
|
81
|
+
get("/", type: nil)
|
82
|
+
expect(last_response.status).to eq 400
|
83
|
+
expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'allows for a required param in child scope' do
|
87
|
+
get('/optional_with_required_values')
|
88
|
+
expect(last_response.status).to eq 200
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'allows a valid default value' do
|
93
|
+
get("/default/valid")
|
94
|
+
expect(last_response.status).to eq 200
|
95
|
+
expect(last_response.body).to eq({ type: "valid-type2" }.to_json)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'allows a proc for values' do
|
99
|
+
get('/lambda', type: 'valid-type1')
|
100
|
+
expect(last_response.status).to eq 200
|
101
|
+
expect(last_response.body).to eq({ type: "valid-type1" }.to_json)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'does not validate updated values without proc' do
|
105
|
+
ValuesModel.add_value('valid-type4')
|
106
|
+
|
107
|
+
get('/', type: 'valid-type4')
|
108
|
+
expect(last_response.status).to eq 400
|
109
|
+
expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'validates against values in a proc' do
|
113
|
+
ValuesModel.add_value('valid-type4')
|
114
|
+
|
115
|
+
get('/lambda', type: 'valid-type4')
|
116
|
+
expect(last_response.status).to eq 200
|
117
|
+
expect(last_response.body).to eq({ type: "valid-type4" }.to_json)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'does not allow an invalid value for a parameter using lambda' do
|
121
|
+
get("/lambda", type: 'invalid-type')
|
122
|
+
expect(last_response.status).to eq 400
|
123
|
+
expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'raises IncompatibleOptionValues on an invalid default value' do
|
127
|
+
subject = Class.new(Grape::API)
|
128
|
+
expect {
|
129
|
+
subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: 'invalid-type' }
|
130
|
+
}.to raise_error Grape::Exceptions::IncompatibleOptionValues
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'raises IncompatibleOptionValues when type is incompatible with values array' do
|
134
|
+
subject = Class.new(Grape::API)
|
135
|
+
expect {
|
136
|
+
subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol }
|
137
|
+
}.to raise_error Grape::Exceptions::IncompatibleOptionValues
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'allows values to be a kind of the coerced type not just an instance of it' do
|
141
|
+
get("/values/coercion", type: 10)
|
142
|
+
expect(last_response.status).to eq 200
|
143
|
+
expect(last_response.body).to eq({ type: 10 }.to_json)
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'raises IncompatibleOptionValues when values contains a value that is not a kind of the type' do
|
147
|
+
subject = Class.new(Grape::API)
|
148
|
+
expect {
|
149
|
+
subject.params { requires :type, values: [10.5, 11], type: Integer }
|
150
|
+
}.to raise_error Grape::Exceptions::IncompatibleOptionValues
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,994 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Validations do
|
4
|
+
|
5
|
+
subject { Class.new(Grape::API) }
|
6
|
+
|
7
|
+
def app
|
8
|
+
subject
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'params' do
|
12
|
+
context 'optional' do
|
13
|
+
it 'validates when params is present' do
|
14
|
+
subject.params do
|
15
|
+
optional :a_number, regexp: /^[0-9]+$/
|
16
|
+
end
|
17
|
+
subject.get '/optional' do
|
18
|
+
'optional works!'
|
19
|
+
end
|
20
|
+
|
21
|
+
get '/optional', a_number: 'string'
|
22
|
+
expect(last_response.status).to eq(400)
|
23
|
+
expect(last_response.body).to eq('a_number is invalid')
|
24
|
+
|
25
|
+
get '/optional', a_number: 45
|
26
|
+
expect(last_response.status).to eq(200)
|
27
|
+
expect(last_response.body).to eq('optional works!')
|
28
|
+
end
|
29
|
+
|
30
|
+
it "doesn't validate when param not present" do
|
31
|
+
subject.params do
|
32
|
+
optional :a_number, regexp: /^[0-9]+$/
|
33
|
+
end
|
34
|
+
subject.get '/optional' do
|
35
|
+
'optional works!'
|
36
|
+
end
|
37
|
+
|
38
|
+
get '/optional'
|
39
|
+
expect(last_response.status).to eq(200)
|
40
|
+
expect(last_response.body).to eq('optional works!')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'adds to declared parameters' do
|
44
|
+
subject.params do
|
45
|
+
optional :some_param
|
46
|
+
end
|
47
|
+
expect(subject.settings[:declared_params]).to eq([:some_param])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'required' do
|
52
|
+
before do
|
53
|
+
subject.params do
|
54
|
+
requires :key
|
55
|
+
end
|
56
|
+
subject.get '/required' do
|
57
|
+
'required works'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'errors when param not present' do
|
62
|
+
get '/required'
|
63
|
+
expect(last_response.status).to eq(400)
|
64
|
+
expect(last_response.body).to eq('key is missing')
|
65
|
+
end
|
66
|
+
|
67
|
+
it "doesn't throw a missing param when param is present" do
|
68
|
+
get '/required', key: 'cool'
|
69
|
+
expect(last_response.status).to eq(200)
|
70
|
+
expect(last_response.body).to eq('required works')
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'adds to declared parameters' do
|
74
|
+
subject.params do
|
75
|
+
requires :some_param
|
76
|
+
end
|
77
|
+
expect(subject.settings[:declared_params]).to eq([:some_param])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'requires :all using Grape::Entity documentation' do
|
82
|
+
def define_requires_all
|
83
|
+
documentation = {
|
84
|
+
required_field: { type: String },
|
85
|
+
optional_field: { type: String }
|
86
|
+
}
|
87
|
+
subject.params do
|
88
|
+
requires :all, except: :optional_field, using: documentation
|
89
|
+
end
|
90
|
+
end
|
91
|
+
before do
|
92
|
+
define_requires_all
|
93
|
+
subject.get '/required' do
|
94
|
+
'required works'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'adds entity documentation to declared params' do
|
99
|
+
define_requires_all
|
100
|
+
expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field])
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'errors when required_field is not present' do
|
104
|
+
get '/required'
|
105
|
+
expect(last_response.status).to eq(400)
|
106
|
+
expect(last_response.body).to eq('required_field is missing')
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'works when required_field is present' do
|
110
|
+
get '/required', required_field: 'woof'
|
111
|
+
expect(last_response.status).to eq(200)
|
112
|
+
expect(last_response.body).to eq('required works')
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'requires :none using Grape::Entity documentation' do
|
117
|
+
def define_requires_none
|
118
|
+
documentation = {
|
119
|
+
required_field: { type: String },
|
120
|
+
optional_field: { type: String }
|
121
|
+
}
|
122
|
+
subject.params do
|
123
|
+
requires :none, except: :required_field, using: documentation
|
124
|
+
end
|
125
|
+
end
|
126
|
+
before do
|
127
|
+
define_requires_none
|
128
|
+
subject.get '/required' do
|
129
|
+
'required works'
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'adds entity documentation to declared params' do
|
134
|
+
define_requires_none
|
135
|
+
expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field])
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'errors when required_field is not present' do
|
139
|
+
get '/required'
|
140
|
+
expect(last_response.status).to eq(400)
|
141
|
+
expect(last_response.body).to eq('required_field is missing')
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'works when required_field is present' do
|
145
|
+
get '/required', required_field: 'woof'
|
146
|
+
expect(last_response.status).to eq(200)
|
147
|
+
expect(last_response.body).to eq('required works')
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
context 'requires :all or :none but except a non-existent field using Grape::Entity documentation' do
|
152
|
+
context 'requires :all' do
|
153
|
+
def define_requires_all
|
154
|
+
documentation = {
|
155
|
+
required_field: { type: String },
|
156
|
+
optional_field: { type: String }
|
157
|
+
}
|
158
|
+
subject.params do
|
159
|
+
requires :all, except: :non_existent_field, using: documentation
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'adds only the entity documentation to declared params, nothing more' do
|
164
|
+
define_requires_all
|
165
|
+
expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field])
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
context 'requires :none' do
|
170
|
+
def define_requires_none
|
171
|
+
documentation = {
|
172
|
+
required_field: { type: String },
|
173
|
+
optional_field: { type: String }
|
174
|
+
}
|
175
|
+
subject.params do
|
176
|
+
requires :none, except: :non_existent_field, using: documentation
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'adds only the entity documentation to declared params, nothing more' do
|
181
|
+
expect { define_requires_none }.to raise_error(ArgumentError)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context 'required with an Array block' do
|
187
|
+
before do
|
188
|
+
subject.params do
|
189
|
+
requires :items, type: Array do
|
190
|
+
requires :key
|
191
|
+
end
|
192
|
+
end
|
193
|
+
subject.get '/required' do
|
194
|
+
'required works'
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'errors when param not present' do
|
199
|
+
get '/required'
|
200
|
+
expect(last_response.status).to eq(400)
|
201
|
+
expect(last_response.body).to eq('items is missing')
|
202
|
+
end
|
203
|
+
|
204
|
+
it "errors when param is not an Array" do
|
205
|
+
get '/required', items: "hello"
|
206
|
+
expect(last_response.status).to eq(400)
|
207
|
+
expect(last_response.body).to eq('items is invalid, items[key] is missing')
|
208
|
+
|
209
|
+
get '/required', items: { key: 'foo' }
|
210
|
+
expect(last_response.status).to eq(400)
|
211
|
+
expect(last_response.body).to eq('items is invalid')
|
212
|
+
end
|
213
|
+
|
214
|
+
it "doesn't throw a missing param when param is present" do
|
215
|
+
get '/required', items: [{ key: 'hello' }, { key: 'world' }]
|
216
|
+
expect(last_response.status).to eq(200)
|
217
|
+
expect(last_response.body).to eq('required works')
|
218
|
+
end
|
219
|
+
|
220
|
+
it "doesn't allow any key in the options hash other than type" do
|
221
|
+
expect {
|
222
|
+
subject.params do
|
223
|
+
requires(:items, desc: 'Foo') do
|
224
|
+
requires :key
|
225
|
+
end
|
226
|
+
end
|
227
|
+
}.to raise_error ArgumentError
|
228
|
+
end
|
229
|
+
|
230
|
+
it 'adds to declared parameters' do
|
231
|
+
subject.params do
|
232
|
+
requires :items do
|
233
|
+
requires :key
|
234
|
+
end
|
235
|
+
end
|
236
|
+
expect(subject.settings[:declared_params]).to eq([items: [:key]])
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
context 'required with a Hash block' do
|
241
|
+
before do
|
242
|
+
subject.params do
|
243
|
+
requires :items, type: Hash do
|
244
|
+
requires :key
|
245
|
+
end
|
246
|
+
end
|
247
|
+
subject.get '/required' do
|
248
|
+
'required works'
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
it 'errors when param not present' do
|
253
|
+
get '/required'
|
254
|
+
expect(last_response.status).to eq(400)
|
255
|
+
expect(last_response.body).to eq('items is missing, items[key] is missing')
|
256
|
+
end
|
257
|
+
|
258
|
+
it "errors when param is not a Hash" do
|
259
|
+
get '/required', items: "hello"
|
260
|
+
expect(last_response.status).to eq(400)
|
261
|
+
expect(last_response.body).to eq('items is invalid, items[key] is missing')
|
262
|
+
|
263
|
+
get '/required', items: [{ key: 'foo' }]
|
264
|
+
expect(last_response.status).to eq(400)
|
265
|
+
expect(last_response.body).to eq('items is invalid')
|
266
|
+
end
|
267
|
+
|
268
|
+
it "doesn't throw a missing param when param is present" do
|
269
|
+
get '/required', items: { key: 'hello' }
|
270
|
+
expect(last_response.status).to eq(200)
|
271
|
+
expect(last_response.body).to eq('required works')
|
272
|
+
end
|
273
|
+
|
274
|
+
it "doesn't allow any key in the options hash other than type" do
|
275
|
+
expect {
|
276
|
+
subject.params do
|
277
|
+
requires(:items, desc: 'Foo') do
|
278
|
+
requires :key
|
279
|
+
end
|
280
|
+
end
|
281
|
+
}.to raise_error ArgumentError
|
282
|
+
end
|
283
|
+
|
284
|
+
it 'adds to declared parameters' do
|
285
|
+
subject.params do
|
286
|
+
requires :items do
|
287
|
+
requires :key
|
288
|
+
end
|
289
|
+
end
|
290
|
+
expect(subject.settings[:declared_params]).to eq([items: [:key]])
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
context 'group' do
|
295
|
+
before do
|
296
|
+
subject.params do
|
297
|
+
group :items do
|
298
|
+
requires :key
|
299
|
+
end
|
300
|
+
end
|
301
|
+
subject.get '/required' do
|
302
|
+
'required works'
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'errors when param not present' do
|
307
|
+
get '/required'
|
308
|
+
expect(last_response.status).to eq(400)
|
309
|
+
expect(last_response.body).to eq('items is missing')
|
310
|
+
end
|
311
|
+
|
312
|
+
it "doesn't throw a missing param when param is present" do
|
313
|
+
get '/required', items: [key: 'hello', key: 'world']
|
314
|
+
expect(last_response.status).to eq(200)
|
315
|
+
expect(last_response.body).to eq('required works')
|
316
|
+
end
|
317
|
+
|
318
|
+
it 'adds to declared parameters' do
|
319
|
+
subject.params do
|
320
|
+
group :items do
|
321
|
+
requires :key
|
322
|
+
end
|
323
|
+
end
|
324
|
+
expect(subject.settings[:declared_params]).to eq([items: [:key]])
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
context 'validation within arrays' do
|
329
|
+
before do
|
330
|
+
subject.params do
|
331
|
+
group :children do
|
332
|
+
requires :name
|
333
|
+
group :parents do
|
334
|
+
requires :name
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
subject.get '/within_array' do
|
339
|
+
'within array works'
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
it 'can handle new scopes within child elements' do
|
344
|
+
get '/within_array', children: [
|
345
|
+
{ name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] },
|
346
|
+
{ name: 'Joe', parents: [{ name: 'Josie' }] }
|
347
|
+
]
|
348
|
+
expect(last_response.status).to eq(200)
|
349
|
+
expect(last_response.body).to eq('within array works')
|
350
|
+
end
|
351
|
+
|
352
|
+
it 'errors when a parameter is not present' do
|
353
|
+
get '/within_array', children: [
|
354
|
+
{ name: 'Jim', parents: [{}] },
|
355
|
+
{ name: 'Job', parents: [{ name: 'Joy' }] }
|
356
|
+
]
|
357
|
+
# NOTE: with body parameters in json or XML or similar this
|
358
|
+
# should actually fail with: children[parents][name] is missing.
|
359
|
+
expect(last_response.status).to eq(400)
|
360
|
+
expect(last_response.body).to eq('children[parents] is missing')
|
361
|
+
end
|
362
|
+
|
363
|
+
it 'safely handles empty arrays and blank parameters' do
|
364
|
+
# NOTE: with body parameters in json or XML or similar this
|
365
|
+
# should actually return 200, since an empty array is valid.
|
366
|
+
get '/within_array', children: []
|
367
|
+
expect(last_response.status).to eq(400)
|
368
|
+
expect(last_response.body).to eq('children is missing')
|
369
|
+
get '/within_array', children: [name: 'Jay']
|
370
|
+
expect(last_response.status).to eq(400)
|
371
|
+
expect(last_response.body).to eq('children[parents] is missing')
|
372
|
+
end
|
373
|
+
|
374
|
+
it "errors when param is not an Array" do
|
375
|
+
# NOTE: would be nicer if these just returned 'children is invalid'
|
376
|
+
get '/within_array', children: "hello"
|
377
|
+
expect(last_response.status).to eq(400)
|
378
|
+
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')
|
379
|
+
|
380
|
+
get '/within_array', children: { name: 'foo' }
|
381
|
+
expect(last_response.status).to eq(400)
|
382
|
+
expect(last_response.body).to eq('children is invalid, children[parents] is missing')
|
383
|
+
|
384
|
+
get '/within_array', children: [name: 'Jay', parents: { name: 'Fred' }]
|
385
|
+
expect(last_response.status).to eq(400)
|
386
|
+
expect(last_response.body).to eq('children[parents] is invalid')
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
context 'with block param' do
|
391
|
+
before do
|
392
|
+
subject.params do
|
393
|
+
requires :planets do
|
394
|
+
requires :name
|
395
|
+
end
|
396
|
+
end
|
397
|
+
subject.get '/req' do
|
398
|
+
'within array works'
|
399
|
+
end
|
400
|
+
subject.put '/req' do
|
401
|
+
''
|
402
|
+
end
|
403
|
+
|
404
|
+
subject.params do
|
405
|
+
group :stars do
|
406
|
+
requires :name
|
407
|
+
end
|
408
|
+
end
|
409
|
+
subject.get '/grp' do
|
410
|
+
'within array works'
|
411
|
+
end
|
412
|
+
subject.put '/grp' do
|
413
|
+
''
|
414
|
+
end
|
415
|
+
|
416
|
+
subject.params do
|
417
|
+
requires :name
|
418
|
+
optional :moons do
|
419
|
+
requires :name
|
420
|
+
end
|
421
|
+
end
|
422
|
+
subject.get '/opt' do
|
423
|
+
'within array works'
|
424
|
+
end
|
425
|
+
subject.put '/opt' do
|
426
|
+
''
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
it 'requires defaults to Array type' do
|
431
|
+
get '/req', planets: "Jupiter, Saturn"
|
432
|
+
expect(last_response.status).to eq(400)
|
433
|
+
expect(last_response.body).to eq('planets is invalid, planets[name] is missing')
|
434
|
+
|
435
|
+
get '/req', planets: { name: 'Jupiter' }
|
436
|
+
expect(last_response.status).to eq(400)
|
437
|
+
expect(last_response.body).to eq('planets is invalid')
|
438
|
+
|
439
|
+
get '/req', planets: [{ name: 'Venus' }, { name: 'Mars' }]
|
440
|
+
expect(last_response.status).to eq(200)
|
441
|
+
|
442
|
+
put_with_json '/req', planets: []
|
443
|
+
expect(last_response.status).to eq(200)
|
444
|
+
end
|
445
|
+
|
446
|
+
it 'optional defaults to Array type' do
|
447
|
+
get '/opt', name: "Jupiter", moons: "Europa, Ganymede"
|
448
|
+
expect(last_response.status).to eq(400)
|
449
|
+
expect(last_response.body).to eq('moons is invalid, moons[name] is missing')
|
450
|
+
|
451
|
+
get '/opt', name: "Jupiter", moons: { name: 'Ganymede' }
|
452
|
+
expect(last_response.status).to eq(400)
|
453
|
+
expect(last_response.body).to eq('moons is invalid')
|
454
|
+
|
455
|
+
get '/opt', name: "Jupiter", moons: [{ name: 'Io' }, { name: 'Callisto' }]
|
456
|
+
expect(last_response.status).to eq(200)
|
457
|
+
|
458
|
+
put_with_json '/opt', name: "Venus"
|
459
|
+
expect(last_response.status).to eq(200)
|
460
|
+
|
461
|
+
put_with_json '/opt', name: "Mercury", moons: []
|
462
|
+
expect(last_response.status).to eq(200)
|
463
|
+
end
|
464
|
+
|
465
|
+
it 'group defaults to Array type' do
|
466
|
+
get '/grp', stars: "Sun"
|
467
|
+
expect(last_response.status).to eq(400)
|
468
|
+
expect(last_response.body).to eq('stars is invalid, stars[name] is missing')
|
469
|
+
|
470
|
+
get '/grp', stars: { name: 'Sun' }
|
471
|
+
expect(last_response.status).to eq(400)
|
472
|
+
expect(last_response.body).to eq('stars is invalid')
|
473
|
+
|
474
|
+
get '/grp', stars: [{ name: 'Sun' }]
|
475
|
+
expect(last_response.status).to eq(200)
|
476
|
+
|
477
|
+
put_with_json '/grp', stars: []
|
478
|
+
expect(last_response.status).to eq(200)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
context 'validation within arrays with JSON' do
|
483
|
+
before do
|
484
|
+
subject.params do
|
485
|
+
group :children do
|
486
|
+
requires :name
|
487
|
+
group :parents do
|
488
|
+
requires :name
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
subject.put '/within_array' do
|
493
|
+
'within array works'
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
it 'can handle new scopes within child elements' do
|
498
|
+
put_with_json '/within_array', children: [
|
499
|
+
{ name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] },
|
500
|
+
{ name: 'Joe', parents: [{ name: 'Josie' }] }
|
501
|
+
]
|
502
|
+
expect(last_response.status).to eq(200)
|
503
|
+
expect(last_response.body).to eq('within array works')
|
504
|
+
end
|
505
|
+
|
506
|
+
it 'errors when a parameter is not present' do
|
507
|
+
put_with_json '/within_array', children: [
|
508
|
+
{ name: 'Jim', parents: [{}] },
|
509
|
+
{ name: 'Job', parents: [{ name: 'Joy' }] }
|
510
|
+
]
|
511
|
+
expect(last_response.status).to eq(400)
|
512
|
+
expect(last_response.body).to eq('children[parents][name] is missing')
|
513
|
+
end
|
514
|
+
|
515
|
+
it 'safely handles empty arrays and blank parameters' do
|
516
|
+
put_with_json '/within_array', children: []
|
517
|
+
expect(last_response.status).to eq(200)
|
518
|
+
put_with_json '/within_array', children: [name: 'Jay']
|
519
|
+
expect(last_response.status).to eq(400)
|
520
|
+
expect(last_response.body).to eq('children[parents] is missing')
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
context 'optional with an Array block' do
|
525
|
+
before do
|
526
|
+
subject.params do
|
527
|
+
optional :items, type: Array do
|
528
|
+
requires :key
|
529
|
+
end
|
530
|
+
end
|
531
|
+
subject.get '/optional_group' do
|
532
|
+
'optional group works'
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
it "doesn't throw a missing param when the group isn't present" do
|
537
|
+
get '/optional_group'
|
538
|
+
expect(last_response.status).to eq(200)
|
539
|
+
expect(last_response.body).to eq('optional group works')
|
540
|
+
end
|
541
|
+
|
542
|
+
it "doesn't throw a missing param when both group and param are given" do
|
543
|
+
get '/optional_group', items: [{ key: 'foo' }]
|
544
|
+
expect(last_response.status).to eq(200)
|
545
|
+
expect(last_response.body).to eq('optional group works')
|
546
|
+
end
|
547
|
+
|
548
|
+
it "errors when group is present, but required param is not" do
|
549
|
+
get '/optional_group', items: [{ not_key: 'foo' }]
|
550
|
+
expect(last_response.status).to eq(400)
|
551
|
+
expect(last_response.body).to eq('items[key] is missing')
|
552
|
+
end
|
553
|
+
|
554
|
+
it "errors when param is present but isn't an Array" do
|
555
|
+
get '/optional_group', items: "hello"
|
556
|
+
expect(last_response.status).to eq(400)
|
557
|
+
expect(last_response.body).to eq('items is invalid, items[key] is missing')
|
558
|
+
|
559
|
+
get '/optional_group', items: { key: 'foo' }
|
560
|
+
expect(last_response.status).to eq(400)
|
561
|
+
expect(last_response.body).to eq('items is invalid')
|
562
|
+
end
|
563
|
+
|
564
|
+
it 'adds to declared parameters' do
|
565
|
+
subject.params do
|
566
|
+
optional :items do
|
567
|
+
requires :key
|
568
|
+
end
|
569
|
+
end
|
570
|
+
expect(subject.settings[:declared_params]).to eq([items: [:key]])
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
context 'nested optional Array blocks' do
|
575
|
+
before do
|
576
|
+
subject.params do
|
577
|
+
optional :items, type: Array do
|
578
|
+
requires :key
|
579
|
+
optional(:optional_subitems, type: Array) { requires :value }
|
580
|
+
requires(:required_subitems, type: Array) { requires :value }
|
581
|
+
end
|
582
|
+
end
|
583
|
+
subject.get('/nested_optional_group') { 'nested optional group works' }
|
584
|
+
end
|
585
|
+
|
586
|
+
it 'does no internal validations if the outer group is blank' do
|
587
|
+
get '/nested_optional_group'
|
588
|
+
expect(last_response.status).to eq(200)
|
589
|
+
expect(last_response.body).to eq('nested optional group works')
|
590
|
+
end
|
591
|
+
|
592
|
+
it 'does internal validations if the outer group is present' do
|
593
|
+
get '/nested_optional_group', items: [{ key: 'foo' }]
|
594
|
+
expect(last_response.status).to eq(400)
|
595
|
+
expect(last_response.body).to eq('items[required_subitems] is missing')
|
596
|
+
|
597
|
+
get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
|
598
|
+
expect(last_response.status).to eq(200)
|
599
|
+
expect(last_response.body).to eq('nested optional group works')
|
600
|
+
end
|
601
|
+
|
602
|
+
it 'handles deep nesting' do
|
603
|
+
get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }]
|
604
|
+
expect(last_response.status).to eq(400)
|
605
|
+
expect(last_response.body).to eq('items[optional_subitems][value] is missing')
|
606
|
+
|
607
|
+
get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ value: 'baz' }] }]
|
608
|
+
expect(last_response.status).to eq(200)
|
609
|
+
expect(last_response.body).to eq('nested optional group works')
|
610
|
+
end
|
611
|
+
|
612
|
+
it 'handles validation within arrays' do
|
613
|
+
get '/nested_optional_group', items: [{ key: 'foo' }]
|
614
|
+
expect(last_response.status).to eq(400)
|
615
|
+
expect(last_response.body).to eq('items[required_subitems] is missing')
|
616
|
+
|
617
|
+
get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
|
618
|
+
expect(last_response.status).to eq(200)
|
619
|
+
expect(last_response.body).to eq('nested optional group works')
|
620
|
+
|
621
|
+
get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }]
|
622
|
+
expect(last_response.status).to eq(400)
|
623
|
+
expect(last_response.body).to eq('items[optional_subitems][value] is missing')
|
624
|
+
end
|
625
|
+
|
626
|
+
it 'adds to declared parameters' do
|
627
|
+
subject.params do
|
628
|
+
optional :items do
|
629
|
+
requires :key
|
630
|
+
optional(:optional_subitems) { requires :value }
|
631
|
+
requires(:required_subitems) { requires :value }
|
632
|
+
end
|
633
|
+
end
|
634
|
+
expect(subject.settings[:declared_params]).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]])
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
context 'multiple validation errors' do
|
639
|
+
before do
|
640
|
+
subject.params do
|
641
|
+
requires :yolo
|
642
|
+
requires :swag
|
643
|
+
end
|
644
|
+
subject.get '/two_required' do
|
645
|
+
'two required works'
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
it 'throws the validation errors' do
|
650
|
+
get '/two_required'
|
651
|
+
expect(last_response.status).to eq(400)
|
652
|
+
expect(last_response.body).to match(/yolo is missing/)
|
653
|
+
expect(last_response.body).to match(/swag is missing/)
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
context 'custom validation' do
|
658
|
+
module CustomValidations
|
659
|
+
class Customvalidator < Grape::Validations::Validator
|
660
|
+
def validate_param!(attr_name, params)
|
661
|
+
unless params[attr_name] == 'im custom'
|
662
|
+
raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message: "is not custom!"
|
663
|
+
end
|
664
|
+
end
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
context 'when using optional with a custom validator' do
|
669
|
+
before do
|
670
|
+
subject.params do
|
671
|
+
optional :custom, customvalidator: true
|
672
|
+
end
|
673
|
+
subject.get '/optional_custom' do
|
674
|
+
'optional with custom works!'
|
675
|
+
end
|
676
|
+
end
|
677
|
+
|
678
|
+
it 'validates when param is present' do
|
679
|
+
get '/optional_custom', custom: 'im custom'
|
680
|
+
expect(last_response.status).to eq(200)
|
681
|
+
expect(last_response.body).to eq('optional with custom works!')
|
682
|
+
|
683
|
+
get '/optional_custom', custom: 'im wrong'
|
684
|
+
expect(last_response.status).to eq(400)
|
685
|
+
expect(last_response.body).to eq('custom is not custom!')
|
686
|
+
end
|
687
|
+
|
688
|
+
it "skips validation when parameter isn't present" do
|
689
|
+
get '/optional_custom'
|
690
|
+
expect(last_response.status).to eq(200)
|
691
|
+
expect(last_response.body).to eq('optional with custom works!')
|
692
|
+
end
|
693
|
+
|
694
|
+
it 'validates with custom validator when param present and incorrect type' do
|
695
|
+
subject.params do
|
696
|
+
optional :custom, type: String, customvalidator: true
|
697
|
+
end
|
698
|
+
|
699
|
+
get '/optional_custom', custom: 123
|
700
|
+
expect(last_response.status).to eq(400)
|
701
|
+
expect(last_response.body).to eq('custom is not custom!')
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
context 'when using requires with a custom validator' do
|
706
|
+
before do
|
707
|
+
subject.params do
|
708
|
+
requires :custom, customvalidator: true
|
709
|
+
end
|
710
|
+
subject.get '/required_custom' do
|
711
|
+
'required with custom works!'
|
712
|
+
end
|
713
|
+
end
|
714
|
+
|
715
|
+
it 'validates when param is present' do
|
716
|
+
get '/required_custom', custom: 'im wrong, validate me'
|
717
|
+
expect(last_response.status).to eq(400)
|
718
|
+
expect(last_response.body).to eq('custom is not custom!')
|
719
|
+
|
720
|
+
get '/required_custom', custom: 'im custom'
|
721
|
+
expect(last_response.status).to eq(200)
|
722
|
+
expect(last_response.body).to eq('required with custom works!')
|
723
|
+
end
|
724
|
+
|
725
|
+
it 'validates when param is not present' do
|
726
|
+
get '/required_custom'
|
727
|
+
expect(last_response.status).to eq(400)
|
728
|
+
expect(last_response.body).to eq('custom is missing, custom is not custom!')
|
729
|
+
end
|
730
|
+
|
731
|
+
context 'nested namespaces' do
|
732
|
+
before do
|
733
|
+
subject.params do
|
734
|
+
requires :custom, customvalidator: true
|
735
|
+
end
|
736
|
+
subject.namespace 'nested' do
|
737
|
+
get 'one' do
|
738
|
+
'validation failed'
|
739
|
+
end
|
740
|
+
namespace 'nested' do
|
741
|
+
get 'two' do
|
742
|
+
'validation failed'
|
743
|
+
end
|
744
|
+
end
|
745
|
+
end
|
746
|
+
subject.namespace 'peer' do
|
747
|
+
get 'one' do
|
748
|
+
'no validation required'
|
749
|
+
end
|
750
|
+
namespace 'nested' do
|
751
|
+
get 'two' do
|
752
|
+
'no validation required'
|
753
|
+
end
|
754
|
+
end
|
755
|
+
end
|
756
|
+
|
757
|
+
subject.namespace 'unrelated' do
|
758
|
+
params do
|
759
|
+
requires :name
|
760
|
+
end
|
761
|
+
get 'one' do
|
762
|
+
'validation required'
|
763
|
+
end
|
764
|
+
|
765
|
+
namespace 'double' do
|
766
|
+
get 'two' do
|
767
|
+
'no validation required'
|
768
|
+
end
|
769
|
+
end
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
773
|
+
specify 'the parent namespace uses the validator' do
|
774
|
+
get '/nested/one', custom: 'im wrong, validate me'
|
775
|
+
expect(last_response.status).to eq(400)
|
776
|
+
expect(last_response.body).to eq('custom is not custom!')
|
777
|
+
end
|
778
|
+
|
779
|
+
specify 'the nested namesapce inherits the custom validator' do
|
780
|
+
get '/nested/nested/two', custom: 'im wrong, validate me'
|
781
|
+
expect(last_response.status).to eq(400)
|
782
|
+
expect(last_response.body).to eq('custom is not custom!')
|
783
|
+
end
|
784
|
+
|
785
|
+
specify 'peer namesapces does not have the validator' do
|
786
|
+
get '/peer/one', custom: 'im not validated'
|
787
|
+
expect(last_response.status).to eq(200)
|
788
|
+
expect(last_response.body).to eq('no validation required')
|
789
|
+
end
|
790
|
+
|
791
|
+
specify 'namespaces nested in peers should also not have the validator' do
|
792
|
+
get '/peer/nested/two', custom: 'im not validated'
|
793
|
+
expect(last_response.status).to eq(200)
|
794
|
+
expect(last_response.body).to eq('no validation required')
|
795
|
+
end
|
796
|
+
|
797
|
+
specify 'when nested, specifying a route should clear out the validations for deeper nested params' do
|
798
|
+
get '/unrelated/one'
|
799
|
+
expect(last_response.status).to eq(400)
|
800
|
+
get '/unrelated/double/two'
|
801
|
+
expect(last_response.status).to eq(200)
|
802
|
+
end
|
803
|
+
end
|
804
|
+
end
|
805
|
+
end # end custom validation
|
806
|
+
|
807
|
+
context 'named' do
|
808
|
+
context 'can be defined' do
|
809
|
+
it 'in helpers' do
|
810
|
+
subject.helpers do
|
811
|
+
params :pagination do
|
812
|
+
end
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
it 'in helper module which kind of Grape::API::Helpers' do
|
817
|
+
module SharedParams
|
818
|
+
extend Grape::API::Helpers
|
819
|
+
params :pagination do
|
820
|
+
end
|
821
|
+
end
|
822
|
+
subject.helpers SharedParams
|
823
|
+
end
|
824
|
+
end
|
825
|
+
|
826
|
+
context 'can be included in usual params' do
|
827
|
+
before do
|
828
|
+
module SharedParams
|
829
|
+
extend Grape::API::Helpers
|
830
|
+
params :period do
|
831
|
+
optional :start_date
|
832
|
+
optional :end_date
|
833
|
+
end
|
834
|
+
end
|
835
|
+
subject.helpers SharedParams
|
836
|
+
|
837
|
+
subject.helpers do
|
838
|
+
params :pagination do
|
839
|
+
optional :page, type: Integer
|
840
|
+
optional :per_page, type: Integer
|
841
|
+
end
|
842
|
+
end
|
843
|
+
end
|
844
|
+
|
845
|
+
it 'by #use' do
|
846
|
+
subject.params do
|
847
|
+
use :pagination
|
848
|
+
end
|
849
|
+
expect(subject.settings[:declared_params]).to eq [:page, :per_page]
|
850
|
+
end
|
851
|
+
|
852
|
+
it 'by #use with multiple params' do
|
853
|
+
subject.params do
|
854
|
+
use :pagination, :period
|
855
|
+
end
|
856
|
+
expect(subject.settings[:declared_params]).to eq [:page, :per_page, :start_date, :end_date]
|
857
|
+
end
|
858
|
+
|
859
|
+
end
|
860
|
+
|
861
|
+
context 'with block' do
|
862
|
+
before do
|
863
|
+
subject.helpers do
|
864
|
+
params :order do |options|
|
865
|
+
optional :order, type: Symbol, values: [:asc, :desc], default: options[:default_order]
|
866
|
+
optional :order_by, type: Symbol, values: options[:order_by], default: options[:default_order_by]
|
867
|
+
end
|
868
|
+
end
|
869
|
+
subject.format :json
|
870
|
+
subject.params do
|
871
|
+
use :order, default_order: :asc, order_by: [:name, :created_at], default_order_by: :created_at
|
872
|
+
end
|
873
|
+
subject.get '/order' do
|
874
|
+
{
|
875
|
+
order: params[:order],
|
876
|
+
order_by: params[:order_by]
|
877
|
+
}
|
878
|
+
end
|
879
|
+
end
|
880
|
+
it 'returns defaults' do
|
881
|
+
get '/order'
|
882
|
+
expect(last_response.status).to eq(200)
|
883
|
+
expect(last_response.body).to eq({ order: :asc, order_by: :created_at }.to_json)
|
884
|
+
end
|
885
|
+
it 'overrides default value for order' do
|
886
|
+
get '/order?order=desc'
|
887
|
+
expect(last_response.status).to eq(200)
|
888
|
+
expect(last_response.body).to eq({ order: :desc, order_by: :created_at }.to_json)
|
889
|
+
end
|
890
|
+
it 'overrides default value for order_by' do
|
891
|
+
get '/order?order_by=name'
|
892
|
+
expect(last_response.status).to eq(200)
|
893
|
+
expect(last_response.body).to eq({ order: :asc, order_by: :name }.to_json)
|
894
|
+
end
|
895
|
+
it 'fails with invalid value' do
|
896
|
+
get '/order?order=invalid'
|
897
|
+
expect(last_response.status).to eq(400)
|
898
|
+
expect(last_response.body).to eq('{"error":"order does not have a valid value"}')
|
899
|
+
end
|
900
|
+
end
|
901
|
+
end
|
902
|
+
|
903
|
+
context 'documentation' do
|
904
|
+
it 'can be included with a hash' do
|
905
|
+
documentation = { example: 'Joe' }
|
906
|
+
|
907
|
+
subject.params do
|
908
|
+
requires 'first_name', documentation: documentation
|
909
|
+
end
|
910
|
+
subject.get '/' do
|
911
|
+
end
|
912
|
+
|
913
|
+
expect(subject.routes.first.route_params['first_name'][:documentation]).to eq(documentation)
|
914
|
+
end
|
915
|
+
end
|
916
|
+
|
917
|
+
context 'mutually exclusive' do
|
918
|
+
context 'optional params' do
|
919
|
+
it 'errors when two or more are present' do
|
920
|
+
subject.params do
|
921
|
+
optional :beer
|
922
|
+
optional :wine
|
923
|
+
optional :juice
|
924
|
+
mutually_exclusive :beer, :wine, :juice
|
925
|
+
end
|
926
|
+
subject.get '/mutually_exclusive' do
|
927
|
+
'mutually_exclusive works!'
|
928
|
+
end
|
929
|
+
|
930
|
+
get '/mutually_exclusive', beer: 'string', wine: 'anotherstring'
|
931
|
+
expect(last_response.status).to eq(400)
|
932
|
+
expect(last_response.body).to eq("[:beer, :wine] are mutually exclusive")
|
933
|
+
end
|
934
|
+
end
|
935
|
+
|
936
|
+
context 'more than one set of mutually exclusive params' do
|
937
|
+
it 'errors for all sets' do
|
938
|
+
subject.params do
|
939
|
+
optional :beer
|
940
|
+
optional :wine
|
941
|
+
mutually_exclusive :beer, :wine
|
942
|
+
optional :scotch
|
943
|
+
optional :aquavit
|
944
|
+
mutually_exclusive :scotch, :aquavit
|
945
|
+
end
|
946
|
+
subject.get '/mutually_exclusive' do
|
947
|
+
'mutually_exclusive works!'
|
948
|
+
end
|
949
|
+
|
950
|
+
get '/mutually_exclusive', beer: 'true', wine: 'true', scotch: 'true', aquavit: 'true'
|
951
|
+
expect(last_response.status).to eq(400)
|
952
|
+
expect(last_response.body).to match(/\[:beer, :wine\] are mutually exclusive/)
|
953
|
+
expect(last_response.body).to match(/\[:scotch, :aquavit\] are mutually exclusive/)
|
954
|
+
end
|
955
|
+
end
|
956
|
+
end
|
957
|
+
|
958
|
+
context 'exactly one of' do
|
959
|
+
context 'params' do
|
960
|
+
it 'errors when two or more are present' do
|
961
|
+
subject.params do
|
962
|
+
optional :beer
|
963
|
+
optional :wine
|
964
|
+
optional :juice
|
965
|
+
exactly_one_of :beer, :wine, :juice
|
966
|
+
end
|
967
|
+
subject.get '/exactly_one_of' do
|
968
|
+
'exactly_one_of works!'
|
969
|
+
end
|
970
|
+
|
971
|
+
get '/exactly_one_of', beer: 'string', wine: 'anotherstring'
|
972
|
+
expect(last_response.status).to eq(400)
|
973
|
+
expect(last_response.body).to eq("[:beer, :wine] are mutually exclusive")
|
974
|
+
end
|
975
|
+
|
976
|
+
it 'errors when none is selected' do
|
977
|
+
subject.params do
|
978
|
+
optional :beer
|
979
|
+
optional :wine
|
980
|
+
optional :juice
|
981
|
+
exactly_one_of :beer, :wine, :juice
|
982
|
+
end
|
983
|
+
subject.get '/exactly_one_of' do
|
984
|
+
'exactly_one_of works!'
|
985
|
+
end
|
986
|
+
|
987
|
+
get '/exactly_one_of'
|
988
|
+
expect(last_response.status).to eq(400)
|
989
|
+
expect(last_response.body).to eq("[:beer, :wine, :juice] - exactly one parameter must be provided")
|
990
|
+
end
|
991
|
+
end
|
992
|
+
end
|
993
|
+
end
|
994
|
+
end
|