strelka 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/ChangeLog +3293 -3058
  5. data/History.rdoc +17 -0
  6. data/Manifest.txt +3 -0
  7. data/Rakefile +2 -2
  8. data/contrib/hoetemplate/lib/file_name.rb.erb +3 -2
  9. data/contrib/hoetemplate/spec/file_name_spec.rb.erb +1 -1
  10. data/examples/apps/auth-demo +1 -2
  11. data/examples/apps/auth-demo2 +1 -2
  12. data/examples/apps/sessions-demo +1 -2
  13. data/examples/gen-config.rb +1 -2
  14. data/lib/strelka.rb +92 -17
  15. data/lib/strelka/app.rb +7 -6
  16. data/lib/strelka/app/auth.rb +5 -5
  17. data/lib/strelka/app/errors.rb +1 -1
  18. data/lib/strelka/app/filters.rb +1 -1
  19. data/lib/strelka/app/negotiation.rb +1 -1
  20. data/lib/strelka/app/parameters.rb +1 -1
  21. data/lib/strelka/app/restresources.rb +14 -21
  22. data/lib/strelka/app/routing.rb +5 -6
  23. data/lib/strelka/app/sessions.rb +3 -1
  24. data/lib/strelka/app/templating.rb +1 -1
  25. data/lib/strelka/authprovider.rb +1 -1
  26. data/lib/strelka/authprovider/basic.rb +1 -0
  27. data/lib/strelka/authprovider/hostaccess.rb +1 -0
  28. data/lib/strelka/behavior/plugin.rb +2 -2
  29. data/lib/strelka/cli.rb +2 -1
  30. data/lib/strelka/command/config.rb +2 -1
  31. data/lib/strelka/command/discover.rb +2 -1
  32. data/lib/strelka/command/start.rb +2 -1
  33. data/lib/strelka/constants.rb +1 -1
  34. data/lib/strelka/cookie.rb +1 -1
  35. data/lib/strelka/cookieset.rb +1 -1
  36. data/lib/strelka/discovery.rb +1 -1
  37. data/lib/strelka/httprequest.rb +4 -4
  38. data/lib/strelka/httprequest/acceptparams.rb +1 -1
  39. data/lib/strelka/httprequest/auth.rb +3 -1
  40. data/lib/strelka/httprequest/negotiation.rb +1 -1
  41. data/lib/strelka/httprequest/session.rb +3 -1
  42. data/lib/strelka/httpresponse.rb +2 -3
  43. data/lib/strelka/httpresponse/negotiation.rb +1 -1
  44. data/lib/strelka/httpresponse/session.rb +1 -1
  45. data/lib/strelka/mixins.rb +26 -5
  46. data/lib/strelka/multipartparser.rb +3 -3
  47. data/lib/strelka/paramvalidator.rb +4 -4
  48. data/lib/strelka/plugins.rb +14 -5
  49. data/lib/strelka/router.rb +1 -1
  50. data/lib/strelka/router/default.rb +1 -1
  51. data/lib/strelka/router/exclusive.rb +1 -1
  52. data/lib/strelka/session.rb +1 -0
  53. data/lib/strelka/session/db.rb +1 -0
  54. data/lib/strelka/session/default.rb +1 -0
  55. data/lib/strelka/testing.rb +454 -14
  56. data/lib/strelka/websocketserver.rb +150 -36
  57. data/lib/strelka/websocketserver/heartbeat.rb +163 -0
  58. data/lib/strelka/websocketserver/routing.rb +46 -19
  59. data/spec/constants.rb +1 -1
  60. data/spec/helpers.rb +15 -6
  61. data/spec/strelka/app/auth_spec.rb +5 -3
  62. data/spec/strelka/app/errors_spec.rb +2 -2
  63. data/spec/strelka/app/filters_spec.rb +2 -2
  64. data/spec/strelka/app/negotiation_spec.rb +2 -2
  65. data/spec/strelka/app/parameters_spec.rb +5 -5
  66. data/spec/strelka/app/restresources_spec.rb +8 -6
  67. data/spec/strelka/app/routing_spec.rb +3 -3
  68. data/spec/strelka/app/sessions_spec.rb +4 -2
  69. data/spec/strelka/app/templating_spec.rb +2 -2
  70. data/spec/strelka/app_spec.rb +5 -24
  71. data/spec/strelka/authprovider/basic_spec.rb +3 -2
  72. data/spec/strelka/authprovider/hostaccess_spec.rb +3 -2
  73. data/spec/strelka/authprovider_spec.rb +3 -2
  74. data/spec/strelka/cli_spec.rb +7 -4
  75. data/spec/strelka/cookie_spec.rb +2 -2
  76. data/spec/strelka/cookieset_spec.rb +2 -2
  77. data/spec/strelka/discovery_spec.rb +2 -2
  78. data/spec/strelka/exceptions_spec.rb +2 -2
  79. data/spec/strelka/httprequest/acceptparams_spec.rb +2 -2
  80. data/spec/strelka/httprequest/auth_spec.rb +3 -2
  81. data/spec/strelka/httprequest/negotiation_spec.rb +2 -2
  82. data/spec/strelka/httprequest/session_spec.rb +3 -2
  83. data/spec/strelka/httprequest_spec.rb +7 -2
  84. data/spec/strelka/httpresponse/negotiation_spec.rb +6 -5
  85. data/spec/strelka/httpresponse/session_spec.rb +3 -2
  86. data/spec/strelka/httpresponse_spec.rb +4 -3
  87. data/spec/strelka/mixins_spec.rb +85 -2
  88. data/spec/strelka/multipartparser_spec.rb +5 -4
  89. data/spec/strelka/paramvalidator_spec.rb +15 -10
  90. data/spec/strelka/plugins_spec.rb +24 -2
  91. data/spec/strelka/router/default_spec.rb +2 -2
  92. data/spec/strelka/router/exclusive_spec.rb +2 -2
  93. data/spec/strelka/router_spec.rb +2 -2
  94. data/spec/strelka/session/db_spec.rb +3 -2
  95. data/spec/strelka/session/default_spec.rb +3 -2
  96. data/spec/strelka/session_spec.rb +3 -2
  97. data/spec/strelka/testing_spec.rb +772 -0
  98. data/spec/strelka/websocketserver/heartbeat_spec.rb +19 -0
  99. data/spec/strelka/websocketserver/routing_spec.rb +31 -29
  100. data/spec/strelka/websocketserver_spec.rb +210 -75
  101. data/spec/strelka_spec.rb +172 -2
  102. metadata +43 -36
  103. metadata.gz.sig +0 -0
@@ -1,6 +1,6 @@
1
1
  # -*- ruby -*-
2
2
  # vim: set nosta noet ts=4 sw=4:
3
- # encoding: utf-8
3
+ # frozen-string-literal: true
4
4
 
5
5
  require_relative '../../helpers'
6
6
 
@@ -14,7 +14,7 @@ require 'strelka/router/exclusive'
14
14
  ### C O N T E X T S
15
15
  #####################################################################
16
16
 
17
- describe Strelka::Router::Exclusive do
17
+ RSpec.describe Strelka::Router::Exclusive do
18
18
 
19
19
  before( :all ) do
20
20
  @request_factory = Mongrel2::RequestFactory.new( route: '/user' )
@@ -1,6 +1,6 @@
1
1
  # -*- ruby -*-
2
2
  # vim: set nosta noet ts=4 sw=4:
3
- # encoding: utf-8
3
+ # frozen-string-literal: true
4
4
 
5
5
  require_relative '../helpers'
6
6
 
@@ -14,7 +14,7 @@ require 'strelka/router'
14
14
  ### C O N T E X T S
15
15
  #####################################################################
16
16
 
17
- describe Strelka::Router do
17
+ RSpec.describe Strelka::Router do
18
18
 
19
19
  before( :all ) do
20
20
  @request_factory = Mongrel2::RequestFactory.new( route: '/user' )
@@ -1,5 +1,6 @@
1
- # -*- rspec -*-
1
+ # -*- ruby -*-
2
2
  # vim: set nosta noet ts=4 sw=4:
3
+ # frozen-string-literal: true
3
4
 
4
5
  require_relative '../../helpers'
5
6
 
@@ -13,7 +14,7 @@ require 'strelka/session/db'
13
14
  ### C O N T E X T S
14
15
  #####################################################################
15
16
 
16
- describe Strelka::Session::Db do
17
+ RSpec.describe Strelka::Session::Db do
17
18
 
18
19
  before( :all ) do
19
20
  @request_factory = Mongrel2::RequestFactory.new( route: '/frothy' )
@@ -1,5 +1,6 @@
1
- # -*- rspec -*-
1
+ # -*- ruby -*-
2
2
  # vim: set nosta noet ts=4 sw=4:
3
+ # frozen-string-literal: true
3
4
 
4
5
  require_relative '../../helpers'
5
6
 
@@ -13,7 +14,7 @@ require 'strelka/session/default'
13
14
  ### C O N T E X T S
14
15
  #####################################################################
15
16
 
16
- describe Strelka::Session::Default do
17
+ RSpec.describe Strelka::Session::Default do
17
18
 
18
19
  before( :all ) do
19
20
  @request_factory = Mongrel2::RequestFactory.new( route: '/hungry' )
@@ -1,5 +1,6 @@
1
- # -*- rspec -*-
1
+ # -*- ruby -*-
2
2
  # vim: set nosta noet ts=4 sw=4:
3
+ # frozen-string-literal: true
3
4
 
4
5
  require_relative '../helpers'
5
6
 
@@ -13,7 +14,7 @@ require 'strelka/session'
13
14
  ### C O N T E X T S
14
15
  #####################################################################
15
16
 
16
- describe Strelka::Session do
17
+ RSpec.describe Strelka::Session do
17
18
 
18
19
  before( :all ) do
19
20
  @request_factory = Mongrel2::RequestFactory.new( route: '/user' )
@@ -0,0 +1,772 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../helpers'
5
+
6
+ require 'ostruct'
7
+ require 'mongrel2/testing'
8
+ require 'strelka/testing'
9
+
10
+
11
+ RSpec.describe( Strelka::Testing ) do
12
+
13
+ #
14
+ # Expectation-failure Matchers (stolen from rspec-expectations)
15
+ # See the README for licensing information.
16
+ #
17
+
18
+ def fail
19
+ raise_error( RSpec::Expectations::ExpectationNotMetError )
20
+ end
21
+
22
+
23
+ def fail_with( message )
24
+ raise_error( RSpec::Expectations::ExpectationNotMetError, message )
25
+ end
26
+
27
+
28
+ def fail_matching( message )
29
+ if String === message
30
+ regexp = /#{Regexp.escape( message )}/
31
+ else
32
+ regexp = message
33
+ end
34
+ raise_error( RSpec::Expectations::ExpectationNotMetError, regexp )
35
+ end
36
+
37
+
38
+ def fail_including( *messages )
39
+ raise_error do |err|
40
+ expect( err ).to be_a( RSpec::Expectations::ExpectationNotMetError )
41
+ expect( err.message ).to include( *messages )
42
+ end
43
+ end
44
+
45
+
46
+ before( :all ) do
47
+ @request_factory = Mongrel2::RequestFactory.new( route: '/v1/api' )
48
+ end
49
+
50
+
51
+ describe "finish_with matcher" do
52
+
53
+ before( :each ) do
54
+ @app = Class.new( Strelka::App ) do
55
+ def initialize( &block )
56
+ @block = block
57
+ super( TEST_APPID, TEST_SEND_SPEC, TEST_RECV_SPEC )
58
+ end
59
+
60
+ def handle_request( req )
61
+ self.instance_exec( req, &@block )
62
+ end
63
+ end
64
+ end
65
+
66
+ let( :request ) { @request_factory.get('/v1/api/users') }
67
+
68
+
69
+ it "passes if the app finishes with the expected criteria" do
70
+ expect {
71
+ expect {
72
+ @app.new { finish_with(404, "Not found.") }.handle_request( request )
73
+ }.to finish_with( 404, /not found/i )
74
+ }.not_to raise_error
75
+ end
76
+
77
+
78
+ it "passes if the app finishes with the expected criteria and headers" do
79
+ expect {
80
+ expect {
81
+ @app.new {
82
+ finish_with( 303, "See other resource.", location: 'http://ac.me/resource' )
83
+ }.handle_request( request )
84
+ }.to finish_with( 303, /see other/i, 'Location' => 'http://ac.me/resource' )
85
+ }.not_to raise_error
86
+ end
87
+
88
+
89
+ it "fails if the app doesn't call finish_with" do
90
+ expect {
91
+ expect {
92
+ @app.new {|req| req.response }.handle_request( request )
93
+ }.to finish_with( 404, /not found/i )
94
+ }.to fail_matching( /expected response to finish_with/i )
95
+ end
96
+
97
+
98
+ it "fails if the app finishes with a different status" do
99
+ expect {
100
+ expect {
101
+ @app.new {
102
+ finish_with( 415, "Unsupported media type." )
103
+ }.handle_request( request )
104
+ }.to finish_with( 404, /not found/i )
105
+ }.to fail_matching( /with a 404 status, but got 415/i )
106
+ end
107
+
108
+
109
+ it "fails if the app finishes with the correct status, but the wrong message" do
110
+ expect {
111
+ expect {
112
+ @app.new {
113
+ finish_with( 404, "No such user." )
114
+ }.handle_request( request )
115
+ }.to finish_with( 404, /not found/i )
116
+ }.to fail_matching( /with a message matching/i )
117
+ end
118
+
119
+
120
+ it "fails if the app finishes with the correct status and message, but missing a header" do
121
+ expect {
122
+ expect {
123
+ @app.new {
124
+ finish_with( 301, "Moved permanently" )
125
+ }.handle_request( request )
126
+ }.to finish_with( 301, /moved/i, location: 'http://ac.me/this' )
127
+ }.to fail_matching( /with a location header/i )
128
+ end
129
+
130
+
131
+ it "fails if the app finishes with the specified header set to an empty string" do
132
+ expect {
133
+ expect {
134
+ @app.new {
135
+ finish_with( 301, "Moved permanently", location: '' )
136
+ }.handle_request( request )
137
+ }.to finish_with( 301, /moved/i, location: 'http://ac.me/this' )
138
+ }.to fail_matching( /with a location header.*blank/i )
139
+ end
140
+
141
+
142
+ it "fails if the app finishes with the specified header set to the wrong value" do
143
+ expect {
144
+ expect {
145
+ @app.new {
146
+ finish_with( 301, "Moved permanently", location: 'http://localhost' )
147
+ }.handle_request( request )
148
+ }.to finish_with( 301, /moved/i, location: 'http://ac.me/this' )
149
+ }.to fail_matching( /with a location header.*ac\.me.*localhost/i )
150
+ end
151
+
152
+ end
153
+
154
+
155
+ describe "have_json_body matcher" do
156
+
157
+ let( :request ) { @request_factory.get('/v1/api') }
158
+
159
+
160
+ it "fails if the response doesn't have a content type" do
161
+ response = request.response
162
+
163
+ expect {
164
+ expect( response ).to have_json_body
165
+ }.to fail_matching( /doesn't have a content-type/i )
166
+ end
167
+
168
+
169
+ it "fails if the response doesn't have an 'application/json' content type" do
170
+ response = request.response
171
+ response.content_type = 'text/plain'
172
+ response.puts "Stuff."
173
+
174
+ expect {
175
+ expect( response ).to have_json_body
176
+ }.to fail_matching( /content-type is/i )
177
+ end
178
+
179
+
180
+ it "fails if the response body doesn't contain valid JSON" do
181
+ response = request.response
182
+ response.content_type = 'application/json'
183
+ response.body = '<'
184
+
185
+ expect {
186
+ expect( response ).to have_json_body
187
+ }.to fail_matching( /invalid JSON/i )
188
+ end
189
+
190
+
191
+ context "with no additional criteria" do
192
+
193
+ it "passes for a valid JSON response" do
194
+ response = request.response
195
+ response.content_type = 'application/json'
196
+ response.body = '{}'
197
+
198
+ expect {
199
+ expect( response ).to have_json_body
200
+ }.to_not raise_error
201
+ end
202
+
203
+
204
+ it "passes for a valid JSON-subtype response" do
205
+ response = request.response
206
+ response.content_type = 'application/hal+json'
207
+ response.body = '{}'
208
+
209
+ expect {
210
+ expect( response ).to have_json_body
211
+ }.to_not raise_error
212
+ end
213
+
214
+
215
+ it "passes for a valid JSON vendor-subtype response" do
216
+ response = request.response
217
+ response.content_type = 'application/vnd.api+json'
218
+ response.body = '{}'
219
+
220
+ expect {
221
+ expect( response ).to have_json_body
222
+ }.to_not raise_error
223
+ end
224
+
225
+
226
+ it "passes for a valid JSON response that includes a charset" do
227
+ response = request.response
228
+ response.content_type = 'application/json; charset=utf-8'
229
+ response.body = '{}'
230
+
231
+ expect {
232
+ expect( response ).to have_json_body
233
+ }.to_not raise_error
234
+ end
235
+
236
+ end
237
+
238
+
239
+ context "with a type specification" do
240
+
241
+ let( :response ) do
242
+ res = request.response
243
+ res.content_type = 'application/json'
244
+ res.body = '{}'
245
+ res
246
+ end
247
+
248
+
249
+ it "passes for a valid JSON response of the specified type" do
250
+ expect {
251
+ expect( response ).to have_json_body( Object )
252
+ }.to_not raise_error
253
+ end
254
+
255
+
256
+ it "fails for a valid JSON response of a different type" do
257
+ expect {
258
+ expect( response ).to have_json_body( Array )
259
+ }.to fail_matching( /response body isn't a JSON Array/i )
260
+ end
261
+
262
+ end
263
+
264
+
265
+ context "with a member specification" do
266
+
267
+ let( :object_response ) do
268
+ res = request.response
269
+ res.content_type = 'application/json'
270
+ res.body = '{"message":"the message"}'
271
+ res
272
+ end
273
+ let( :array_response ) do
274
+ res = request.response
275
+ res.content_type = 'application/json'
276
+ res.body = '["message"]'
277
+ res
278
+ end
279
+
280
+
281
+ it "passes for a valid JSON Object response that includes the specified members" do
282
+ expect {
283
+ expect( object_response ).to have_json_body.that_includes( :message )
284
+ }.to_not raise_error
285
+ end
286
+
287
+
288
+ it "passes for a valid JSON Array response that includes the specified members" do
289
+ expect {
290
+ expect( array_response ).to have_json_body.that_includes( 'message' )
291
+ }.to_not raise_error
292
+ end
293
+
294
+
295
+ it "fails for a valid JSON response that doesn't include the specified member" do
296
+ expect {
297
+ expect( object_response ).to have_json_body.that_includes( :code )
298
+ }.to fail_matching( /to include :code/i )
299
+ end
300
+
301
+
302
+ it "passes for a valid JSON Object response that excludes the specified members" do
303
+ expect {
304
+ expect( object_response ).to have_json_body.that_excludes( :other )
305
+ }.to_not raise_error
306
+ end
307
+
308
+
309
+ it "passes for a valid JSON Array response that excludes the specified members" do
310
+ expect {
311
+ expect( array_response ).to have_json_body.that_excludes( 'other' )
312
+ }.to_not raise_error
313
+ end
314
+
315
+
316
+ it "fails for a valid JSON response that doesn't exclude the specified member" do
317
+ expect {
318
+ expect( object_response ).to have_json_body.that_excludes( :message )
319
+ }.to fail_matching( /not to include :message/i )
320
+ end
321
+
322
+ end
323
+
324
+
325
+ context "with a length specification" do
326
+
327
+ let( :object_response ) do
328
+ res = request.response
329
+ res.content_type = 'application/json'
330
+ res.body = '{"ebb":"nitzer", "chant":"join in the"}'
331
+ res
332
+ end
333
+ let( :array_response ) do
334
+ res = request.response
335
+ res.content_type = 'application/json'
336
+ res.body = '["lies","gold","guns","fire","gold","judge","guns","fire"]'
337
+ res
338
+ end
339
+
340
+
341
+ it "passes for a valid JSON Object response that has the specified number of members" do
342
+ expect {
343
+ expect( object_response ).to have_json_body.of_length( 2 )
344
+ }.to_not raise_error
345
+ end
346
+
347
+
348
+ it "passes for a valid JSON Array response that includes the specified members" do
349
+ expect {
350
+ expect( array_response ).to have_json_body.of_length( 8 )
351
+ }.to_not raise_error
352
+ end
353
+
354
+
355
+ it "fails for a valid JSON response that doesn't include the specified member" do
356
+ expect {
357
+ expect( array_response ).to have_json_body.of_length( 2 )
358
+ }.to fail_matching( /:length => 2/i )
359
+ end
360
+
361
+ end
362
+
363
+
364
+ context "with a type and a member specification" do
365
+
366
+ let( :object_response ) do
367
+ res = request.response
368
+ res.content_type = 'application/json'
369
+ res.body = '{"message":"the message"}'
370
+ res
371
+ end
372
+ let( :array_response ) do
373
+ res = request.response
374
+ res.content_type = 'application/json'
375
+ res.body = '["message"]'
376
+ res
377
+ end
378
+
379
+
380
+ it "passes for a valid JSON Object response that is of the correct type and includes " +
381
+ "the specified members" do
382
+ expect {
383
+ expect( object_response ).to have_json_body( Object ).that_includes( :message )
384
+ }.to_not raise_error
385
+ end
386
+
387
+
388
+ it "fails for a valid JSON response that includes the specified members " +
389
+ "but is of a different type" do
390
+ expect {
391
+ expect( array_response ).to have_json_body( Object ).that_includes( 'message' )
392
+ }.to fail_matching( /isn't a JSON Object/i )
393
+ end
394
+
395
+
396
+ it "fails for a valid JSON response that is of the correct type " +
397
+ "but doesn't include the specified members " do
398
+ expect {
399
+ expect( object_response ).to have_json_body( Object ).that_includes( :code )
400
+ }.to fail_matching( /to include :code/i )
401
+ end
402
+
403
+ end
404
+
405
+
406
+ context "with a type and a length specification" do
407
+
408
+ let( :object_response ) do
409
+ res = request.response
410
+ res.content_type = 'application/json'
411
+ res.body = '{"message":"the message","type":"the type"}'
412
+ res
413
+ end
414
+ let( :array_response ) do
415
+ res = request.response
416
+ res.content_type = 'application/json'
417
+ res.body = '["message","type","brand"]'
418
+ res
419
+ end
420
+
421
+
422
+ it "passes for a valid JSON Object response that is of the correct type and length" do
423
+ expect {
424
+ expect( object_response ).to have_json_body( Object ).of_length( 2 )
425
+ }.to_not raise_error
426
+ end
427
+
428
+
429
+ it "fails for a valid JSON response that includes the specified length " +
430
+ "but is of a different type" do
431
+ expect {
432
+ expect( array_response ).to have_json_body( Object ).of_length( 2 )
433
+ }.to fail_matching( /isn't a JSON Object/i )
434
+ end
435
+
436
+
437
+ it "fails for a valid JSON response that is of the correct type but a different length" do
438
+ expect {
439
+ expect( array_response ).to have_json_body( Array ).of_length( 2 )
440
+ }.to fail_matching( /length: 2/i )
441
+ end
442
+
443
+ end
444
+
445
+
446
+ context "with additional expectations" do
447
+
448
+ let( :object_response ) do
449
+ res = request.response
450
+ res.content_type = 'application/json'
451
+ res.body = '{"message":"the message", "massage":"Shiatsu", "messiah":"complex"}'
452
+ res
453
+ end
454
+ let( :array_response ) do
455
+ res = request.response
456
+ res.content_type = 'application/json'
457
+ res.body = '["message", "note", "postage", "demiurge"]'
458
+ res
459
+ end
460
+
461
+
462
+ it "passes for a valid JSON Object that matches all of them" do
463
+ expect {
464
+ expect( object_response ).to have_json_body( Object ).
465
+ and( all( satisfy {|key,val| key.length > 4} ) )
466
+ }.to_not raise_error
467
+ end
468
+
469
+
470
+ it "passes for a valid JSON Array that matches all of them" do
471
+ expect {
472
+ expect( array_response ).to have_json_body( Array ).
473
+ and( all( be_a( String ) ) ).
474
+ and( all( end_with( 'e' ) ) )
475
+ }.to_not raise_error
476
+ end
477
+
478
+
479
+ it "fails for a valid JSON Object that doesn't match all of them" do
480
+ expect {
481
+ expect( object_response ).to have_json_body( Object ).
482
+ and( all( satisfy {|key,_| key.length > 4} ) ).
483
+ and( all( satisfy {|key,_| key.to_s.end_with?( 'e' )} ) )
484
+ }.to fail_matching( /to all satisfy expression/ )
485
+ end
486
+
487
+
488
+ it "fails for a valid JSON Array that doesn't match all of them" do
489
+ expect {
490
+ expect( array_response ).to have_json_body( Array ).
491
+ and( all( be_a( String ) ) ).
492
+ and( all( start_with( 'm' ) ) )
493
+ }.to fail_matching( /to all start with "m"/ )
494
+ end
495
+
496
+ end
497
+
498
+ end
499
+
500
+
501
+ describe "have_json_collection matcher" do
502
+
503
+ let( :request ) { @request_factory.get('/v1/api') }
504
+
505
+
506
+ it "fails if the response doesn't have a content type" do
507
+ response = request.response
508
+
509
+ expect {
510
+ expect( response ).to have_json_collection
511
+ }.to fail_matching( /doesn't have a content-type/i )
512
+ end
513
+
514
+
515
+ it "fails if the response doesn't have an 'application/json' content type" do
516
+ response = request.response
517
+ response.content_type = 'text/plain'
518
+ response.body = 'Stuff.'
519
+
520
+ expect {
521
+ expect( response ).to have_json_collection
522
+ }.to fail_matching( /content-type is/i )
523
+ end
524
+
525
+
526
+ it "fails if the response body doesn't contain valid JSON" do
527
+ response = request.response
528
+ response.content_type = 'application/json'
529
+ response.body = '<'
530
+
531
+ expect {
532
+ expect( response ).to have_json_collection
533
+ }.to fail_matching( /invalid JSON/i )
534
+ end
535
+
536
+
537
+ it "fails if the response body isn't an Array" do
538
+ response = request.response
539
+ response.content_type = 'application/json'
540
+ response.body = '{}'
541
+
542
+ expect {
543
+ expect( response ).to have_json_collection
544
+ }.to fail_matching( /response body isn't a json array/i )
545
+ end
546
+
547
+
548
+ it "fails if the response body isn't an Array of Objects" do
549
+ response = request.response
550
+ response.content_type = 'application/json'
551
+ response.body = '[[],[]]'
552
+
553
+ expect {
554
+ expect( response ).to have_json_collection
555
+ }.to fail_matching( /expected \[\] to be a kind of Hash/i )
556
+ end
557
+
558
+
559
+ context "with no additional criteria" do
560
+
561
+ it "passes for a response that has a JSON array of objects" do
562
+ response = request.response
563
+ response.content_type = 'application/json'
564
+ response.body = '[{},{},{}]'
565
+
566
+ expect {
567
+ expect( response ).to have_json_collection
568
+ }.to_not raise_error
569
+ end
570
+
571
+ end
572
+
573
+
574
+ context "with a set of IDs to match" do
575
+
576
+ let( :response ) do
577
+ response = request.response
578
+ response.content_type = 'application/json'
579
+ response.body = '[{"id": 11}, {"id": 19}, {"id": 5}]'
580
+ return response
581
+ end
582
+
583
+
584
+ it "passes for a response that has objects with the same IDs" do
585
+ expect {
586
+ expect( response ).to have_json_collection.with_ids( 5, 11, 19 )
587
+ }.to_not raise_error
588
+ end
589
+
590
+
591
+ it "fails for a response whose objects don't have ID fields" do
592
+ response = request.response
593
+ response.content_type = 'application/json'
594
+ response.body = '[{"size": 11}, {"size": 19}, {"size": 5}]'
595
+
596
+ expect {
597
+ expect( response ).to have_json_collection.with_ids( 5, 11, 19 )
598
+ }.to fail_matching( /expected.*to include :id/i )
599
+ end
600
+
601
+
602
+ it "fails for a response that has objects with extra IDs" do
603
+ expect {
604
+ expect( response ).to have_json_collection.with_ids( 5, 11 )
605
+ }.to fail_matching( /collection has extra ids: \[19\]/i )
606
+ end
607
+
608
+
609
+ it "fails for a response that has objects with missing IDs" do
610
+ expect {
611
+ expect( response ).to have_json_collection.with_ids( 5, 11, 19, 23 )
612
+ }.to fail_matching( /collection is missing expected ids: \[23\]/i )
613
+ end
614
+
615
+
616
+ it "fails for a response that has both missing and extra IDs" do
617
+ expect {
618
+ expect( response ).to have_json_collection.with_ids( 5, 11, 23 )
619
+ }.to fail_matching( /collection is missing expected ids: \[23\]/i )
620
+ end
621
+
622
+ end
623
+
624
+
625
+ context "with a set of model objects to match" do
626
+
627
+ let( :response ) do
628
+ response = request.response
629
+ response.content_type = 'application/json'
630
+ response.body = '[{"id": 11}, {"id": 19}, {"id": 5}]'
631
+ return response
632
+ end
633
+
634
+
635
+ it "passes for a response that has objects with the same PKs" do
636
+ objects = [ 5, 11, 19 ].map {|id| OpenStruct.new( pk: id ) }
637
+ expect {
638
+ expect( response ).to have_json_collection.with_same_ids_as( *objects )
639
+ }.to_not raise_error
640
+ end
641
+
642
+
643
+ it "doesn't require the object Array to be splatted" do
644
+ objects = [ 5, 11, 19 ].map {|id| OpenStruct.new( pk: id ) }
645
+ expect {
646
+ expect( response ).to have_json_collection.with_same_ids_as( objects )
647
+ }.to_not raise_error
648
+ end
649
+
650
+
651
+ it "passes for a response that has fields with the same `:id` key" do
652
+ objects = [ { id: 5 }, { id: 11 }, { id: 19 }, ]
653
+ expect {
654
+ expect( response ).to have_json_collection.with_same_ids_as( *objects )
655
+ }.to_not raise_error
656
+ end
657
+
658
+
659
+ it "fails for a response that has objects with extra IDs" do
660
+ objects = [ 5, 19 ].map {|id| OpenStruct.new( pk: id ) }
661
+ expect {
662
+ expect( response ).to have_json_collection.with_same_ids_as( objects )
663
+ }.to fail_matching( /collection has extra ids: \[11\]/i )
664
+ end
665
+
666
+ end
667
+
668
+
669
+ context "with a set of ordered IDs to match" do
670
+
671
+ let( :response ) do
672
+ response = request.response
673
+ response.content_type = 'application/json'
674
+ response.body = '[{"id": 11}, {"id": 19}, {"id": 5}]'
675
+ return response
676
+ end
677
+
678
+
679
+ it "passes for a response that has objects with the same IDs in the same order" do
680
+ expect {
681
+ expect( response ).to have_json_collection.with_ids( 11, 19, 5 ).in_same_order
682
+ }.to_not raise_error
683
+ end
684
+
685
+
686
+ it "fails for a response with the same IDs but in a different order" do
687
+ expect {
688
+ expect( response ).to have_json_collection.with_ids( 5, 11, 19 ).in_same_order
689
+ }.to fail_matching( /expected collection ids to be/i )
690
+ end
691
+
692
+ end
693
+
694
+
695
+ context "with a set of fields to require" do
696
+
697
+ let( :response ) do
698
+ response = request.response
699
+ response.content_type = 'application/json'
700
+ response.body = '[{"id": 11, "name": "Chris", "age": 23}, ' +
701
+ '{"id": 19, "name": "Simone", "age": 37}]'
702
+ return response
703
+ end
704
+
705
+
706
+ it "passes for a response that has the required fields" do
707
+ expect {
708
+ expect( response ).to have_json_collection.with_fields( :id, :name, :age )
709
+ }.to_not raise_error
710
+ end
711
+
712
+
713
+ it "fails for a response with the same IDs but in a different order" do
714
+ expect {
715
+ expect( response ).to have_json_collection.with_fields( :id, :first_name, :age )
716
+ }.to fail_matching( /to include :first_name/i )
717
+ end
718
+
719
+
720
+ end
721
+
722
+ end
723
+
724
+
725
+ describe "last_response_json_body" do
726
+
727
+ let( :request ) { @request_factory.get('/v1/api') }
728
+
729
+
730
+ context "with a non-JSON response" do
731
+
732
+ let( :last_response ) { request.response }
733
+
734
+
735
+ it "fails due to the have_json_body expectation first" do
736
+ expect {
737
+ expect( last_response_json_body[:title] ).to eq( 'Ethel the Aardvark' )
738
+ }.to fail_matching( /doesn't have a content-type/i )
739
+ end
740
+
741
+ end
742
+
743
+
744
+ context "with a JSON response" do
745
+
746
+ let( :last_response ) do
747
+ response = request.response
748
+ response.content_type = 'application/json'
749
+ response.body = '{"title":"Ethel the Aardvark"}'
750
+ return response
751
+ end
752
+
753
+
754
+ it "returns the JSON body if the inner expectation passes" do
755
+ expect {
756
+ expect( last_response_json_body[:title] ).to eq( 'Ethel the Aardvark' )
757
+ }.to_not raise_error()
758
+ end
759
+
760
+
761
+ it "fails if the outer expectation fails" do
762
+ expect {
763
+ expect( last_response_json_body ).to be_empty
764
+ }.to fail_matching( /empty\?/ )
765
+ end
766
+
767
+ end
768
+
769
+ end
770
+
771
+ end
772
+