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,324 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grape_entity'
|
3
|
+
|
4
|
+
describe Grape::Entity do
|
5
|
+
subject { Class.new(Grape::API) }
|
6
|
+
|
7
|
+
def app
|
8
|
+
subject
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#present' do
|
12
|
+
it 'sets the object as the body if no options are provided' do
|
13
|
+
subject.get '/example' do
|
14
|
+
present(abc: 'def')
|
15
|
+
body.should == { abc: 'def' }
|
16
|
+
end
|
17
|
+
get '/example'
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'calls through to the provided entity class if one is given' do
|
21
|
+
subject.get '/example' do
|
22
|
+
entity_mock = Object.new
|
23
|
+
entity_mock.should_receive(:represent)
|
24
|
+
present Object.new, with: entity_mock
|
25
|
+
end
|
26
|
+
get '/example'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'pulls a representation from the class options if it exists' do
|
30
|
+
entity = Class.new(Grape::Entity)
|
31
|
+
allow(entity).to receive(:represent).and_return("Hiya")
|
32
|
+
|
33
|
+
subject.represent Object, with: entity
|
34
|
+
subject.get '/example' do
|
35
|
+
present Object.new
|
36
|
+
end
|
37
|
+
get '/example'
|
38
|
+
expect(last_response.body).to eq('Hiya')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'pulls a representation from the class options if the presented object is a collection of objects' do
|
42
|
+
entity = Class.new(Grape::Entity)
|
43
|
+
allow(entity).to receive(:represent).and_return("Hiya")
|
44
|
+
|
45
|
+
class TestObject
|
46
|
+
end
|
47
|
+
|
48
|
+
class FakeCollection
|
49
|
+
def first
|
50
|
+
TestObject.new
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
subject.represent TestObject, with: entity
|
55
|
+
subject.get '/example' do
|
56
|
+
present [TestObject.new]
|
57
|
+
end
|
58
|
+
|
59
|
+
subject.get '/example2' do
|
60
|
+
present FakeCollection.new
|
61
|
+
end
|
62
|
+
|
63
|
+
get '/example'
|
64
|
+
expect(last_response.body).to eq("Hiya")
|
65
|
+
|
66
|
+
get '/example2'
|
67
|
+
expect(last_response.body).to eq("Hiya")
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'pulls a representation from the class ancestor if it exists' do
|
71
|
+
entity = Class.new(Grape::Entity)
|
72
|
+
allow(entity).to receive(:represent).and_return("Hiya")
|
73
|
+
|
74
|
+
subclass = Class.new(Object)
|
75
|
+
|
76
|
+
subject.represent Object, with: entity
|
77
|
+
subject.get '/example' do
|
78
|
+
present subclass.new
|
79
|
+
end
|
80
|
+
get '/example'
|
81
|
+
expect(last_response.body).to eq('Hiya')
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'automatically uses Klass::Entity if that exists' do
|
85
|
+
some_model = Class.new
|
86
|
+
entity = Class.new(Grape::Entity)
|
87
|
+
allow(entity).to receive(:represent).and_return("Auto-detect!")
|
88
|
+
|
89
|
+
some_model.const_set :Entity, entity
|
90
|
+
|
91
|
+
subject.get '/example' do
|
92
|
+
present some_model.new
|
93
|
+
end
|
94
|
+
get '/example'
|
95
|
+
expect(last_response.body).to eq('Auto-detect!')
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'automatically uses Klass::Entity based on the first object in the collection being presented' do
|
99
|
+
some_model = Class.new
|
100
|
+
entity = Class.new(Grape::Entity)
|
101
|
+
allow(entity).to receive(:represent).and_return("Auto-detect!")
|
102
|
+
|
103
|
+
some_model.const_set :Entity, entity
|
104
|
+
|
105
|
+
subject.get '/example' do
|
106
|
+
present [some_model.new]
|
107
|
+
end
|
108
|
+
get '/example'
|
109
|
+
expect(last_response.body).to eq('Auto-detect!')
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'does not run autodetection for Entity when explicitely provided' do
|
113
|
+
entity = Class.new(Grape::Entity)
|
114
|
+
some_array = Array.new
|
115
|
+
|
116
|
+
subject.get '/example' do
|
117
|
+
present some_array, with: entity
|
118
|
+
end
|
119
|
+
|
120
|
+
expect(some_array).not_to receive(:first)
|
121
|
+
get '/example'
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'does not use #first method on ActiveRecord::Relation to prevent needless sql query' do
|
125
|
+
entity = Class.new(Grape::Entity)
|
126
|
+
some_relation = Class.new
|
127
|
+
some_model = Class.new
|
128
|
+
|
129
|
+
allow(entity).to receive(:represent).and_return("Auto-detect!")
|
130
|
+
allow(some_relation).to receive(:first)
|
131
|
+
allow(some_relation).to receive(:klass).and_return(some_model)
|
132
|
+
|
133
|
+
some_model.const_set :Entity, entity
|
134
|
+
|
135
|
+
subject.get '/example' do
|
136
|
+
present some_relation
|
137
|
+
end
|
138
|
+
|
139
|
+
expect(some_relation).not_to receive(:first)
|
140
|
+
get '/example'
|
141
|
+
expect(last_response.body).to eq('Auto-detect!')
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'autodetection does not use Entity if it is not a presenter' do
|
145
|
+
some_model = Class.new
|
146
|
+
entity = Class.new
|
147
|
+
|
148
|
+
some_model.class.const_set :Entity, entity
|
149
|
+
|
150
|
+
subject.get '/example' do
|
151
|
+
present some_model
|
152
|
+
end
|
153
|
+
get '/example'
|
154
|
+
expect(entity).not_to receive(:represent)
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'adds a root key to the output if one is given' do
|
158
|
+
subject.get '/example' do
|
159
|
+
present({ abc: 'def' }, root: :root)
|
160
|
+
body.should == { root: { abc: 'def' } }
|
161
|
+
end
|
162
|
+
get '/example'
|
163
|
+
end
|
164
|
+
|
165
|
+
[:json, :serializable_hash].each do |format|
|
166
|
+
|
167
|
+
it 'presents with #{format}' do
|
168
|
+
entity = Class.new(Grape::Entity)
|
169
|
+
entity.root "examples", "example"
|
170
|
+
entity.expose :id
|
171
|
+
|
172
|
+
subject.format format
|
173
|
+
subject.get '/example' do
|
174
|
+
c = Class.new do
|
175
|
+
attr_reader :id
|
176
|
+
def initialize(id)
|
177
|
+
@id = id
|
178
|
+
end
|
179
|
+
end
|
180
|
+
present c.new(1), with: entity
|
181
|
+
end
|
182
|
+
|
183
|
+
get '/example'
|
184
|
+
expect(last_response.status).to eq(200)
|
185
|
+
expect(last_response.body).to eq('{"example":{"id":1}}')
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'presents with #{format} collection' do
|
189
|
+
entity = Class.new(Grape::Entity)
|
190
|
+
entity.root "examples", "example"
|
191
|
+
entity.expose :id
|
192
|
+
|
193
|
+
subject.format format
|
194
|
+
subject.get '/examples' do
|
195
|
+
c = Class.new do
|
196
|
+
attr_reader :id
|
197
|
+
def initialize(id)
|
198
|
+
@id = id
|
199
|
+
end
|
200
|
+
end
|
201
|
+
examples = [c.new(1), c.new(2)]
|
202
|
+
present examples, with: entity
|
203
|
+
end
|
204
|
+
|
205
|
+
get '/examples'
|
206
|
+
expect(last_response.status).to eq(200)
|
207
|
+
expect(last_response.body).to eq('{"examples":[{"id":1},{"id":2}]}')
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'presents with xml' do
|
213
|
+
entity = Class.new(Grape::Entity)
|
214
|
+
entity.root "examples", "example"
|
215
|
+
entity.expose :name
|
216
|
+
|
217
|
+
subject.format :xml
|
218
|
+
|
219
|
+
subject.get '/example' do
|
220
|
+
c = Class.new do
|
221
|
+
attr_reader :name
|
222
|
+
def initialize(args)
|
223
|
+
@name = args[:name] || "no name set"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
present c.new(name: "johnnyiller"), with: entity
|
227
|
+
end
|
228
|
+
get '/example'
|
229
|
+
expect(last_response.status).to eq(200)
|
230
|
+
expect(last_response.headers['Content-type']).to eq("application/xml")
|
231
|
+
expect(last_response.body).to eq <<-XML
|
232
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
233
|
+
<hash>
|
234
|
+
<example>
|
235
|
+
<name>johnnyiller</name>
|
236
|
+
</example>
|
237
|
+
</hash>
|
238
|
+
XML
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'presents with json' do
|
242
|
+
entity = Class.new(Grape::Entity)
|
243
|
+
entity.root "examples", "example"
|
244
|
+
entity.expose :name
|
245
|
+
|
246
|
+
subject.format :json
|
247
|
+
|
248
|
+
subject.get '/example' do
|
249
|
+
c = Class.new do
|
250
|
+
attr_reader :name
|
251
|
+
def initialize(args)
|
252
|
+
@name = args[:name] || "no name set"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
present c.new(name: "johnnyiller"), with: entity
|
256
|
+
end
|
257
|
+
get '/example'
|
258
|
+
expect(last_response.status).to eq(200)
|
259
|
+
expect(last_response.headers['Content-type']).to eq("application/json")
|
260
|
+
expect(last_response.body).to eq('{"example":{"name":"johnnyiller"}}')
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'presents with jsonp utilising Rack::JSONP' do
|
264
|
+
require 'rack/contrib'
|
265
|
+
|
266
|
+
# Include JSONP middleware
|
267
|
+
subject.use Rack::JSONP
|
268
|
+
|
269
|
+
entity = Class.new(Grape::Entity)
|
270
|
+
entity.root "examples", "example"
|
271
|
+
entity.expose :name
|
272
|
+
|
273
|
+
# Rack::JSONP expects a standard JSON response
|
274
|
+
subject.format :json
|
275
|
+
|
276
|
+
subject.get '/example' do
|
277
|
+
c = Class.new do
|
278
|
+
attr_reader :name
|
279
|
+
def initialize(args)
|
280
|
+
@name = args[:name] || "no name set"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
present c.new(name: "johnnyiller"), with: entity
|
285
|
+
end
|
286
|
+
|
287
|
+
get '/example?callback=abcDef'
|
288
|
+
expect(last_response.status).to eq(200)
|
289
|
+
expect(last_response.headers['Content-type']).to eq("application/javascript")
|
290
|
+
expect(last_response.body).to eq('abcDef({"example":{"name":"johnnyiller"}})')
|
291
|
+
end
|
292
|
+
|
293
|
+
context "present with multiple entities" do
|
294
|
+
it "present with multiple entities using optional symbol" do
|
295
|
+
user = Class.new do
|
296
|
+
attr_reader :name
|
297
|
+
def initialize(args)
|
298
|
+
@name = args[:name] || "no name set"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
user1 = user.new(name: 'user1')
|
302
|
+
user2 = user.new(name: 'user2')
|
303
|
+
|
304
|
+
entity = Class.new(Grape::Entity)
|
305
|
+
entity.expose :name
|
306
|
+
|
307
|
+
subject.format :json
|
308
|
+
subject.get '/example' do
|
309
|
+
present :page, 1
|
310
|
+
present :user1, user1, with: entity
|
311
|
+
present :user2, user2, with: entity
|
312
|
+
end
|
313
|
+
get '/example'
|
314
|
+
expect_response_json = {
|
315
|
+
"page" => 1,
|
316
|
+
"user1" => { "name" => "user1" },
|
317
|
+
"user2" => { "name" => "user2" }
|
318
|
+
}
|
319
|
+
expect(JSON(last_response.body)).to eq(expect_response_json)
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Grape::Exceptions::InvalidFormatter do
|
5
|
+
describe "#message" do
|
6
|
+
let(:error) do
|
7
|
+
described_class.new(String, 'xml')
|
8
|
+
end
|
9
|
+
|
10
|
+
it "contains the problem in the message" do
|
11
|
+
expect(error.message).to include(
|
12
|
+
"cannot convert String to xml"
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Grape::Exceptions::InvalidVersionerOption do
|
5
|
+
describe "#message" do
|
6
|
+
let(:error) do
|
7
|
+
described_class.new("headers")
|
8
|
+
end
|
9
|
+
|
10
|
+
it "contains the problem in the message" do
|
11
|
+
expect(error.message).to include(
|
12
|
+
"Unknown :using for versioner: headers"
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Exceptions::MissingMimeType do
|
4
|
+
describe "#message" do
|
5
|
+
|
6
|
+
let(:error) do
|
7
|
+
described_class.new("new_json")
|
8
|
+
end
|
9
|
+
|
10
|
+
it "contains the problem in the message" do
|
11
|
+
expect(error.message).to include "missing mime type for new_json"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "contains the resolution in the message" do
|
15
|
+
expect(error.message).to include "or add your own with content_type :new_json, 'application/new_json' "
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Grape::Exceptions::MissingOption do
|
5
|
+
describe "#message" do
|
6
|
+
let(:error) do
|
7
|
+
described_class.new(:path)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "contains the problem in the message" do
|
11
|
+
expect(error.message).to include(
|
12
|
+
"You must specify :path options."
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Grape::Exceptions::UnknownOptions do
|
5
|
+
describe "#message" do
|
6
|
+
let(:error) do
|
7
|
+
described_class.new([:a, :b])
|
8
|
+
end
|
9
|
+
|
10
|
+
it "contains the problem in the message" do
|
11
|
+
expect(error.message).to include(
|
12
|
+
"unknown options: "
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Grape::Exceptions::UnknownValidator do
|
5
|
+
describe "#message" do
|
6
|
+
let(:error) do
|
7
|
+
described_class.new('gt_10')
|
8
|
+
end
|
9
|
+
|
10
|
+
it "contains the problem in the message" do
|
11
|
+
expect(error.message).to include(
|
12
|
+
"unknown validator: gt_10"
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
describe Grape::Exceptions::ValidationErrors do
|
5
|
+
let(:validation_message) { "FooBar is invalid" }
|
6
|
+
let(:validation_error) { OpenStruct.new(param: validation_message) }
|
7
|
+
|
8
|
+
context "message" do
|
9
|
+
context "is not repeated" do
|
10
|
+
let(:error) do
|
11
|
+
described_class.new(errors: [validation_error, validation_error])
|
12
|
+
end
|
13
|
+
subject(:message) { error.message.split(',').map(&:strip) }
|
14
|
+
|
15
|
+
it { expect(message).to include validation_message }
|
16
|
+
it { expect(message.size).to eq 1 }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|