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.
- checksums.yaml +4 -4
- data/README.md +104 -0
- data/lib/mockserver/binary_launcher.rb +634 -0
- data/lib/mockserver/client.rb +151 -6
- data/lib/mockserver/models.rb +62 -27
- data/lib/mockserver/version.rb +1 -1
- data/lib/mockserver/websocket_client.rb +129 -2
- data/lib/mockserver-client.rb +1 -0
- metadata +3 -2
data/lib/mockserver/client.rb
CHANGED
|
@@ -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(
|
|
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
|
data/lib/mockserver/models.rb
CHANGED
|
@@ -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,
|
|
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']
|
|
377
|
-
result['string']
|
|
378
|
-
result['json']
|
|
379
|
-
result['base64Bytes']
|
|
380
|
-
result['not']
|
|
381
|
-
result['contentType']
|
|
382
|
-
result['charset']
|
|
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:
|
|
391
|
-
string:
|
|
392
|
-
json:
|
|
393
|
-
base64_bytes:
|
|
394
|
-
not_body:
|
|
395
|
-
content_type:
|
|
396
|
-
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:
|
|
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
|
data/lib/mockserver/version.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
data/lib/mockserver-client.rb
CHANGED
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.
|
|
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-
|
|
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
|