grape-security 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +45 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +70 -0
  5. data/.travis.yml +18 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG.md +314 -0
  8. data/CONTRIBUTING.md +118 -0
  9. data/Gemfile +21 -0
  10. data/Guardfile +14 -0
  11. data/LICENSE +20 -0
  12. data/README.md +1777 -0
  13. data/RELEASING.md +105 -0
  14. data/Rakefile +69 -0
  15. data/UPGRADING.md +124 -0
  16. data/grape-security.gemspec +39 -0
  17. data/grape.png +0 -0
  18. data/lib/grape.rb +99 -0
  19. data/lib/grape/api.rb +646 -0
  20. data/lib/grape/cookies.rb +39 -0
  21. data/lib/grape/endpoint.rb +533 -0
  22. data/lib/grape/error_formatter/base.rb +31 -0
  23. data/lib/grape/error_formatter/json.rb +15 -0
  24. data/lib/grape/error_formatter/txt.rb +16 -0
  25. data/lib/grape/error_formatter/xml.rb +15 -0
  26. data/lib/grape/exceptions/base.rb +66 -0
  27. data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
  28. data/lib/grape/exceptions/invalid_formatter.rb +10 -0
  29. data/lib/grape/exceptions/invalid_versioner_option.rb +10 -0
  30. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +10 -0
  31. data/lib/grape/exceptions/missing_mime_type.rb +10 -0
  32. data/lib/grape/exceptions/missing_option.rb +10 -0
  33. data/lib/grape/exceptions/missing_vendor_option.rb +10 -0
  34. data/lib/grape/exceptions/unknown_options.rb +10 -0
  35. data/lib/grape/exceptions/unknown_validator.rb +10 -0
  36. data/lib/grape/exceptions/validation.rb +26 -0
  37. data/lib/grape/exceptions/validation_errors.rb +43 -0
  38. data/lib/grape/formatter/base.rb +31 -0
  39. data/lib/grape/formatter/json.rb +12 -0
  40. data/lib/grape/formatter/serializable_hash.rb +35 -0
  41. data/lib/grape/formatter/txt.rb +11 -0
  42. data/lib/grape/formatter/xml.rb +12 -0
  43. data/lib/grape/http/request.rb +26 -0
  44. data/lib/grape/locale/en.yml +32 -0
  45. data/lib/grape/middleware/auth/base.rb +30 -0
  46. data/lib/grape/middleware/auth/basic.rb +13 -0
  47. data/lib/grape/middleware/auth/digest.rb +13 -0
  48. data/lib/grape/middleware/auth/oauth2.rb +83 -0
  49. data/lib/grape/middleware/base.rb +62 -0
  50. data/lib/grape/middleware/error.rb +89 -0
  51. data/lib/grape/middleware/filter.rb +17 -0
  52. data/lib/grape/middleware/formatter.rb +150 -0
  53. data/lib/grape/middleware/globals.rb +13 -0
  54. data/lib/grape/middleware/versioner.rb +32 -0
  55. data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
  56. data/lib/grape/middleware/versioner/header.rb +132 -0
  57. data/lib/grape/middleware/versioner/param.rb +42 -0
  58. data/lib/grape/middleware/versioner/path.rb +52 -0
  59. data/lib/grape/namespace.rb +23 -0
  60. data/lib/grape/parser/base.rb +29 -0
  61. data/lib/grape/parser/json.rb +11 -0
  62. data/lib/grape/parser/xml.rb +11 -0
  63. data/lib/grape/path.rb +70 -0
  64. data/lib/grape/route.rb +27 -0
  65. data/lib/grape/util/content_types.rb +18 -0
  66. data/lib/grape/util/deep_merge.rb +23 -0
  67. data/lib/grape/util/hash_stack.rb +120 -0
  68. data/lib/grape/validations.rb +322 -0
  69. data/lib/grape/validations/coerce.rb +63 -0
  70. data/lib/grape/validations/default.rb +25 -0
  71. data/lib/grape/validations/exactly_one_of.rb +26 -0
  72. data/lib/grape/validations/mutual_exclusion.rb +25 -0
  73. data/lib/grape/validations/presence.rb +16 -0
  74. data/lib/grape/validations/regexp.rb +12 -0
  75. data/lib/grape/validations/values.rb +23 -0
  76. data/lib/grape/version.rb +3 -0
  77. data/spec/grape/api_spec.rb +2571 -0
  78. data/spec/grape/endpoint_spec.rb +784 -0
  79. data/spec/grape/entity_spec.rb +324 -0
  80. data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
  81. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
  82. data/spec/grape/exceptions/missing_mime_type_spec.rb +18 -0
  83. data/spec/grape/exceptions/missing_option_spec.rb +18 -0
  84. data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
  85. data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
  86. data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
  87. data/spec/grape/middleware/auth/basic_spec.rb +31 -0
  88. data/spec/grape/middleware/auth/digest_spec.rb +47 -0
  89. data/spec/grape/middleware/auth/oauth2_spec.rb +135 -0
  90. data/spec/grape/middleware/base_spec.rb +58 -0
  91. data/spec/grape/middleware/error_spec.rb +45 -0
  92. data/spec/grape/middleware/exception_spec.rb +184 -0
  93. data/spec/grape/middleware/formatter_spec.rb +258 -0
  94. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
  95. data/spec/grape/middleware/versioner/header_spec.rb +302 -0
  96. data/spec/grape/middleware/versioner/param_spec.rb +58 -0
  97. data/spec/grape/middleware/versioner/path_spec.rb +44 -0
  98. data/spec/grape/middleware/versioner_spec.rb +22 -0
  99. data/spec/grape/path_spec.rb +229 -0
  100. data/spec/grape/util/hash_stack_spec.rb +132 -0
  101. data/spec/grape/validations/coerce_spec.rb +208 -0
  102. data/spec/grape/validations/default_spec.rb +123 -0
  103. data/spec/grape/validations/exactly_one_of_spec.rb +71 -0
  104. data/spec/grape/validations/mutual_exclusion_spec.rb +61 -0
  105. data/spec/grape/validations/presence_spec.rb +142 -0
  106. data/spec/grape/validations/regexp_spec.rb +40 -0
  107. data/spec/grape/validations/values_spec.rb +152 -0
  108. data/spec/grape/validations/zh-CN.yml +10 -0
  109. data/spec/grape/validations_spec.rb +994 -0
  110. data/spec/shared/versioning_examples.rb +121 -0
  111. data/spec/spec_helper.rb +26 -0
  112. data/spec/support/basic_auth_encode_helpers.rb +3 -0
  113. data/spec/support/content_type_helpers.rb +11 -0
  114. data/spec/support/versioned_helpers.rb +50 -0
  115. 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,10 @@
1
+ zh-CN:
2
+ grape:
3
+ errors:
4
+ format: ! '%{attribute}%{message}'
5
+ attributes:
6
+ age: 年龄
7
+ messages:
8
+ coerce: '格式不正确'
9
+ presence: '请填写'
10
+ regexp: '格式不正确'
@@ -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