strelka 0.15.0 → 0.16.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.
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