mockserver-client 7.0.0 → 7.1.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,14 @@ 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
+ # Verify that a request (and optionally a response) was received.
264
+ # @param request [HttpRequest, nil]
265
265
  # @param times [VerificationTimes, nil]
266
+ # @param response [HttpResponse, nil]
266
267
  # @return [nil]
267
268
  # @raise [VerificationError] if verification fails (HTTP 406)
268
- def verify(request, times: nil)
269
- verification = Verification.new(http_request: request, times: times)
269
+ def verify(request = nil, times: nil, response: nil)
270
+ verification = Verification.new(http_request: request, http_response: response, times: times)
270
271
  body = JSON.generate(verification.to_h)
271
272
  status, response_body = do_request('PUT', '/mockserver/verify', body)
272
273
  if status == 406
@@ -282,10 +283,14 @@ module MockServer
282
283
 
283
284
  # Verify that requests were received in sequence.
284
285
  # @param requests [Array<HttpRequest>]
286
+ # @param responses [Array<HttpResponse>, nil] index-aligned response matchers
285
287
  # @return [nil]
286
288
  # @raise [VerificationError] if verification fails (HTTP 406)
287
- def verify_sequence(*requests)
288
- verification = VerificationSequence.new(http_requests: requests.to_a)
289
+ def verify_sequence(*requests, responses: nil)
290
+ verification = VerificationSequence.new(
291
+ http_requests: requests.empty? ? nil : requests.to_a,
292
+ http_responses: responses
293
+ )
289
294
  body = JSON.generate(verification.to_h)
290
295
  status, response_body = request('PUT', '/mockserver/verifySequence', body)
291
296
  if status == 406
@@ -476,6 +481,120 @@ module MockServer
476
481
  ForwardChainExpectation.new(self, expectation)
477
482
  end
478
483
 
484
+ # -------------------------------------------------------------------
485
+ # Breakpoint matcher management
486
+ # -------------------------------------------------------------------
487
+
488
+ # Register a breakpoint matcher with callback handlers.
489
+ # The callback WebSocket is opened lazily and reused.
490
+ #
491
+ # @param matcher [HttpRequest] the request definition to match
492
+ # @param phases [Array<String>] e.g. ["REQUEST", "RESPONSE"]
493
+ # @param request_handler [Proc, nil] handler for REQUEST phase
494
+ # @param response_handler [Proc, nil] handler for RESPONSE phase
495
+ # @param stream_frame_handler [Proc, nil] handler for streaming phases
496
+ # @return [String] the server-assigned breakpoint matcher id
497
+ def add_breakpoint(matcher, phases,
498
+ request_handler: nil, response_handler: nil,
499
+ stream_frame_handler: nil)
500
+ raise ArgumentError, 'add_breakpoint requires a non-nil matcher' if matcher.nil?
501
+ raise ArgumentError, 'add_breakpoint requires a non-empty phases array' if phases.nil? || phases.empty?
502
+
503
+ ws_client = ensure_breakpoint_websocket
504
+ client_id = ws_client.client_id
505
+
506
+ body = JSON.generate({
507
+ 'httpRequest' => matcher.to_h,
508
+ 'phases' => phases,
509
+ 'clientId' => client_id
510
+ })
511
+ status, response_body = request('PUT', '/mockserver/breakpoint/matcher', body)
512
+ if status >= 400
513
+ raise Error, "Failed to register breakpoint matcher (status=#{status}): #{response_body}"
514
+ end
515
+
516
+ parsed = response_body && !response_body.empty? ? JSON.parse(response_body) : {}
517
+ breakpoint_id = parsed['id']
518
+ raise Error, 'Server did not return a breakpoint id' unless breakpoint_id
519
+
520
+ # Install per-breakpoint-id handlers
521
+ ws_client.set_breakpoint_request_handler(breakpoint_id, request_handler) if request_handler
522
+ ws_client.set_breakpoint_response_handler(breakpoint_id, response_handler) if response_handler
523
+ ws_client.set_breakpoint_stream_frame_handler(breakpoint_id, stream_frame_handler) if stream_frame_handler
524
+
525
+ breakpoint_id
526
+ end
527
+
528
+ # Convenience: register a REQUEST-only breakpoint.
529
+ # @param matcher [HttpRequest]
530
+ # @param request_handler [Proc]
531
+ # @return [String]
532
+ def add_request_breakpoint(matcher, request_handler)
533
+ add_breakpoint(matcher, ['REQUEST'], request_handler: request_handler)
534
+ end
535
+
536
+ # Convenience: register a REQUEST+RESPONSE breakpoint.
537
+ # @param matcher [HttpRequest]
538
+ # @param request_handler [Proc]
539
+ # @param response_handler [Proc]
540
+ # @return [String]
541
+ def add_request_and_response_breakpoint(matcher, request_handler, response_handler)
542
+ add_breakpoint(matcher, %w[REQUEST RESPONSE],
543
+ request_handler: request_handler,
544
+ response_handler: response_handler)
545
+ end
546
+
547
+ # List all registered breakpoint matchers.
548
+ # @return [Hash] e.g. {"matchers" => [{...}, ...]}
549
+ def list_breakpoint_matchers
550
+ status, response_body = request('GET', '/mockserver/breakpoint/matchers')
551
+ if status >= 400
552
+ raise Error, "Failed to list breakpoint matchers (status=#{status}): #{response_body}"
553
+ end
554
+
555
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
556
+ end
557
+
558
+ # Remove a breakpoint matcher by id.
559
+ # @param breakpoint_id [String]
560
+ # @return [Hash]
561
+ def remove_breakpoint_matcher(breakpoint_id)
562
+ raise ArgumentError, 'remove_breakpoint_matcher requires a non-empty id' if breakpoint_id.nil? || breakpoint_id.empty?
563
+
564
+ body = JSON.generate({ 'id' => breakpoint_id })
565
+ status, response_body = request('PUT', '/mockserver/breakpoint/matcher/remove', body)
566
+ if status >= 400
567
+ raise Error, "Failed to remove breakpoint matcher (status=#{status}): #{response_body}"
568
+ end
569
+
570
+ # Remove client-side handlers
571
+ @websocket_mutex.synchronize do
572
+ @websocket_clients.each do |ws|
573
+ ws.remove_breakpoint_handlers(breakpoint_id) if ws.respond_to?(:remove_breakpoint_handlers)
574
+ end
575
+ end
576
+
577
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
578
+ end
579
+
580
+ # Clear all registered breakpoint matchers.
581
+ # @return [Hash]
582
+ def clear_breakpoint_matchers
583
+ status, response_body = request('PUT', '/mockserver/breakpoint/matcher/clear')
584
+ if status >= 400
585
+ raise Error, "Failed to clear breakpoint matchers (status=#{status}): #{response_body}"
586
+ end
587
+
588
+ # Clear client-side handlers
589
+ @websocket_mutex.synchronize do
590
+ @websocket_clients.each do |ws|
591
+ ws.clear_breakpoint_handlers if ws.respond_to?(:clear_breakpoint_handlers)
592
+ end
593
+ end
594
+
595
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
596
+ end
597
+
479
598
  # -------------------------------------------------------------------
480
599
  # Callback methods
481
600
  # -------------------------------------------------------------------
@@ -530,6 +649,32 @@ module MockServer
530
649
 
531
650
  private
532
651
 
652
+ # @api private
653
+ # Ensure a callback WS is connected for breakpoint use, returning it.
654
+ def ensure_breakpoint_websocket
655
+ # Hold the mutex for the whole check-create-append so two concurrent
656
+ # add_breakpoint calls cannot both create a breakpoint WS (TOCTOU).
657
+ # Breakpoint WS creation is rare, so blocking on connect under the lock
658
+ # is acceptable.
659
+ @websocket_mutex.synchronize do
660
+ existing = @websocket_clients.find { |ws| ws.instance_variable_get(:@is_breakpoint_ws) }
661
+ return existing if existing
662
+
663
+ ws_client = WebSocketClient.new
664
+ ws_client.connect(
665
+ @host, @port,
666
+ context_path: @context_path,
667
+ secure: @secure,
668
+ ca_cert_path: @ca_cert_path,
669
+ tls_verify: @tls_verify
670
+ )
671
+ ws_client.instance_variable_set(:@is_breakpoint_ws, true)
672
+ ws_client.listen
673
+ @websocket_clients << ws_client
674
+ ws_client
675
+ end
676
+ end
677
+
533
678
  # @api private
534
679
  def register_websocket_callback(callback_type, callback_fn, forward_response_fn = nil)
535
680
  ws_client = WebSocketClient.new
@@ -44,6 +44,8 @@ module MockServer
44
44
  'http_sse_response' => 'httpSseResponse',
45
45
  'http_websocket_response' => 'httpWebSocketResponse',
46
46
  'template_type' => 'templateType',
47
+ 'template_file' => 'templateFile',
48
+ 'file_path' => 'filePath',
47
49
  'base64_bytes' => 'base64Bytes',
48
50
  'not_body' => 'not',
49
51
  'content_type' => 'contentType',
@@ -81,7 +83,8 @@ module MockServer
81
83
  'degradation_ramp_millis' => 'degradationRampMillis',
82
84
  'http_class_callback' => 'httpClassCallback',
83
85
  'http_object_callback' => 'httpObjectCallback',
84
- 'failure_policy' => 'failurePolicy'
86
+ 'failure_policy' => 'failurePolicy',
87
+ 'http_responses' => 'httpResponses'
85
88
  }.freeze
86
89
 
87
90
  REVERSE_FIELD_MAP = FIELD_MAP.invert.freeze
@@ -89,7 +92,7 @@ module MockServer
89
92
  # Known Body type strings used to distinguish Body objects from plain hashes
90
93
  # during deserialization.
91
94
  BODY_TYPES = Set.new(%w[
92
- STRING JSON REGEX XML BINARY JSON_SCHEMA JSON_PATH XPATH XML_SCHEMA JSON_RPC GRAPHQL
95
+ STRING JSON REGEX XML BINARY JSON_SCHEMA JSON_PATH XPATH XML_SCHEMA JSON_RPC GRAPHQL FILE
93
96
  ]).freeze
94
97
 
95
98
  # -------------------------------------------------------------------
@@ -359,9 +362,11 @@ module MockServer
359
362
  end
360
363
 
361
364
  class Body
362
- attr_accessor :type, :string, :json, :base64_bytes, :not_body, :content_type, :charset
365
+ attr_accessor :type, :string, :json, :base64_bytes, :not_body, :content_type, :charset,
366
+ :file_path, :template_type
363
367
 
364
- def initialize(type: nil, string: nil, json: nil, base64_bytes: nil, not_body: nil, content_type: nil, charset: nil)
368
+ def initialize(type: nil, string: nil, json: nil, base64_bytes: nil, not_body: nil,
369
+ content_type: nil, charset: nil, file_path: nil, template_type: nil)
365
370
  @type = type
366
371
  @string = string
367
372
  @json = json
@@ -369,17 +374,21 @@ module MockServer
369
374
  @not_body = not_body
370
375
  @content_type = content_type
371
376
  @charset = charset
377
+ @file_path = file_path
378
+ @template_type = template_type
372
379
  end
373
380
 
374
381
  def to_h
375
382
  result = {}
376
- result['type'] = @type unless @type.nil?
377
- result['string'] = @string unless @string.nil?
378
- result['json'] = @json unless @json.nil?
379
- result['base64Bytes'] = @base64_bytes unless @base64_bytes.nil?
380
- result['not'] = @not_body unless @not_body.nil?
381
- result['contentType'] = @content_type unless @content_type.nil?
382
- result['charset'] = @charset unless @charset.nil?
383
+ result['type'] = @type unless @type.nil?
384
+ result['string'] = @string unless @string.nil?
385
+ result['json'] = @json unless @json.nil?
386
+ result['base64Bytes'] = @base64_bytes unless @base64_bytes.nil?
387
+ result['not'] = @not_body unless @not_body.nil?
388
+ result['contentType'] = @content_type unless @content_type.nil?
389
+ result['charset'] = @charset unless @charset.nil?
390
+ result['filePath'] = @file_path unless @file_path.nil?
391
+ result['templateType'] = @template_type unless @template_type.nil?
383
392
  result
384
393
  end
385
394
 
@@ -387,13 +396,15 @@ module MockServer
387
396
  return nil if data.nil?
388
397
 
389
398
  new(
390
- type: data['type'],
391
- string: data['string'],
392
- json: data['json'],
393
- base64_bytes: data['base64Bytes'],
394
- not_body: data['not'],
395
- content_type: data['contentType'],
396
- charset: data['charset']
399
+ type: data['type'],
400
+ string: data['string'],
401
+ json: data['json'],
402
+ base64_bytes: data['base64Bytes'],
403
+ not_body: data['not'],
404
+ content_type: data['contentType'],
405
+ charset: data['charset'],
406
+ file_path: data['filePath'],
407
+ template_type: data['templateType']
397
408
  )
398
409
  end
399
410
 
@@ -417,6 +428,10 @@ module MockServer
417
428
  new(type: 'XML', string: value)
418
429
  end
419
430
 
431
+ def self.file(file_path, content_type: nil, template_type: nil)
432
+ new(type: 'FILE', file_path: file_path, content_type: content_type, template_type: template_type)
433
+ end
434
+
420
435
  def self.json_rpc(method_name, params_schema: nil)
421
436
  JsonRpcBody.new(method_name: method_name, params_schema: params_schema)
422
437
  end
@@ -424,6 +439,11 @@ module MockServer
424
439
  def self.graphql(query, operation_name: nil, variables_schema: nil)
425
440
  GraphQLBody.new(query: query, operation_name: operation_name, variables_schema: variables_schema)
426
441
  end
442
+
443
+ def with_template_type(template_type)
444
+ @template_type = template_type
445
+ self
446
+ end
427
447
  end
428
448
 
429
449
  class JsonRpcBody
@@ -809,11 +829,12 @@ module MockServer
809
829
  end
810
830
 
811
831
  class HttpTemplate
812
- attr_accessor :template_type, :template, :delay, :primary
832
+ attr_accessor :template_type, :template, :template_file, :delay, :primary
813
833
 
814
- def initialize(template_type: 'JAVASCRIPT', template: nil, delay: nil, primary: nil)
834
+ def initialize(template_type: 'JAVASCRIPT', template: nil, template_file: nil, delay: nil, primary: nil)
815
835
  @template_type = template_type
816
836
  @template = template
837
+ @template_file = template_file
817
838
  @delay = delay
818
839
  @primary = primary
819
840
  end
@@ -822,6 +843,7 @@ module MockServer
822
843
  MockServer.strip_none({
823
844
  'templateType' => @template_type,
824
845
  'template' => @template,
846
+ 'templateFile' => @template_file,
825
847
  'delay' => @delay&.to_h,
826
848
  'primary' => @primary
827
849
  })
@@ -833,13 +855,19 @@ module MockServer
833
855
  new(
834
856
  template_type: data.fetch('templateType', 'JAVASCRIPT'),
835
857
  template: data['template'],
858
+ template_file: data['templateFile'],
836
859
  delay: Delay.from_hash(data['delay']),
837
860
  primary: data['primary']
838
861
  )
839
862
  end
840
863
 
841
- def self.template(template_type, template = nil)
842
- new(template_type: template_type, template: template)
864
+ def self.template(template_type, template = nil, template_file: nil)
865
+ new(template_type: template_type, template: template, template_file: template_file)
866
+ end
867
+
868
+ def with_template_file(template_file)
869
+ @template_file = template_file
870
+ self
843
871
  end
844
872
  end
845
873
 
@@ -1910,12 +1938,13 @@ module MockServer
1910
1938
  end
1911
1939
 
1912
1940
  class Verification
1913
- attr_accessor :http_request, :expectation_id, :times,
1941
+ attr_accessor :http_request, :http_response, :expectation_id, :times,
1914
1942
  :maximum_number_of_request_to_return_in_verification_failure
1915
1943
 
1916
- def initialize(http_request: nil, expectation_id: nil, times: nil,
1944
+ def initialize(http_request: nil, http_response: nil, expectation_id: nil, times: nil,
1917
1945
  maximum_number_of_request_to_return_in_verification_failure: nil)
1918
1946
  @http_request = http_request
1947
+ @http_response = http_response
1919
1948
  @expectation_id = expectation_id
1920
1949
  @times = times
1921
1950
  @maximum_number_of_request_to_return_in_verification_failure = maximum_number_of_request_to_return_in_verification_failure
@@ -1924,6 +1953,7 @@ module MockServer
1924
1953
  def to_h
1925
1954
  MockServer.strip_none({
1926
1955
  'httpRequest' => @http_request&.to_h,
1956
+ 'httpResponse' => @http_response&.to_h,
1927
1957
  'expectationId' => @expectation_id&.to_h,
1928
1958
  'times' => @times&.to_h,
1929
1959
  'maximumNumberOfRequestToReturnInVerificationFailure' => @maximum_number_of_request_to_return_in_verification_failure
@@ -1935,6 +1965,7 @@ module MockServer
1935
1965
 
1936
1966
  new(
1937
1967
  http_request: HttpRequest.from_hash(data['httpRequest']),
1968
+ http_response: HttpResponse.from_hash(data['httpResponse']),
1938
1969
  expectation_id: ExpectationId.from_hash(data['expectationId']),
1939
1970
  times: VerificationTimes.from_hash(data['times']),
1940
1971
  maximum_number_of_request_to_return_in_verification_failure: data['maximumNumberOfRequestToReturnInVerificationFailure']
@@ -1943,16 +1974,18 @@ module MockServer
1943
1974
  end
1944
1975
 
1945
1976
  class VerificationSequence
1946
- attr_accessor :http_requests, :expectation_ids
1977
+ attr_accessor :http_requests, :http_responses, :expectation_ids
1947
1978
 
1948
- def initialize(http_requests: nil, expectation_ids: nil)
1979
+ def initialize(http_requests: nil, http_responses: nil, expectation_ids: nil)
1949
1980
  @http_requests = http_requests
1981
+ @http_responses = http_responses
1950
1982
  @expectation_ids = expectation_ids
1951
1983
  end
1952
1984
 
1953
1985
  def to_h
1954
1986
  MockServer.strip_none({
1955
1987
  'httpRequests' => @http_requests&.map(&:to_h),
1988
+ 'httpResponses' => @http_responses&.map(&:to_h),
1956
1989
  'expectationIds' => @expectation_ids&.map(&:to_h)
1957
1990
  })
1958
1991
  end
@@ -1961,9 +1994,11 @@ module MockServer
1961
1994
  return nil if data.nil?
1962
1995
 
1963
1996
  http_requests_data = data['httpRequests']
1997
+ http_responses_data = data['httpResponses']
1964
1998
  expectation_ids_data = data['expectationIds']
1965
1999
  new(
1966
- http_requests: http_requests_data&.map { |r| HttpRequest.from_hash(r) },
2000
+ http_requests: http_requests_data&.map { |r| HttpRequest.from_hash(r) },
2001
+ http_responses: http_responses_data&.map { |r| HttpResponse.from_hash(r) },
1967
2002
  expectation_ids: expectation_ids_data&.map { |e| ExpectationId.from_hash(e) }
1968
2003
  )
1969
2004
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MockServer
4
- VERSION = '7.0.0'
4
+ VERSION = '7.1.0'
5
5
  end
@@ -8,6 +8,7 @@ require 'timeout'
8
8
 
9
9
  module MockServer
10
10
  WEB_SOCKET_CORRELATION_ID_HEADER_NAME = 'WebSocketCorrelationId'
11
+ BREAKPOINT_ID_HEADER_NAME = 'X-MockServer-BreakpointId'
11
12
  CLIENT_REGISTRATION_ID_HEADER = 'X-CLIENT-REGISTRATION-ID'
12
13
 
13
14
  WEBSOCKET_PATH = '/_mockserver_callback_websocket'
@@ -17,6 +18,8 @@ module MockServer
17
18
  TYPE_HTTP_REQUEST_AND_RESPONSE = 'org.mockserver.model.HttpRequestAndHttpResponse'
18
19
  TYPE_CLIENT_ID_DTO = 'org.mockserver.serialization.model.WebSocketClientIdDTO'
19
20
  TYPE_ERROR_DTO = 'org.mockserver.serialization.model.WebSocketErrorDTO'
21
+ TYPE_PAUSED_STREAM_FRAME_DTO = 'org.mockserver.serialization.model.PausedStreamFrameDTO'
22
+ TYPE_STREAM_FRAME_DECISION_DTO = 'org.mockserver.serialization.model.StreamFrameDecisionDTO'
20
23
 
21
24
  MAX_RECONNECT_ATTEMPTS = 3
22
25
  REGISTRATION_TIMEOUT = 10
@@ -32,6 +35,17 @@ module MockServer
32
35
  nil
33
36
  end
34
37
 
38
+ def self.extract_breakpoint_id(request)
39
+ return nil if request.headers.nil?
40
+
41
+ request.headers.each do |header|
42
+ if header.name == BREAKPOINT_ID_HEADER_NAME
43
+ return header.values.first if header.values && !header.values.empty?
44
+ end
45
+ end
46
+ nil
47
+ end
48
+
35
49
  def self.add_correlation_id_header(message, correlation_id)
36
50
  message.headers ||= []
37
51
  message.headers.each do |header|
@@ -94,6 +108,10 @@ module MockServer
94
108
  @logger.progname = 'MockServer::WebSocketClient'
95
109
  @logger.level = Logger::WARN
96
110
  @registration_queue = nil
111
+ # Per-breakpoint-id handlers for matcher-driven breakpoints
112
+ @breakpoint_request_handlers = {}
113
+ @breakpoint_response_handlers = {}
114
+ @breakpoint_stream_frame_handlers = {}
97
115
  end
98
116
 
99
117
  def connected?
@@ -124,6 +142,37 @@ module MockServer
124
142
  @forward_response_callback = response_fn
125
143
  end
126
144
 
145
+ # Register a REQUEST-phase breakpoint handler keyed by breakpoint id.
146
+ def set_breakpoint_request_handler(breakpoint_id, handler)
147
+ @breakpoint_request_handlers[breakpoint_id] = handler if breakpoint_id && handler
148
+ end
149
+
150
+ # Register a RESPONSE-phase breakpoint handler keyed by breakpoint id.
151
+ def set_breakpoint_response_handler(breakpoint_id, handler)
152
+ @breakpoint_response_handlers[breakpoint_id] = handler if breakpoint_id && handler
153
+ end
154
+
155
+ # Register a stream-frame breakpoint handler keyed by breakpoint id.
156
+ def set_breakpoint_stream_frame_handler(breakpoint_id, handler)
157
+ @breakpoint_stream_frame_handlers[breakpoint_id] = handler if breakpoint_id && handler
158
+ end
159
+
160
+ # Remove all handlers for the given breakpoint id.
161
+ def remove_breakpoint_handlers(breakpoint_id)
162
+ return unless breakpoint_id
163
+
164
+ @breakpoint_request_handlers.delete(breakpoint_id)
165
+ @breakpoint_response_handlers.delete(breakpoint_id)
166
+ @breakpoint_stream_frame_handlers.delete(breakpoint_id)
167
+ end
168
+
169
+ # Remove all breakpoint handlers.
170
+ def clear_breakpoint_handlers
171
+ @breakpoint_request_handlers.clear
172
+ @breakpoint_response_handlers.clear
173
+ @breakpoint_stream_frame_handlers.clear
174
+ end
175
+
127
176
  def listen
128
177
  @listen_thread = Thread.new { listen_loop }
129
178
  @listen_thread.abort_on_exception = false
@@ -270,8 +319,12 @@ module MockServer
270
319
  if msg_type == TYPE_HTTP_REQUEST
271
320
  request = HttpRequest.from_hash(JSON.parse(msg_value))
272
321
  correlation_id = MockServer.extract_correlation_id(request)
322
+ breakpoint_id = MockServer.extract_breakpoint_id(request)
273
323
 
274
- if @forward_callback
324
+ bp_handler = breakpoint_id ? @breakpoint_request_handlers[breakpoint_id] : nil
325
+ if bp_handler
326
+ handle_breakpoint_request(request, correlation_id, bp_handler)
327
+ elsif @forward_callback
275
328
  handle_forward_request(request, correlation_id)
276
329
  elsif @response_callback
277
330
  handle_response_request(request, correlation_id)
@@ -284,8 +337,12 @@ module MockServer
284
337
  if msg_type == TYPE_HTTP_REQUEST_AND_RESPONSE
285
338
  req_and_resp = HttpRequestAndHttpResponse.from_hash(JSON.parse(msg_value))
286
339
  correlation_id = MockServer.extract_correlation_id(req_and_resp.http_request)
340
+ breakpoint_id = MockServer.extract_breakpoint_id(req_and_resp.http_request)
287
341
 
288
- if @forward_response_callback
342
+ bp_handler = breakpoint_id ? @breakpoint_response_handlers[breakpoint_id] : nil
343
+ if bp_handler
344
+ handle_breakpoint_response(req_and_resp, correlation_id, bp_handler)
345
+ elsif @forward_response_callback
289
346
  handle_forward_response(req_and_resp, correlation_id)
290
347
  else
291
348
  @logger.warn("Received HttpRequestAndHttpResponse callback but no forward_response_callback registered")
@@ -293,6 +350,12 @@ module MockServer
293
350
  return
294
351
  end
295
352
 
353
+ if msg_type == TYPE_PAUSED_STREAM_FRAME_DTO
354
+ paused_frame = JSON.parse(msg_value)
355
+ handle_breakpoint_stream_frame(paused_frame)
356
+ return
357
+ end
358
+
296
359
  @logger.warn("Received unhandled WebSocket message type: #{msg_type}")
297
360
  end
298
361
 
@@ -349,5 +412,69 @@ module MockServer
349
412
  @ws.send(error_msg)
350
413
  end
351
414
  end
415
+
416
+ def handle_breakpoint_request(request, correlation_id, handler)
417
+ result = handler.call(request)
418
+ result = request if result.nil? # auto-continue
419
+
420
+ MockServer.add_correlation_id_header(result, correlation_id) if correlation_id
421
+
422
+ type_name = result.is_a?(HttpResponse) ? TYPE_HTTP_RESPONSE : TYPE_HTTP_REQUEST
423
+ msg = MockServer.build_ws_message(type_name, result.to_h)
424
+ @ws.send(msg)
425
+ rescue StandardError => exc
426
+ @logger.error("Error in breakpoint request handler, auto-continuing: #{exc.message}")
427
+ MockServer.add_correlation_id_header(request, correlation_id) if correlation_id
428
+ msg = MockServer.build_ws_message(TYPE_HTTP_REQUEST, request.to_h)
429
+ @ws.send(msg)
430
+ end
431
+
432
+ def handle_breakpoint_response(req_and_resp, correlation_id, handler)
433
+ result = handler.call(req_and_resp.http_request, req_and_resp.http_response)
434
+ result = req_and_resp.http_response if result.nil? # auto-continue
435
+
436
+ unless result.is_a?(HttpResponse)
437
+ raise CallbackError, "Breakpoint response handler must return HttpResponse, got #{result.class}"
438
+ end
439
+
440
+ MockServer.add_correlation_id_header(result, correlation_id) if correlation_id
441
+ msg = MockServer.build_ws_message(TYPE_HTTP_RESPONSE, result.to_h)
442
+ @ws.send(msg)
443
+ rescue StandardError => exc
444
+ @logger.error("Error in breakpoint response handler, auto-continuing: #{exc.message}")
445
+ resp = req_and_resp.http_response || HttpResponse.new
446
+ MockServer.add_correlation_id_header(resp, correlation_id) if correlation_id
447
+ msg = MockServer.build_ws_message(TYPE_HTTP_RESPONSE, resp.to_h)
448
+ @ws.send(msg)
449
+ end
450
+
451
+ def handle_breakpoint_stream_frame(paused_frame)
452
+ breakpoint_id = paused_frame['breakpointId']
453
+ correlation_id = paused_frame['correlationId'] || ''
454
+ handler = breakpoint_id ? @breakpoint_stream_frame_handlers[breakpoint_id] : nil
455
+
456
+ decision = if handler
457
+ begin
458
+ result = handler.call(paused_frame)
459
+ if result.nil?
460
+ { 'correlationId' => correlation_id, 'action' => 'CONTINUE' }
461
+ else
462
+ result['correlationId'] = correlation_id # ensure echoed
463
+ result
464
+ end
465
+ rescue StandardError => exc
466
+ @logger.error("Error in breakpoint stream frame handler, auto-continuing: #{exc.message}")
467
+ { 'correlationId' => correlation_id, 'action' => 'CONTINUE' }
468
+ end
469
+ else
470
+ { 'correlationId' => correlation_id, 'action' => 'CONTINUE' }
471
+ end
472
+
473
+ msg = JSON.generate({
474
+ 'type' => TYPE_STREAM_FRAME_DECISION_DTO,
475
+ 'value' => JSON.generate(decision)
476
+ })
477
+ @ws.send(msg)
478
+ end
352
479
  end
353
480
  end
@@ -6,3 +6,4 @@ require_relative 'mockserver/models'
6
6
  require_relative 'mockserver/websocket_client'
7
7
  require_relative 'mockserver/forward_chain_expectation'
8
8
  require_relative 'mockserver/client'
9
+ require_relative 'mockserver/binary_launcher'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mockserver-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0
4
+ version: 7.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Bloom
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-06 00:00:00.000000000 Z
11
+ date: 2026-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger
@@ -91,6 +91,7 @@ files:
91
91
  - Gemfile
92
92
  - README.md
93
93
  - lib/mockserver-client.rb
94
+ - lib/mockserver/binary_launcher.rb
94
95
  - lib/mockserver/client.rb
95
96
  - lib/mockserver/errors.rb
96
97
  - lib/mockserver/forward_chain_expectation.rb