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.
- checksums.yaml +4 -4
- data/README.md +216 -0
- data/lib/mockserver/binary_launcher.rb +664 -0
- data/lib/mockserver/client.rb +462 -13
- data/lib/mockserver/forward_chain_expectation.rb +16 -6
- data/lib/mockserver/llm.rb +855 -0
- data/lib/mockserver/mcp.rb +453 -0
- data/lib/mockserver/models.rb +488 -37
- data/lib/mockserver/rspec.rb +56 -0
- data/lib/mockserver/version.rb +1 -1
- data/lib/mockserver/websocket_client.rb +129 -2
- data/lib/mockserver-client.rb +3 -0
- metadata +6 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'client'
|
|
4
|
+
|
|
5
|
+
module MockServer
|
|
6
|
+
# RSpec integration helpers for MockServer.
|
|
7
|
+
#
|
|
8
|
+
# Require this file from your `spec_helper.rb` to get a shared context that
|
|
9
|
+
# provides a fresh {MockServer::Client} per example and automatically resets
|
|
10
|
+
# the server between examples, so recorded requests, expectations and logs
|
|
11
|
+
# never leak from one example to the next:
|
|
12
|
+
#
|
|
13
|
+
# require 'mockserver/rspec'
|
|
14
|
+
#
|
|
15
|
+
# RSpec.describe 'my integration', :mockserver do
|
|
16
|
+
# it 'records the request' do
|
|
17
|
+
# # `mockserver` is the shared, reset client
|
|
18
|
+
# mockserver.when(MockServer::HttpRequest.request(path: '/hello'))
|
|
19
|
+
# .respond(MockServer::HttpResponse.response(body: 'world'))
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# The host and port default to `127.0.0.1:1080` and can be overridden with the
|
|
24
|
+
# `MOCKSERVER_HOST` / `MOCKSERVER_PORT` environment variables, or by defining a
|
|
25
|
+
# `mockserver_host` / `mockserver_port` `let` in your example group.
|
|
26
|
+
module RSpec
|
|
27
|
+
SHARED_CONTEXT_NAME = 'mockserver client'
|
|
28
|
+
|
|
29
|
+
if defined?(::RSpec)
|
|
30
|
+
::RSpec.shared_context SHARED_CONTEXT_NAME do
|
|
31
|
+
let(:mockserver_host) { ENV.fetch('MOCKSERVER_HOST', '127.0.0.1') }
|
|
32
|
+
let(:mockserver_port) { Integer(ENV.fetch('MOCKSERVER_PORT', '1080')) }
|
|
33
|
+
|
|
34
|
+
# A fresh client per example. Memoised so the same instance is returned
|
|
35
|
+
# within a single example, but rebuilt for the next one.
|
|
36
|
+
let(:mockserver) { MockServer::Client.new(mockserver_host, mockserver_port) }
|
|
37
|
+
|
|
38
|
+
# Start each example from a clean server and tear down again afterwards,
|
|
39
|
+
# closing any websocket connections opened by the client.
|
|
40
|
+
before do
|
|
41
|
+
mockserver.reset
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
after do
|
|
45
|
+
mockserver.reset
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Make the shared context available to any example group tagged
|
|
50
|
+
# `:mockserver` without an explicit `include_context`.
|
|
51
|
+
::RSpec.configure do |config|
|
|
52
|
+
config.include_context SHARED_CONTEXT_NAME, :mockserver
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
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
|
@@ -6,3 +6,6 @@ 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/llm'
|
|
10
|
+
require_relative 'mockserver/mcp'
|
|
11
|
+
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.
|
|
4
|
+
version: 7.2.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-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: logger
|
|
@@ -91,10 +91,14 @@ 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
|
|
98
|
+
- lib/mockserver/llm.rb
|
|
99
|
+
- lib/mockserver/mcp.rb
|
|
97
100
|
- lib/mockserver/models.rb
|
|
101
|
+
- lib/mockserver/rspec.rb
|
|
98
102
|
- lib/mockserver/version.rb
|
|
99
103
|
- lib/mockserver/websocket_client.rb
|
|
100
104
|
- mockserver-client.gemspec
|