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 'loggability'
6
6
 
@@ -1,5 +1,6 @@
1
1
  # -*- ruby -*-
2
2
  # vim: set nosta noet ts=4 sw=4:
3
+ # frozen-string-literal: true
3
4
 
4
5
  require 'digest/sha1'
5
6
  require 'pluggability'
@@ -1,5 +1,6 @@
1
1
  # -*- ruby -*-
2
2
  # vim: set nosta noet ts=4 sw=4:
3
+ # frozen-string-literal: true
3
4
 
4
5
  require 'loggability'
5
6
  require 'securerandom'
@@ -1,5 +1,6 @@
1
1
  # -*- ruby -*-
2
2
  # vim: set nosta noet ts=4 sw=4:
3
+ # frozen-string-literal: true
3
4
 
4
5
  require 'loggability'
5
6
  require 'securerandom'
@@ -1,6 +1,8 @@
1
1
  # -*- ruby -*-
2
2
  # vim: set nosta noet ts=4 sw=4:
3
- # encoding: utf-8
3
+ # frozen-string-literal: true
4
+
5
+ require 'loggability'
4
6
 
5
7
  require 'rspec'
6
8
  require 'rspec/matchers'
@@ -9,10 +11,6 @@ require 'rspec/matchers'
9
11
  # and libraries.
10
12
  module Strelka::Testing
11
13
 
12
- ###############
13
- module_function
14
- ###############
15
-
16
14
  #
17
15
  # Matchers
18
16
  #
@@ -24,15 +22,15 @@ module Strelka::Testing
24
22
  end
25
23
  end
26
24
 
27
- # Collection .all? matcher
28
- RSpec::Matchers.define( :all_be_a ) do |expected|
29
- match do |collection|
30
- collection.all? {|obj| obj.is_a?(expected) }
31
- end
32
- end
25
+ RSpec::Matchers.define_negated_matcher( :exclude, :include )
26
+
33
27
 
34
28
  # finish_with matcher
35
29
  class FinishWithMatcher
30
+ extend Loggability
31
+
32
+ log_to :strelka
33
+
36
34
 
37
35
  ### Create a new matcher for the specified +status+, +expected_message+, and
38
36
  ### +expected_headers+.
@@ -87,7 +85,7 @@ module Strelka::Testing
87
85
  nil
88
86
  end
89
87
 
90
- Loggability[ Strelka ].debug "Test proc called; status info is: %p" % [ status_info ]
88
+ self.log.debug "Test proc called; status info is: %p" % [ status_info ]
91
89
 
92
90
  return self.check_finish( status_info ) &&
93
91
  self.check_status_code( status_info ) &&
@@ -141,13 +139,20 @@ module Strelka::Testing
141
139
  headers = self.expected_headers or return true
142
140
  return true if headers.empty?
143
141
 
144
- status_headers = status_info[:headers]
142
+ status_headers = Mongrel2::Table.new( status_info[:headers] )
145
143
  headers.each do |name, value|
144
+ self.log.debug "Testing for %p header: %p" % [ name, value ]
146
145
  unless status_value = status_headers[ name ]
147
146
  @failure = "a %s header" % [ name ]
148
147
  return false
149
148
  end
150
149
 
150
+ if status_value.empty?
151
+ @failure = "a %s header matching %p, but it was blank" % [ name, value ]
152
+ return false
153
+ end
154
+
155
+ self.log.debug " got value: %p" % [ status_value ]
151
156
  if value.respond_to?( :match )
152
157
  unless value.match( status_value )
153
158
  @failure = "a %s header matching %p, but got %p" %
@@ -182,12 +187,447 @@ module Strelka::Testing
182
187
  end # class FinishWithMatcher
183
188
 
184
189
 
190
+ # RSpec matcher for matching Strelka::HTTPResponse body
191
+ #
192
+ # Expect that the response consists of JSON of some sort:
193
+ #
194
+ # expect( last_response ).to have_json_body
195
+ #
196
+ # Expect that it's a JSON body that deserializes as an Object:
197
+ #
198
+ # expect( last_response ).to have_json_body( Object )
199
+ # # -or-
200
+ # expect( last_response ).to have_json_body( Hash )
201
+ #
202
+ # Expect that it's a JSON body that deserializes as an Array:
203
+ #
204
+ # expect( last_response ).to have_json_body( Array )
205
+ #
206
+ # Expect that it's a JSON body that deserializes as an Object that has
207
+ # expected keys:
208
+ #
209
+ # expect( last_response ).to have_json_body( Object ).
210
+ # that_includes( :id, :first_name, :last_name )
211
+ #
212
+ # Expect that it's a JSON body that deserializes as an Object that has
213
+ # expected keys and values:
214
+ #
215
+ # expect( last_response ).to have_json_body( Object ).
216
+ # that_includes(
217
+ # id: 118,
218
+ # first_name: 'Princess',
219
+ # last_name: 'Buttercup'
220
+ # )
221
+ #
222
+ # Expect that it's a JSON body that has other expected stuff:
223
+ #
224
+ # expect( last_response ).to have_json_body( Object ).
225
+ # that_includes(
226
+ # last_name: a_string_matching(/humperdink/i),
227
+ # profile: a_hash_including(:age, :eyecolor, :tracking_ability)
228
+ # )
229
+ #
230
+ # Expect a JSON Array with objects that all match the criteria:
231
+ #
232
+ # expect( last_response ).to have_json_body( Array ).
233
+ # of_lenth( 20 ).
234
+ # and( all( be_an(Integer) ) )
235
+ #
236
+ class HaveJSONBodyMatcher
237
+ extend Loggability
238
+ include RSpec::Matchers
239
+
240
+
241
+ log_to :strelka
242
+
243
+
244
+ ### Create a new matcher that expects a response with a JSON body. If +expected_type+
245
+ ### is not specified, any JSON body will be sufficient for a match.
246
+ def initialize( expected_type=nil )
247
+ @expected_type = expected_type
248
+ @additional_expectations = []
249
+ @response = nil
250
+ @failure_description = nil
251
+ end
252
+
253
+
254
+ attr_reader :expected_type,
255
+ :additional_expectations,
256
+ :response,
257
+ :failure_description
258
+
259
+
260
+ ### RSpec matcher API -- returns +true+ if all expectations of the specified
261
+ ### +response+ are met.
262
+ def matches?( response )
263
+ @response = response
264
+
265
+ return self.correct_content_type? &&
266
+ self.correct_json_type? &&
267
+ self.matches_additional_expectations?
268
+ rescue Yajl::ParseError => err
269
+ return self.fail_with "Response has invalid JSON body: %s" % [ err.message ]
270
+ end
271
+
272
+
273
+ ### RSpec matcher API -- return a message describing an expectation failure.
274
+ def failure_message
275
+ return "\n---\n%s\n---\n\nReason: %s\n" % [
276
+ self.pretty_print_response,
277
+ self.failure_description
278
+ ]
279
+ end
280
+
281
+
282
+ ### RSpec matcher API -- return a message describing an expectation being met
283
+ ### when the matcher was used in a negated context.
284
+ def failure_message_when_negated
285
+ msg = "expected response not to have a %s" % [ self.describe_type_expectation ]
286
+ msg << " and " << self.describe_additional_expectations.join( ', ' ) unless
287
+ self.additional_expectations.emtpy?
288
+ msg << ", but it did."
289
+
290
+ return "\n---\n%s\n---\n\nReason: %s\n" % [
291
+ self.pretty_print_response,
292
+ msg
293
+ ]
294
+ end
295
+
296
+
297
+ ### Return the response's body parsed as JSON.
298
+ def parsed_response_body
299
+ return @parsed_response_body ||=
300
+ Yajl::Parser.parse( self.response.body, check_utf8: true, symbolize_keys: true )
301
+ end
302
+
303
+
304
+ #
305
+ # Mutators
306
+ #
307
+
308
+ ### Add an additional expectation that the JSON body contains the specified +members+.
309
+ def that_includes( *memberset )
310
+ @additional_expectations << include( *memberset )
311
+ return self
312
+ end
313
+ alias_method :which_includes, :that_includes
314
+
315
+
316
+ ### Add an additional expectation that the JSON body does not contain the
317
+ ### specified +members+.
318
+ def that_excludes( *memberset )
319
+ @additional_expectations << exclude( *memberset )
320
+ return self
321
+ end
322
+
323
+
324
+ ### Add an additional expectation that the JSON body contain the specified
325
+ ### +number+ of members.
326
+ def of_length( number )
327
+ @additional_expectations << have_attributes( length: number )
328
+ return self
329
+ end
330
+ alias_method :of_size, :of_length
331
+
332
+
333
+ ### Add the specified +matchers+ as expectations of the Hash or Array that's
334
+ ### parsed from the JSON body.
335
+ def and( *matchers )
336
+ @additional_expectations.concat( matchers )
337
+ return self
338
+ end
339
+
340
+
341
+ #########
342
+ protected
343
+ #########
344
+
345
+ ### Return a String that contains a pretty-printed version of the response object.
346
+ def pretty_print_response
347
+ return self.response.to_s
348
+ end
349
+
350
+
351
+ ### Return +false+ after setting the failure message to +message+.
352
+ def fail_with( message )
353
+ @failure_description = message
354
+ self.log.error "Failing with: %s" % [ message ]
355
+ return false
356
+ end
357
+
358
+
359
+ ### Returns +true+ if the response has a JSON content-type header.
360
+ def correct_content_type?
361
+ content_type = self.response.headers[:content_type] or
362
+ return self.fail_with "response doesn't have a Content-type header"
363
+
364
+ return fail_with "response's Content-type is %p" % [ content_type ] unless
365
+ content_type.start_with?( 'application/json' ) ||
366
+ content_type.match?( %r|\Aapplication/(vnd\.)?\w+\+json\b| )
367
+
368
+ return true
369
+ end
370
+
371
+
372
+ ### Return an Array of text describing the expectation that the body be an
373
+ ### Object or an Array, if a type was expected. If no type was expected, returns
374
+ ### an empty Array.
375
+ def describe_type_expectation
376
+ return case self.expected_type
377
+ when Object, Hash
378
+ "a JSON Object/Hash body"
379
+ when Array
380
+ "a JSON Array body"
381
+ else
382
+ "a JSON body"
383
+ end
384
+ end
385
+
386
+
387
+ ### Check that the JSON body of the response has the correct type, if a type
388
+ ### was specified.
389
+ def correct_json_type?
390
+ return self.parsed_response_body unless self.expected_type
391
+
392
+ if self.expected_type == Array
393
+ return self.fail_with( "response body isn't a JSON Array" ) unless
394
+ self.parsed_response_body.is_a?( Array )
395
+ elsif self.expected_type == Object || self.expected_type == Hash
396
+ return self.fail_with( "response body isn't a JSON Object" ) unless
397
+ self.parsed_response_body.is_a?( Hash )
398
+ else
399
+ warn "A valid JSON response can't be a %p!" % [ self.expected_type ]
400
+ end
401
+
402
+ return true
403
+ end
404
+
405
+
406
+ ### Return an Array of descriptions of the members that were expected to be included in the
407
+ ### response body, if any were specified. If none were specified, returns an empty
408
+ ### Array.
409
+ def describe_additional_expectations
410
+ return self.additional_expectations.map( &:description )
411
+ end
412
+
413
+
414
+ ### Check that any additional matchers registered via the `.and` mutator also
415
+ ### match the parsed response body.
416
+ def matches_additional_expectations?
417
+ return self.additional_expectations.all? do |matcher|
418
+ matcher.matches?( self.parsed_response_body ) or
419
+ fail_with( matcher.failure_message )
420
+ end
421
+ end
422
+
423
+ end # class HaveJSONBodyMatcher
424
+
425
+
426
+ # RSpec matcher for matching Strelka::HTTPResponse body from a collection endpoint
427
+ #
428
+ # Expect that the response is a JSON Array of Objects:
429
+ #
430
+ # expect( last_response ).to have_json_collection
431
+ #
432
+ # Expect that there be 4 Objects in the collection:
433
+ #
434
+ # expect( last_response ).to have_json_collection.of_length( 4 )
435
+ #
436
+ # Expect that the collection's objects each have an `id` field with the specified
437
+ # IDs:
438
+ #
439
+ # expect( last_response ).to have_json_collection.with_ids( 3, 6, 11, 14 )
440
+ # # -or- with an Array of IDs (no need to splat them)
441
+ # ids = [3, 6, 11, 14]
442
+ # expect( last_response ).to have_json_collection.with_ids( ids )
443
+ #
444
+ # Expect that the collection's objects have the same IDs as an Array of model
445
+ # objects (or other objects that respond to #pk):
446
+ #
447
+ # payments = payment_fixture_factory.take( 4 )
448
+ # expect( last_response ).to have_json_collection.
449
+ # with_same_ids_as( payments )
450
+ #
451
+ # Expect that the collection's objects have the same IDs as an Array of Hashes with
452
+ # `:id` fields:
453
+ #
454
+ # payment_rows = payments_table.where( sender_id: 71524 ).all
455
+ # expect( last_response ).to have_json_collection.
456
+ # with_same_ids_as( payment_rows )
457
+ #
458
+ # Expect that the collection's objects appear in the same order as the source Array:
459
+ #
460
+ # payments = payment_fixture_factory.take( 4 )
461
+ # expect( last_response ).to have_json_collection.
462
+ # with_same_ids_as( payments ).in_same_order
463
+ #
464
+ # Add aggregate matchers for each object in the collection:
465
+ #
466
+ # expect( last_response ).to have_json_collection.
467
+ # with_same_ids_as( payments ).
468
+ # and_all( include(amount_cents: a_value > 0) )
469
+ #
470
+ class HaveJSONCollectionMatcher < HaveJSONBodyMatcher
471
+ include RSpec::Matchers
472
+
473
+
474
+ ### Overridden to set the expected type to Array.
475
+ def initialize # :notnew:
476
+ super( Array )
477
+
478
+ @additional_expectations << all( be_a Hash )
479
+
480
+ @expected_ids = nil
481
+ @collection_ids = nil
482
+ @extra_ids = nil
483
+ @missing_ids = nil
484
+ @order_enforced = false
485
+ end
486
+
487
+
488
+ ######
489
+ public
490
+ ######
491
+
492
+ # Sets of IDs, actual vs. expected
493
+ attr_reader :expected_ids,
494
+ :collection_ids,
495
+ :extra_ids,
496
+ :missing_ids,
497
+ :order_enforced
498
+
499
+
500
+ ### Overridden to include matching against collection IDs.
501
+ def matches?( response )
502
+ return false unless super( response )
503
+
504
+ if @expected_ids
505
+ @collection_ids = self.parsed_response_body.collect {|obj| obj[:id] }
506
+ @extra_ids = @collection_ids - @expected_ids
507
+ @missing_ids = @expected_ids - @collection_ids
508
+ end
509
+
510
+ return self.has_required_ids?
511
+ end
512
+
513
+
514
+ ### Return an Array of text describing the expectation that the body be an
515
+ ### Object or an Array, if a type was expected. If no type was expected, returns
516
+ ### an empty Array.
517
+ def describe_type_expectation
518
+ return "a JSON collection (Array of Objects)"
519
+ end
520
+
521
+
522
+ ### Add the specified +matchers+ as expectations of each member of the collection.
523
+ def and_all( *matchers )
524
+ matchers = matchers.map {|m| all( m ) }
525
+ @additional_expectations.concat( matchers )
526
+ return self
527
+ end
528
+
529
+
530
+ ### Set the expectation that the given +expected_ids+ will be present as the
531
+ ### values of the `:id` field of the collection.
532
+ def with_ids( *expected_ids )
533
+ self.and_all( include :id )
534
+ @expected_ids = expected_ids.flatten( 1 )
535
+ return self
536
+ end
537
+
538
+
539
+ ### Add an expectation that the collection's objects all have an ':id' field,
540
+ ### and that the corresponding values be the same as the primary key values of
541
+ ### the given +objects+ (fetched via their #pk methods).
542
+ def with_same_ids_as( *objects )
543
+ objects.flatten!( 1 )
544
+
545
+ ids = if objects.first.respond_to?( :pk )
546
+ objects.flatten.map( &:pk )
547
+ else
548
+ objects.map {|obj| obj[:id] }
549
+ end
550
+
551
+ return self.with_ids( *ids )
552
+ end
553
+
554
+
555
+ ### Enforce ordering when matching IDs.
556
+ def in_same_order
557
+ @order_enforced = true
558
+ return self
559
+ end
560
+
561
+
562
+ ### Adds an expectation that all members of the resulting collection have each
563
+ ### of the keys in the specified +fieldset+.
564
+ def with_fields( *fieldset )
565
+ return self.and_all( include *fieldset )
566
+ end
567
+ alias_method :and_fields, :with_fields
568
+
569
+
570
+ #########
571
+ protected
572
+ #########
573
+
574
+ ### Returns +true+ if the collection contains exactly the IDs specified by
575
+ ### #with_same_ids_as, or if no IDs were specified.
576
+ def has_required_ids?
577
+ return true unless @expected_ids
578
+
579
+ if @order_enforced && @expected_ids != @collection_ids
580
+ return self.fail_with "expected collection IDs to be %p, but they were: %p" %
581
+ [ @expected_ids, @collection_ids ]
582
+ elsif @missing_ids && !@missing_ids.empty?
583
+ return self.fail_with( "collection is missing expected IDs: %p" % [@missing_ids] )
584
+ elsif @extra_ids && !@extra_ids.empty?
585
+ return self.fail_with( "collection has extra IDs: %p" % [@extra_ids] )
586
+ end
587
+
588
+ return true
589
+ end
590
+
591
+ end # class HaveJSONCollectionMatcher
592
+
593
+
594
+
595
+ ###############
596
+ module_function
597
+ ###############
598
+
185
599
  ### Match a response thrown via the +finish_with+ function.
186
600
  def finish_with( status, message=nil, headers={} )
187
- FinishWithMatcher.new( status, message, headers )
601
+ return FinishWithMatcher.new( status, message, headers )
602
+ end
603
+
604
+
605
+ ### Create a new matcher that will expect the response to have a JSON body of
606
+ ### the +expected_type+. If +expected_type+ is omitted, any JSON body will be sufficient
607
+ ### for a match.
608
+ def have_json_body( expected_type=nil )
609
+ return HaveJSONBodyMatcher.new( expected_type )
188
610
  end
189
611
 
190
612
 
613
+ ### Create a new matcher that will expect the response to have a JSON body which is
614
+ ### an Array of Objects (Hashes).
615
+ def have_json_collection
616
+ return HaveJSONCollectionMatcher.new
617
+ end
618
+
619
+
620
+ ### Parse the body of the last response and return it as a Ruby object.
621
+ def last_response_json_body( expected_type=nil )
622
+ @have_json_body_matcher ||= begin
623
+ matcher = have_json_body( expected_type )
624
+ expect( last_response ).to( matcher )
625
+ matcher
626
+ end
627
+
628
+ return @have_json_body_matcher.parsed_response_body
629
+ end
630
+
191
631
  end # module Strelka::Testing
192
632
 
193
633