mockserver-client 7.0.0 → 7.2.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.
@@ -260,13 +260,264 @@ module MockServer
260
260
  response_body && !response_body.empty? ? JSON.parse(response_body) : {}
261
261
  end
262
262
 
263
- # Verify that a request was received.
264
- # @param request [HttpRequest]
263
+ # -------------------------------------------------------------------
264
+ # Load scenario registry (load injection)
265
+ # -------------------------------------------------------------------
266
+ #
267
+ # The load scenario registry decouples *registering* a scenario from
268
+ # *running* it:
269
+ #
270
+ # * registering (load_scenario) only stores the definition keyed by its
271
+ # unique +name+ and is allowed even when +loadGenerationEnabled+ is off;
272
+ # * starting (start_load_scenarios) is what actually drives traffic and
273
+ # requires +loadGenerationEnabled+ on the server (otherwise a 403).
274
+ #
275
+ # Scenario states are: LOADED, PENDING, RUNNING, COMPLETED, STOPPED.
276
+
277
+ # Register (load) a scenario into the registry without running it.
278
+ #
279
+ # +scenario+ may be a {LoadScenario} model (which responds to +to_h+) or a
280
+ # plain Hash already shaped to the +LoadScenario+ JSON contract. It must
281
+ # carry a unique +name+. Registering is permitted even when load generation
282
+ # is disabled on the server.
283
+ #
284
+ # @param scenario [LoadScenario, Hash] the scenario to register
285
+ # @return [Hash] parsed response of the form { "name" => ..., "state" => ... }
286
+ def load_scenario(scenario)
287
+ payload = scenario.respond_to?(:to_h) ? scenario.to_h : scenario
288
+ body = JSON.generate(payload)
289
+ status, response_body = request('PUT', '/mockserver/loadScenario', body)
290
+ if status >= 400
291
+ raise Error, "Failed to register load scenario (status=#{status}): #{response_body}"
292
+ end
293
+
294
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
295
+ end
296
+
297
+ # List all registered load scenarios.
298
+ #
299
+ # @return [Hash] parsed response of the form
300
+ # { "scenarios" => [ { "name" => ..., "state" => ..., "definition" => ..., "status" => ... }, ... ] }
301
+ def load_scenarios
302
+ status, response_body = request('GET', '/mockserver/loadScenario')
303
+ if status >= 400
304
+ raise Error, "Failed to list load scenarios (status=#{status}): #{response_body}"
305
+ end
306
+
307
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
308
+ end
309
+
310
+ # Fetch a single registered load scenario by name.
311
+ #
312
+ # @param name [String] the unique scenario name
313
+ # @return [Hash] parsed scenario entry { "name" => ..., "state" => ..., "definition" => ..., "status" => ... }
314
+ # @raise [Error] if the scenario does not exist (404) or another failure occurs
315
+ def get_load_scenario(name)
316
+ status, response_body = request('GET', "/mockserver/loadScenario/#{encode_path_segment(name)}")
317
+ if status == 404
318
+ raise Error, "Load scenario not found (status=404): #{name}"
319
+ end
320
+ if status >= 400
321
+ raise Error, "Failed to get load scenario (status=#{status}): #{response_body}"
322
+ end
323
+
324
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
325
+ end
326
+
327
+ # Remove a single registered load scenario by name.
328
+ #
329
+ # @param name [String] the unique scenario name
330
+ # @return [Hash] parsed response (may be empty)
331
+ def delete_load_scenario(name)
332
+ status, response_body = request('DELETE', "/mockserver/loadScenario/#{encode_path_segment(name)}")
333
+ if status >= 400
334
+ raise Error, "Failed to delete load scenario (status=#{status}): #{response_body}"
335
+ end
336
+
337
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
338
+ end
339
+
340
+ # Clear all registered load scenarios.
341
+ #
342
+ # @return [Hash] parsed response (may be empty)
343
+ def clear_load_scenarios
344
+ status, response_body = request('DELETE', '/mockserver/loadScenario')
345
+ if status >= 400
346
+ raise Error, "Failed to clear load scenarios (status=#{status}): #{response_body}"
347
+ end
348
+
349
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
350
+ end
351
+
352
+ # Start one or more registered scenarios.
353
+ #
354
+ # +names+ may be a single scenario name (String) or an Array of names; it is
355
+ # always sent as { "names" => [...] }. Honours each scenario's
356
+ # +startDelayMillis+. Requires +loadGenerationEnabled+ on the server; a 403
357
+ # response raises a clear error explaining the feature is disabled.
358
+ #
359
+ # @param names [String, Array<String>] scenario name(s) to start
360
+ # @return [Hash] parsed response of the form
361
+ # { "started" => [ { "name" => ..., "state" => ... }, ... ], "status" => ... }
362
+ def start_load_scenarios(names)
363
+ payload = { 'names' => Array(names) }
364
+ body = JSON.generate(payload)
365
+ status, response_body = request('PUT', '/mockserver/loadScenario/start', body)
366
+ if status == 403
367
+ raise Error, 'Load scenario start rejected (status=403): load generation is disabled ' \
368
+ '(set loadGenerationEnabled=true on the server to enable it)'
369
+ end
370
+ if status == 404
371
+ raise Error, "Load scenario not found (status=404): #{response_body}"
372
+ end
373
+ if status >= 400
374
+ raise Error, "Failed to start load scenarios (status=#{status}): #{response_body}"
375
+ end
376
+
377
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
378
+ end
379
+
380
+ # Stop running scenarios.
381
+ #
382
+ # +names+ may be:
383
+ # * a single scenario name (String) -> { "names" => ["a"] }
384
+ # * an Array of names -> { "names" => ["a", "b"] }
385
+ # * nil (the default) -> empty body, which stops all running scenarios
386
+ #
387
+ # @param names [String, Array<String>, nil] scenario name(s) to stop, or nil for all
388
+ # @return [Hash] parsed response of the form
389
+ # { "stopped" => [ ... ], "status" => ... }
390
+ def stop_load_scenarios(names = nil)
391
+ body = names.nil? ? nil : JSON.generate({ 'names' => Array(names) })
392
+ status, response_body = request('PUT', '/mockserver/loadScenario/stop', body)
393
+ if status >= 400
394
+ raise Error, "Failed to stop load scenarios (status=#{status}): #{response_body}"
395
+ end
396
+
397
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
398
+ end
399
+
400
+ # Convenience: register a scenario then immediately start it.
401
+ #
402
+ # Equivalent to calling {#load_scenario} followed by {#start_load_scenarios}
403
+ # for the scenario's name. Requires +loadGenerationEnabled+ on the server for
404
+ # the start step.
405
+ #
406
+ # @param scenario [LoadScenario, Hash] the scenario to register and run
407
+ # @return [Hash] parsed response from the start call
408
+ def run_load_scenario(scenario)
409
+ payload = scenario.respond_to?(:to_h) ? scenario.to_h : scenario
410
+ name = payload.respond_to?(:[]) ? (payload['name'] || payload[:name]) : nil
411
+ if name.nil? || name.to_s.empty?
412
+ raise ArgumentError, 'scenario must carry a non-empty name to run'
413
+ end
414
+
415
+ load_scenario(payload)
416
+ start_load_scenarios(name)
417
+ end
418
+
419
+ # -------------------------------------------------------------------
420
+ # Stateful scenarios (state machine control plane)
421
+ # -------------------------------------------------------------------
422
+
423
+ # Return a handle to the named stateful scenario, wrapping the
424
+ # +/mockserver/scenario/{name}+ control-plane endpoints.
425
+ #
426
+ # @param name [String] the scenario (state-machine) name
427
+ # @return [ScenarioHandle]
428
+ def scenario(name)
429
+ ScenarioHandle.new(self, name)
430
+ end
431
+
432
+ # List every known scenario and its current state.
433
+ #
434
+ # @return [Array<ScenarioState>] each with +scenario_name+ and +current_state+
435
+ def scenarios
436
+ result = scenario_request('GET', '/mockserver/scenario')
437
+ list = result.is_a?(Hash) ? (result['scenarios'] || []) : []
438
+ list.map { |s| ScenarioState.from_hash(s) }
439
+ end
440
+
441
+ # @api private
442
+ # Issue a control-plane scenario request, parsing the JSON response and
443
+ # raising {Error} on any >= 400 status. Reuses the same transport
444
+ # (+request+) as the other +/mockserver/...+ control endpoints.
445
+ def scenario_request(method, path, body = nil)
446
+ status, response_body = request(method, path, body)
447
+ if status >= 400
448
+ raise Error, "Scenario request failed (status=#{status}): #{response_body}"
449
+ end
450
+
451
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
452
+ end
453
+
454
+ # -------------------------------------------------------------------
455
+ # gRPC descriptor management
456
+ # -------------------------------------------------------------------
457
+
458
+ # Upload a compiled protobuf descriptor set so gRPC requests can be matched.
459
+ #
460
+ # +descriptor_bytes+ must be the raw bytes of a +FileDescriptorSet+ (e.g. the
461
+ # output of +protoc --descriptor_set_out=... --include_imports+). The bytes are
462
+ # sent verbatim as +application/octet-stream+ (NOT base64-encoded).
463
+ #
464
+ # @param descriptor_bytes [String] raw descriptor set bytes (binary string)
465
+ # @return [nil]
466
+ def upload_grpc_descriptor(descriptor_bytes)
467
+ if descriptor_bytes.nil? || descriptor_bytes.empty?
468
+ raise ArgumentError, 'descriptor bytes must not be empty'
469
+ end
470
+
471
+ status, response_body = request(
472
+ 'PUT', '/mockserver/grpc/descriptors', descriptor_bytes,
473
+ content_type: 'application/octet-stream'
474
+ )
475
+ if status >= 400
476
+ raise Error, "Failed to upload gRPC descriptor (status=#{status}): #{response_body}"
477
+ end
478
+
479
+ nil
480
+ end
481
+
482
+ # Retrieve the gRPC services registered from uploaded descriptor sets.
483
+ #
484
+ # Returns an array of service hashes, each with a +"name"+ and a list of
485
+ # +"methods"+ (+"name"+, +"inputType"+, +"outputType"+, +"clientStreaming"+,
486
+ # +"serverStreaming"+).
487
+ #
488
+ # @return [Array<Hash>]
489
+ def retrieve_grpc_services
490
+ status, response_body = request('PUT', '/mockserver/grpc/services')
491
+ if status >= 400
492
+ raise Error, "Failed to retrieve gRPC services (status=#{status}): #{response_body}"
493
+ end
494
+
495
+ if response_body && !response_body.empty?
496
+ parsed = JSON.parse(response_body)
497
+ return parsed if parsed.is_a?(Array)
498
+ end
499
+ []
500
+ end
501
+
502
+ # Clear all uploaded gRPC descriptor sets and registered services.
503
+ # @return [nil]
504
+ def clear_grpc_descriptors
505
+ status, response_body = request('PUT', '/mockserver/grpc/clear')
506
+ if status >= 400
507
+ raise Error, "Failed to clear gRPC descriptors (status=#{status}): #{response_body}"
508
+ end
509
+
510
+ nil
511
+ end
512
+
513
+ # Verify that a request (and optionally a response) was received.
514
+ # @param request [HttpRequest, nil]
265
515
  # @param times [VerificationTimes, nil]
516
+ # @param response [HttpResponse, nil]
266
517
  # @return [nil]
267
518
  # @raise [VerificationError] if verification fails (HTTP 406)
268
- def verify(request, times: nil)
269
- verification = Verification.new(http_request: request, times: times)
519
+ def verify(request = nil, times: nil, response: nil)
520
+ verification = Verification.new(http_request: request, http_response: response, times: times)
270
521
  body = JSON.generate(verification.to_h)
271
522
  status, response_body = do_request('PUT', '/mockserver/verify', body)
272
523
  if status == 406
@@ -282,10 +533,14 @@ module MockServer
282
533
 
283
534
  # Verify that requests were received in sequence.
284
535
  # @param requests [Array<HttpRequest>]
536
+ # @param responses [Array<HttpResponse>, nil] index-aligned response matchers
285
537
  # @return [nil]
286
538
  # @raise [VerificationError] if verification fails (HTTP 406)
287
- def verify_sequence(*requests)
288
- verification = VerificationSequence.new(http_requests: requests.to_a)
539
+ def verify_sequence(*requests, responses: nil)
540
+ verification = VerificationSequence.new(
541
+ http_requests: requests.empty? ? nil : requests.to_a,
542
+ http_responses: responses
543
+ )
289
544
  body = JSON.generate(verification.to_h)
290
545
  status, response_body = request('PUT', '/mockserver/verifySequence', body)
291
546
  if status == 406
@@ -365,6 +620,44 @@ module MockServer
365
620
  []
366
621
  end
367
622
 
623
+ # Retrieve the active expectations as MockServer SDK setup code (the builder
624
+ # code that recreates the expectations) in the requested language.
625
+ # @param format [String] one of "java", "javascript", "python", "go",
626
+ # "csharp", "ruby", "rust" or "php" (case-insensitive)
627
+ # @param request [HttpRequest, nil]
628
+ # @return [String] the generated code
629
+ def retrieve_expectations_as_code(format: 'java', request: nil)
630
+ body = request ? JSON.generate(request.to_h) : ''
631
+ status, response_body = do_request(
632
+ 'PUT', '/mockserver/retrieve', body,
633
+ { 'type' => 'ACTIVE_EXPECTATIONS', 'format' => format.to_s.upcase }
634
+ )
635
+ if status >= 400
636
+ raise Error, "Failed to retrieve expectations as code (status=#{status}): #{response_body}"
637
+ end
638
+
639
+ response_body || ''
640
+ end
641
+
642
+ # Retrieve the recorded (proxied) request/response pairs as MockServer SDK
643
+ # setup code in the requested language.
644
+ # @param format [String] one of "java", "javascript", "python", "go",
645
+ # "csharp", "ruby", "rust" or "php" (case-insensitive)
646
+ # @param request [HttpRequest, nil]
647
+ # @return [String] the generated code
648
+ def retrieve_recorded_expectations_as_code(format: 'java', request: nil)
649
+ body = request ? JSON.generate(request.to_h) : ''
650
+ status, response_body = do_request(
651
+ 'PUT', '/mockserver/retrieve', body,
652
+ { 'type' => 'RECORDED_EXPECTATIONS', 'format' => format.to_s.upcase }
653
+ )
654
+ if status >= 400
655
+ raise Error, "Failed to retrieve recorded expectations as code (status=#{status}): #{response_body}"
656
+ end
657
+
658
+ response_body || ''
659
+ end
660
+
368
661
  # Retrieve recorded requests and responses.
369
662
  # @param request [HttpRequest, nil]
370
663
  # @return [Array<HttpRequestAndHttpResponse>]
@@ -476,6 +769,120 @@ module MockServer
476
769
  ForwardChainExpectation.new(self, expectation)
477
770
  end
478
771
 
772
+ # -------------------------------------------------------------------
773
+ # Breakpoint matcher management
774
+ # -------------------------------------------------------------------
775
+
776
+ # Register a breakpoint matcher with callback handlers.
777
+ # The callback WebSocket is opened lazily and reused.
778
+ #
779
+ # @param matcher [HttpRequest] the request definition to match
780
+ # @param phases [Array<String>] e.g. ["REQUEST", "RESPONSE"]
781
+ # @param request_handler [Proc, nil] handler for REQUEST phase
782
+ # @param response_handler [Proc, nil] handler for RESPONSE phase
783
+ # @param stream_frame_handler [Proc, nil] handler for streaming phases
784
+ # @return [String] the server-assigned breakpoint matcher id
785
+ def add_breakpoint(matcher, phases,
786
+ request_handler: nil, response_handler: nil,
787
+ stream_frame_handler: nil)
788
+ raise ArgumentError, 'add_breakpoint requires a non-nil matcher' if matcher.nil?
789
+ raise ArgumentError, 'add_breakpoint requires a non-empty phases array' if phases.nil? || phases.empty?
790
+
791
+ ws_client = ensure_breakpoint_websocket
792
+ client_id = ws_client.client_id
793
+
794
+ body = JSON.generate({
795
+ 'httpRequest' => matcher.to_h,
796
+ 'phases' => phases,
797
+ 'clientId' => client_id
798
+ })
799
+ status, response_body = request('PUT', '/mockserver/breakpoint/matcher', body)
800
+ if status >= 400
801
+ raise Error, "Failed to register breakpoint matcher (status=#{status}): #{response_body}"
802
+ end
803
+
804
+ parsed = response_body && !response_body.empty? ? JSON.parse(response_body) : {}
805
+ breakpoint_id = parsed['id']
806
+ raise Error, 'Server did not return a breakpoint id' unless breakpoint_id
807
+
808
+ # Install per-breakpoint-id handlers
809
+ ws_client.set_breakpoint_request_handler(breakpoint_id, request_handler) if request_handler
810
+ ws_client.set_breakpoint_response_handler(breakpoint_id, response_handler) if response_handler
811
+ ws_client.set_breakpoint_stream_frame_handler(breakpoint_id, stream_frame_handler) if stream_frame_handler
812
+
813
+ breakpoint_id
814
+ end
815
+
816
+ # Convenience: register a REQUEST-only breakpoint.
817
+ # @param matcher [HttpRequest]
818
+ # @param request_handler [Proc]
819
+ # @return [String]
820
+ def add_request_breakpoint(matcher, request_handler)
821
+ add_breakpoint(matcher, ['REQUEST'], request_handler: request_handler)
822
+ end
823
+
824
+ # Convenience: register a REQUEST+RESPONSE breakpoint.
825
+ # @param matcher [HttpRequest]
826
+ # @param request_handler [Proc]
827
+ # @param response_handler [Proc]
828
+ # @return [String]
829
+ def add_request_and_response_breakpoint(matcher, request_handler, response_handler)
830
+ add_breakpoint(matcher, %w[REQUEST RESPONSE],
831
+ request_handler: request_handler,
832
+ response_handler: response_handler)
833
+ end
834
+
835
+ # List all registered breakpoint matchers.
836
+ # @return [Hash] e.g. {"matchers" => [{...}, ...]}
837
+ def list_breakpoint_matchers
838
+ status, response_body = request('GET', '/mockserver/breakpoint/matchers')
839
+ if status >= 400
840
+ raise Error, "Failed to list breakpoint matchers (status=#{status}): #{response_body}"
841
+ end
842
+
843
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
844
+ end
845
+
846
+ # Remove a breakpoint matcher by id.
847
+ # @param breakpoint_id [String]
848
+ # @return [Hash]
849
+ def remove_breakpoint_matcher(breakpoint_id)
850
+ raise ArgumentError, 'remove_breakpoint_matcher requires a non-empty id' if breakpoint_id.nil? || breakpoint_id.empty?
851
+
852
+ body = JSON.generate({ 'id' => breakpoint_id })
853
+ status, response_body = request('PUT', '/mockserver/breakpoint/matcher/remove', body)
854
+ if status >= 400
855
+ raise Error, "Failed to remove breakpoint matcher (status=#{status}): #{response_body}"
856
+ end
857
+
858
+ # Remove client-side handlers
859
+ @websocket_mutex.synchronize do
860
+ @websocket_clients.each do |ws|
861
+ ws.remove_breakpoint_handlers(breakpoint_id) if ws.respond_to?(:remove_breakpoint_handlers)
862
+ end
863
+ end
864
+
865
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
866
+ end
867
+
868
+ # Clear all registered breakpoint matchers.
869
+ # @return [Hash]
870
+ def clear_breakpoint_matchers
871
+ status, response_body = request('PUT', '/mockserver/breakpoint/matcher/clear')
872
+ if status >= 400
873
+ raise Error, "Failed to clear breakpoint matchers (status=#{status}): #{response_body}"
874
+ end
875
+
876
+ # Clear client-side handlers
877
+ @websocket_mutex.synchronize do
878
+ @websocket_clients.each do |ws|
879
+ ws.clear_breakpoint_handlers if ws.respond_to?(:clear_breakpoint_handlers)
880
+ end
881
+ end
882
+
883
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
884
+ end
885
+
479
886
  # -------------------------------------------------------------------
480
887
  # Callback methods
481
888
  # -------------------------------------------------------------------
@@ -530,6 +937,32 @@ module MockServer
530
937
 
531
938
  private
532
939
 
940
+ # @api private
941
+ # Ensure a callback WS is connected for breakpoint use, returning it.
942
+ def ensure_breakpoint_websocket
943
+ # Hold the mutex for the whole check-create-append so two concurrent
944
+ # add_breakpoint calls cannot both create a breakpoint WS (TOCTOU).
945
+ # Breakpoint WS creation is rare, so blocking on connect under the lock
946
+ # is acceptable.
947
+ @websocket_mutex.synchronize do
948
+ existing = @websocket_clients.find { |ws| ws.instance_variable_get(:@is_breakpoint_ws) }
949
+ return existing if existing
950
+
951
+ ws_client = WebSocketClient.new
952
+ ws_client.connect(
953
+ @host, @port,
954
+ context_path: @context_path,
955
+ secure: @secure,
956
+ ca_cert_path: @ca_cert_path,
957
+ tls_verify: @tls_verify
958
+ )
959
+ ws_client.instance_variable_set(:@is_breakpoint_ws, true)
960
+ ws_client.listen
961
+ @websocket_clients << ws_client
962
+ ws_client
963
+ end
964
+ end
965
+
533
966
  # @api private
534
967
  def register_websocket_callback(callback_type, callback_fn, forward_response_fn = nil)
535
968
  ws_client = WebSocketClient.new
@@ -555,7 +988,8 @@ module MockServer
555
988
 
556
989
  # Perform an HTTP request with optional query parameters.
557
990
  # @api private
558
- def do_request(method, path, body = nil, query_params = nil)
991
+ def do_request(method, path, body = nil, query_params = nil,
992
+ content_type: 'application/json; charset=utf-8')
559
993
  url = "#{@base_url}#{path}"
560
994
  if query_params && !query_params.empty?
561
995
  url = "#{url}?#{URI.encode_www_form(query_params)}"
@@ -564,14 +998,24 @@ module MockServer
564
998
  uri = URI.parse(url)
565
999
  http = build_http(uri)
566
1000
 
567
- req = build_request(method, uri, body)
1001
+ req = build_request(method, uri, body, content_type)
568
1002
  execute_request(http, req)
569
1003
  end
570
1004
 
571
1005
  # Perform an HTTP request (no query params).
572
1006
  # @api private
573
- def request(method, path, body = nil)
574
- do_request(method, path, body, nil)
1007
+ def request(method, path, body = nil,
1008
+ content_type: 'application/json; charset=utf-8')
1009
+ do_request(method, path, body, nil, content_type: content_type)
1010
+ end
1011
+
1012
+ # @api private
1013
+ # Percent-encode a single URL path segment (e.g. a scenario name) so that
1014
+ # spaces, slashes, and other reserved characters are transmitted safely.
1015
+ def encode_path_segment(value)
1016
+ raise ArgumentError, 'name must not be nil or empty' if value.nil? || value.to_s.empty?
1017
+
1018
+ URI.encode_www_form_component(value.to_s).gsub('+', '%20')
575
1019
  end
576
1020
 
577
1021
  # @api private
@@ -596,7 +1040,7 @@ module MockServer
596
1040
  end
597
1041
 
598
1042
  # @api private
599
- def build_request(method, uri, body)
1043
+ def build_request(method, uri, body, content_type = 'application/json; charset=utf-8')
600
1044
  request_path = uri.request_uri
601
1045
  case method.upcase
602
1046
  when 'PUT'
@@ -610,14 +1054,19 @@ module MockServer
610
1054
  else
611
1055
  req = Net::HTTP::Put.new(request_path)
612
1056
  end
613
- req['Content-Type'] = 'application/json; charset=utf-8'
1057
+ req['Content-Type'] = content_type
614
1058
  req.body = body if body
615
1059
  req
616
1060
  end
617
1061
 
618
1062
  # @api private
619
1063
  def execute_request(http, req)
620
- response = http.request(req)
1064
+ # Use the block form of #start so the underlying TCP/TLS connection is
1065
+ # always closed (#finish) when the request completes, rather than being
1066
+ # left open until garbage collection. All connection options (use_ssl,
1067
+ # ca_file, verify_mode, read_timeout, open_timeout) configured on +http+
1068
+ # by #build_http are preserved because #start operates on this instance.
1069
+ response = http.start { |conn| conn.request(req) }
621
1070
  [response.code.to_i, response.body || '']
622
1071
  rescue Net::OpenTimeout, Net::ReadTimeout => e
623
1072
  raise ConnectionError, "Request to MockServer at #{@base_url} timed out: #{e.message}"
@@ -182,14 +182,24 @@ module MockServer
182
182
  @client.upsert(@expectation)
183
183
  end
184
184
 
185
- # Set a forward class callback action.
186
- # @param class_callback [HttpClassCallback]
185
+ # Set a response class-callback action: the server invokes a server-side
186
+ # class implementing the response callback interface. Accepts either a
187
+ # fully-qualified class-name String (e.g. "com.example.MyResponseCallback")
188
+ # or a pre-built {HttpClassCallback} (carrying an optional +delay+ /
189
+ # +primary+).
190
+ # @param class_callback [String, HttpClassCallback]
191
+ # @return [Array<Expectation>]
192
+ def respond_with_class_callback(class_callback)
193
+ @expectation.http_response_class_callback = class_callback
194
+ @client.upsert(@expectation)
195
+ end
196
+
197
+ # Set a forward class-callback action: the server invokes a server-side
198
+ # class implementing the forward callback interface. Accepts either a
199
+ # fully-qualified class-name String or a pre-built {HttpClassCallback}.
200
+ # @param class_callback [String, HttpClassCallback]
187
201
  # @return [Array<Expectation>]
188
202
  def forward_with_class_callback(class_callback)
189
- unless class_callback.is_a?(HttpClassCallback)
190
- raise TypeError,
191
- "Expected HttpClassCallback, got #{class_callback.class.name}"
192
- end
193
203
  @expectation.http_forward_class_callback = class_callback
194
204
  @client.upsert(@expectation)
195
205
  end