mockserver-client 1.0.8.pre → 6.0.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 +5 -5
- data/Gemfile +2 -1
- data/README.md +79 -227
- data/lib/mockserver/client.rb +518 -0
- data/lib/mockserver/errors.rb +18 -0
- data/lib/mockserver/forward_chain_expectation.rb +117 -0
- data/lib/mockserver/models.rb +1507 -0
- data/lib/mockserver/version.rb +3 -3
- data/lib/mockserver/websocket_client.rb +353 -0
- data/lib/mockserver-client.rb +7 -16
- data/mockserver-client.gemspec +26 -27
- metadata +54 -206
- data/.gitignore +0 -21
- data/.rubocop.yml +0 -7
- data/Rakefile +0 -10
- data/bin/mockserver +0 -9
- data/lib/cli.rb +0 -146
- data/lib/mockserver/abstract_client.rb +0 -111
- data/lib/mockserver/mock_server_client.rb +0 -46
- data/lib/mockserver/model/array_of.rb +0 -85
- data/lib/mockserver/model/body.rb +0 -56
- data/lib/mockserver/model/cookie.rb +0 -36
- data/lib/mockserver/model/delay.rb +0 -34
- data/lib/mockserver/model/enum.rb +0 -47
- data/lib/mockserver/model/expectation.rb +0 -139
- data/lib/mockserver/model/forward.rb +0 -41
- data/lib/mockserver/model/header.rb +0 -43
- data/lib/mockserver/model/parameter.rb +0 -43
- data/lib/mockserver/model/request.rb +0 -81
- data/lib/mockserver/model/response.rb +0 -45
- data/lib/mockserver/model/times.rb +0 -61
- data/lib/mockserver/proxy_client.rb +0 -9
- data/lib/mockserver/utility_methods.rb +0 -59
- data/pom.xml +0 -118
- data/spec/fixtures/forward_mockserver.json +0 -7
- data/spec/fixtures/incorrect_login_response.json +0 -20
- data/spec/fixtures/post_login_request.json +0 -22
- data/spec/fixtures/register_expectation.json +0 -50
- data/spec/fixtures/retrieved_request.json +0 -22
- data/spec/fixtures/search_request.json +0 -6
- data/spec/fixtures/times_once.json +0 -6
- data/spec/integration/mock_client_integration_spec.rb +0 -82
- data/spec/mockserver/builder_spec.rb +0 -90
- data/spec/mockserver/mock_client_spec.rb +0 -80
- data/spec/mockserver/proxy_client_spec.rb +0 -38
- data/spec/spec_helper.rb +0 -61
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
|
|
8
|
+
module MockServer
|
|
9
|
+
# Synchronous MockServer client.
|
|
10
|
+
#
|
|
11
|
+
# Provides the full MockServer REST API plus a fluent builder DSL and
|
|
12
|
+
# WebSocket-based object callback support.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# client = MockServer::Client.new('localhost', 1080)
|
|
16
|
+
# client.when(
|
|
17
|
+
# HttpRequest.request(path: '/hello')
|
|
18
|
+
# ).respond(
|
|
19
|
+
# HttpResponse.response(body: 'world')
|
|
20
|
+
# )
|
|
21
|
+
# client.close
|
|
22
|
+
#
|
|
23
|
+
# @example Block form (auto-close)
|
|
24
|
+
# MockServer::Client.new('localhost', 1080) do |c|
|
|
25
|
+
# c.when(HttpRequest.request(path: '/hello'))
|
|
26
|
+
# .respond(HttpResponse.response(body: 'world'))
|
|
27
|
+
# end
|
|
28
|
+
class Client
|
|
29
|
+
HTTP_TIMEOUT = 60 # seconds, matching Python client
|
|
30
|
+
|
|
31
|
+
# @param host [String]
|
|
32
|
+
# @param port [Integer]
|
|
33
|
+
# @param context_path [String]
|
|
34
|
+
# @param secure [Boolean]
|
|
35
|
+
# @param ca_cert_path [String, nil]
|
|
36
|
+
# @param tls_verify [Boolean]
|
|
37
|
+
def initialize(host, port, context_path: '', secure: false,
|
|
38
|
+
ca_cert_path: nil, tls_verify: true)
|
|
39
|
+
@host = host
|
|
40
|
+
@port = port
|
|
41
|
+
@context_path = context_path
|
|
42
|
+
@secure = secure
|
|
43
|
+
@ca_cert_path = ca_cert_path
|
|
44
|
+
@tls_verify = tls_verify
|
|
45
|
+
@websocket_clients = []
|
|
46
|
+
@websocket_mutex = Mutex.new
|
|
47
|
+
|
|
48
|
+
scheme = secure ? 'https' : 'http'
|
|
49
|
+
ctx_path = ''
|
|
50
|
+
if context_path && !context_path.empty?
|
|
51
|
+
ctx_path = context_path.start_with?('/') ? context_path : "/#{context_path}"
|
|
52
|
+
end
|
|
53
|
+
@base_url = "#{scheme}://#{host}:#{port}#{ctx_path}"
|
|
54
|
+
|
|
55
|
+
if block_given?
|
|
56
|
+
begin
|
|
57
|
+
yield self
|
|
58
|
+
ensure
|
|
59
|
+
close
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# -------------------------------------------------------------------
|
|
65
|
+
# REST API methods
|
|
66
|
+
# -------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
# Create or update expectations.
|
|
69
|
+
# @param expectations [Array<Expectation>]
|
|
70
|
+
# @return [Array<Expectation>]
|
|
71
|
+
def upsert(*expectations)
|
|
72
|
+
body = JSON.generate(expectations.map(&:to_h))
|
|
73
|
+
status, response_body = request('PUT', '/mockserver/expectation', body)
|
|
74
|
+
if status == 400
|
|
75
|
+
raise Error, "Invalid expectation: #{response_body}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if status >= 400
|
|
79
|
+
raise Error, "Failed to upsert expectations (status=#{status}): #{response_body}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if response_body && !response_body.empty?
|
|
83
|
+
parsed = JSON.parse(response_body)
|
|
84
|
+
return parsed.map { |e| Expectation.from_hash(e) } if parsed.is_a?(Array)
|
|
85
|
+
end
|
|
86
|
+
expectations.to_a
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Create an OpenAPI expectation.
|
|
90
|
+
# @param expectation [OpenAPIExpectation]
|
|
91
|
+
# @return [nil]
|
|
92
|
+
def open_api_expectation(expectation)
|
|
93
|
+
body = JSON.generate(expectation.to_h)
|
|
94
|
+
status, response_body = request('PUT', '/mockserver/openapi', body)
|
|
95
|
+
if status >= 400
|
|
96
|
+
raise Error, "Failed to create OpenAPI expectation (status=#{status}): #{response_body}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Clear expectations and/or logs.
|
|
103
|
+
# @param request [HttpRequest, nil]
|
|
104
|
+
# @param type [String, nil] "EXPECTATIONS", "LOG", or "ALL"
|
|
105
|
+
# @return [nil]
|
|
106
|
+
def clear(request = nil, type: nil)
|
|
107
|
+
query_params = {}
|
|
108
|
+
query_params['type'] = type if type
|
|
109
|
+
body = request ? JSON.generate(request.to_h) : ''
|
|
110
|
+
status, response_body = do_request(
|
|
111
|
+
'PUT', '/mockserver/clear', body, query_params.empty? ? nil : query_params
|
|
112
|
+
)
|
|
113
|
+
if status >= 400
|
|
114
|
+
raise Error, "Failed to clear (status=#{status}): #{response_body}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Clear by expectation ID.
|
|
121
|
+
# @param expectation_id [String]
|
|
122
|
+
# @param type [String, nil]
|
|
123
|
+
# @return [nil]
|
|
124
|
+
def clear_by_id(expectation_id, type: nil)
|
|
125
|
+
query_params = {}
|
|
126
|
+
query_params['type'] = type if type
|
|
127
|
+
body = JSON.generate({ 'id' => expectation_id })
|
|
128
|
+
status, response_body = do_request(
|
|
129
|
+
'PUT', '/mockserver/clear', body, query_params.empty? ? nil : query_params
|
|
130
|
+
)
|
|
131
|
+
if status >= 400
|
|
132
|
+
raise Error, "Failed to clear by id (status=#{status}): #{response_body}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Reset all expectations and logs.
|
|
139
|
+
# @return [nil]
|
|
140
|
+
def reset
|
|
141
|
+
status, response_body = request('PUT', '/mockserver/reset')
|
|
142
|
+
if status >= 400
|
|
143
|
+
raise Error, "Failed to reset (status=#{status}): #{response_body}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
nil
|
|
147
|
+
ensure
|
|
148
|
+
close
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Verify that a request was received.
|
|
152
|
+
# @param request [HttpRequest]
|
|
153
|
+
# @param times [VerificationTimes, nil]
|
|
154
|
+
# @return [nil]
|
|
155
|
+
# @raise [VerificationError] if verification fails (HTTP 406)
|
|
156
|
+
def verify(request, times: nil)
|
|
157
|
+
verification = Verification.new(http_request: request, times: times)
|
|
158
|
+
body = JSON.generate(verification.to_h)
|
|
159
|
+
status, response_body = do_request('PUT', '/mockserver/verify', body)
|
|
160
|
+
if status == 406
|
|
161
|
+
raise VerificationError, response_body
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if status >= 400
|
|
165
|
+
raise Error, "Failed to verify (status=#{status}): #{response_body}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Verify that requests were received in sequence.
|
|
172
|
+
# @param requests [Array<HttpRequest>]
|
|
173
|
+
# @return [nil]
|
|
174
|
+
# @raise [VerificationError] if verification fails (HTTP 406)
|
|
175
|
+
def verify_sequence(*requests)
|
|
176
|
+
verification = VerificationSequence.new(http_requests: requests.to_a)
|
|
177
|
+
body = JSON.generate(verification.to_h)
|
|
178
|
+
status, response_body = request('PUT', '/mockserver/verifySequence', body)
|
|
179
|
+
if status == 406
|
|
180
|
+
raise VerificationError, response_body
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if status >= 400
|
|
184
|
+
raise Error, "Failed to verify sequence (status=#{status}): #{response_body}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Verify zero interactions.
|
|
191
|
+
# @return [nil]
|
|
192
|
+
def verify_zero_interactions
|
|
193
|
+
verify(HttpRequest.new, times: VerificationTimes.new(at_most: 0))
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Retrieve recorded requests.
|
|
197
|
+
# @param request [HttpRequest, nil]
|
|
198
|
+
# @return [Array<HttpRequest>]
|
|
199
|
+
def retrieve_recorded_requests(request: nil)
|
|
200
|
+
body = request ? JSON.generate(request.to_h) : ''
|
|
201
|
+
status, response_body = do_request(
|
|
202
|
+
'PUT', '/mockserver/retrieve', body,
|
|
203
|
+
{ 'type' => 'REQUESTS', 'format' => 'JSON' }
|
|
204
|
+
)
|
|
205
|
+
if status >= 400
|
|
206
|
+
raise Error, "Failed to retrieve recorded requests (status=#{status}): #{response_body}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
if response_body && !response_body.empty?
|
|
210
|
+
parsed = JSON.parse(response_body)
|
|
211
|
+
return parsed.map { |r| HttpRequest.from_hash(r) } if parsed.is_a?(Array)
|
|
212
|
+
end
|
|
213
|
+
[]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Retrieve active expectations.
|
|
217
|
+
# @param request [HttpRequest, nil]
|
|
218
|
+
# @return [Array<Expectation>]
|
|
219
|
+
def retrieve_active_expectations(request: nil)
|
|
220
|
+
body = request ? JSON.generate(request.to_h) : ''
|
|
221
|
+
status, response_body = do_request(
|
|
222
|
+
'PUT', '/mockserver/retrieve', body,
|
|
223
|
+
{ 'type' => 'ACTIVE_EXPECTATIONS', 'format' => 'JSON' }
|
|
224
|
+
)
|
|
225
|
+
if status >= 400
|
|
226
|
+
raise Error, "Failed to retrieve active expectations (status=#{status}): #{response_body}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if response_body && !response_body.empty?
|
|
230
|
+
parsed = JSON.parse(response_body)
|
|
231
|
+
return parsed.map { |e| Expectation.from_hash(e) } if parsed.is_a?(Array)
|
|
232
|
+
end
|
|
233
|
+
[]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Retrieve recorded expectations.
|
|
237
|
+
# @param request [HttpRequest, nil]
|
|
238
|
+
# @return [Array<Expectation>]
|
|
239
|
+
def retrieve_recorded_expectations(request: nil)
|
|
240
|
+
body = request ? JSON.generate(request.to_h) : ''
|
|
241
|
+
status, response_body = do_request(
|
|
242
|
+
'PUT', '/mockserver/retrieve', body,
|
|
243
|
+
{ 'type' => 'RECORDED_EXPECTATIONS', 'format' => 'JSON' }
|
|
244
|
+
)
|
|
245
|
+
if status >= 400
|
|
246
|
+
raise Error, "Failed to retrieve recorded expectations (status=#{status}): #{response_body}"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
if response_body && !response_body.empty?
|
|
250
|
+
parsed = JSON.parse(response_body)
|
|
251
|
+
return parsed.map { |e| Expectation.from_hash(e) } if parsed.is_a?(Array)
|
|
252
|
+
end
|
|
253
|
+
[]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Retrieve recorded requests and responses.
|
|
257
|
+
# @param request [HttpRequest, nil]
|
|
258
|
+
# @return [Array<HttpRequestAndHttpResponse>]
|
|
259
|
+
def retrieve_recorded_requests_and_responses(request: nil)
|
|
260
|
+
body = request ? JSON.generate(request.to_h) : ''
|
|
261
|
+
status, response_body = do_request(
|
|
262
|
+
'PUT', '/mockserver/retrieve', body,
|
|
263
|
+
{ 'type' => 'REQUEST_RESPONSES', 'format' => 'JSON' }
|
|
264
|
+
)
|
|
265
|
+
if status >= 400
|
|
266
|
+
raise Error, "Failed to retrieve request/responses (status=#{status}): #{response_body}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
if response_body && !response_body.empty?
|
|
270
|
+
parsed = JSON.parse(response_body)
|
|
271
|
+
return parsed.map { |rr| HttpRequestAndHttpResponse.from_hash(rr) } if parsed.is_a?(Array)
|
|
272
|
+
end
|
|
273
|
+
[]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Retrieve log messages.
|
|
277
|
+
# @param request [HttpRequest, nil]
|
|
278
|
+
# @return [Array<String>]
|
|
279
|
+
def retrieve_log_messages(request: nil)
|
|
280
|
+
body = request ? JSON.generate(request.to_h) : ''
|
|
281
|
+
status, response_body = do_request(
|
|
282
|
+
'PUT', '/mockserver/retrieve', body,
|
|
283
|
+
{ 'type' => 'LOGS' }
|
|
284
|
+
)
|
|
285
|
+
if status >= 400
|
|
286
|
+
raise Error, "Failed to retrieve log messages (status=#{status}): #{response_body}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
if response_body && !response_body.empty?
|
|
290
|
+
begin
|
|
291
|
+
parsed = JSON.parse(response_body)
|
|
292
|
+
return parsed if parsed.is_a?(Array)
|
|
293
|
+
rescue JSON::ParserError
|
|
294
|
+
return response_body.split("------------------------------------\n")
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
[]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Bind additional ports.
|
|
301
|
+
# @param ports [Array<Integer>]
|
|
302
|
+
# @return [Array<Integer>]
|
|
303
|
+
def bind(*ports)
|
|
304
|
+
body = JSON.generate(Ports.new(ports: ports.flatten).to_h)
|
|
305
|
+
status, response_body = request('PUT', '/mockserver/bind', body)
|
|
306
|
+
if status >= 400
|
|
307
|
+
raise Error, "Failed to bind ports (status=#{status}): #{response_body}"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
if response_body && !response_body.empty?
|
|
311
|
+
parsed = JSON.parse(response_body)
|
|
312
|
+
return Ports.from_hash(parsed).ports
|
|
313
|
+
end
|
|
314
|
+
[]
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Stop the MockServer instance.
|
|
318
|
+
# @return [nil]
|
|
319
|
+
def stop
|
|
320
|
+
request('PUT', '/mockserver/stop')
|
|
321
|
+
nil
|
|
322
|
+
rescue ConnectionError
|
|
323
|
+
nil
|
|
324
|
+
ensure
|
|
325
|
+
close
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Check if MockServer has started.
|
|
329
|
+
# @param attempts [Integer]
|
|
330
|
+
# @param timeout [Float] seconds between attempts
|
|
331
|
+
# @return [Boolean]
|
|
332
|
+
def has_started?(attempts: 10, timeout: 0.5)
|
|
333
|
+
attempts.times do |i|
|
|
334
|
+
begin
|
|
335
|
+
status, = request('PUT', '/mockserver/status')
|
|
336
|
+
return true if status == 200
|
|
337
|
+
rescue ConnectionError
|
|
338
|
+
# not yet started
|
|
339
|
+
end
|
|
340
|
+
sleep(timeout) if i < attempts - 1
|
|
341
|
+
end
|
|
342
|
+
false
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
alias has_started has_started?
|
|
346
|
+
|
|
347
|
+
# -------------------------------------------------------------------
|
|
348
|
+
# Fluent API
|
|
349
|
+
# -------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
# Begin building an expectation via the fluent API.
|
|
352
|
+
# @param request [HttpRequest]
|
|
353
|
+
# @param times [Times, nil]
|
|
354
|
+
# @param time_to_live [TimeToLive, nil]
|
|
355
|
+
# @param priority [Integer, nil]
|
|
356
|
+
# @return [ForwardChainExpectation]
|
|
357
|
+
def when(request, times: nil, time_to_live: nil, priority: nil)
|
|
358
|
+
expectation = Expectation.new(
|
|
359
|
+
http_request: request,
|
|
360
|
+
times: times,
|
|
361
|
+
time_to_live: time_to_live,
|
|
362
|
+
priority: priority
|
|
363
|
+
)
|
|
364
|
+
ForwardChainExpectation.new(self, expectation)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# -------------------------------------------------------------------
|
|
368
|
+
# Callback methods
|
|
369
|
+
# -------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
# Register a response callback via WebSocket.
|
|
372
|
+
# @param request [HttpRequest]
|
|
373
|
+
# @param callback [Proc]
|
|
374
|
+
# @param times [Times, nil]
|
|
375
|
+
# @param time_to_live [TimeToLive, nil]
|
|
376
|
+
# @return [Array<Expectation>]
|
|
377
|
+
def mock_with_callback(request, callback, times: nil, time_to_live: nil)
|
|
378
|
+
client_id = register_websocket_callback('response', callback)
|
|
379
|
+
expectation = Expectation.new(
|
|
380
|
+
http_request: request,
|
|
381
|
+
http_response_object_callback: HttpObjectCallback.new(client_id: client_id),
|
|
382
|
+
times: times,
|
|
383
|
+
time_to_live: time_to_live
|
|
384
|
+
)
|
|
385
|
+
upsert(expectation)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Register a forward callback via WebSocket.
|
|
389
|
+
# @param request [HttpRequest]
|
|
390
|
+
# @param forward_callback [Proc]
|
|
391
|
+
# @param response_callback [Proc, nil]
|
|
392
|
+
# @param times [Times, nil]
|
|
393
|
+
# @param time_to_live [TimeToLive, nil]
|
|
394
|
+
# @return [Array<Expectation>]
|
|
395
|
+
def mock_with_forward_callback(request, forward_callback, response_callback = nil,
|
|
396
|
+
times: nil, time_to_live: nil)
|
|
397
|
+
client_id = register_websocket_callback('forward', forward_callback, response_callback)
|
|
398
|
+
obj_callback = HttpObjectCallback.new(client_id: client_id)
|
|
399
|
+
obj_callback.response_callback = true if response_callback
|
|
400
|
+
expectation = Expectation.new(
|
|
401
|
+
http_request: request,
|
|
402
|
+
http_forward_object_callback: obj_callback,
|
|
403
|
+
times: times,
|
|
404
|
+
time_to_live: time_to_live
|
|
405
|
+
)
|
|
406
|
+
upsert(expectation)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Close all WebSocket connections.
|
|
410
|
+
# @return [nil]
|
|
411
|
+
def close
|
|
412
|
+
@websocket_mutex.synchronize do
|
|
413
|
+
@websocket_clients.each(&:close)
|
|
414
|
+
@websocket_clients.clear
|
|
415
|
+
end
|
|
416
|
+
nil
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
private
|
|
420
|
+
|
|
421
|
+
# @api private
|
|
422
|
+
def register_websocket_callback(callback_type, callback_fn, forward_response_fn = nil)
|
|
423
|
+
ws_client = WebSocketClient.new
|
|
424
|
+
ws_client.connect(
|
|
425
|
+
@host, @port,
|
|
426
|
+
context_path: @context_path,
|
|
427
|
+
secure: @secure,
|
|
428
|
+
ca_cert_path: @ca_cert_path,
|
|
429
|
+
tls_verify: @tls_verify
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
case callback_type
|
|
433
|
+
when 'response'
|
|
434
|
+
ws_client.register_response_callback(callback_fn)
|
|
435
|
+
when 'forward'
|
|
436
|
+
ws_client.register_forward_callback(callback_fn, forward_response_fn)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
ws_client.listen
|
|
440
|
+
@websocket_mutex.synchronize { @websocket_clients << ws_client }
|
|
441
|
+
ws_client.client_id
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Perform an HTTP request with optional query parameters.
|
|
445
|
+
# @api private
|
|
446
|
+
def do_request(method, path, body = nil, query_params = nil)
|
|
447
|
+
url = "#{@base_url}#{path}"
|
|
448
|
+
if query_params && !query_params.empty?
|
|
449
|
+
url = "#{url}?#{URI.encode_www_form(query_params)}"
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
uri = URI.parse(url)
|
|
453
|
+
http = build_http(uri)
|
|
454
|
+
|
|
455
|
+
req = build_request(method, uri, body)
|
|
456
|
+
execute_request(http, req)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Perform an HTTP request (no query params).
|
|
460
|
+
# @api private
|
|
461
|
+
def request(method, path, body = nil)
|
|
462
|
+
do_request(method, path, body, nil)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# @api private
|
|
466
|
+
def build_http(uri)
|
|
467
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
468
|
+
http.read_timeout = HTTP_TIMEOUT
|
|
469
|
+
http.open_timeout = HTTP_TIMEOUT
|
|
470
|
+
|
|
471
|
+
if @secure
|
|
472
|
+
http.use_ssl = true
|
|
473
|
+
if @ca_cert_path
|
|
474
|
+
http.ca_file = @ca_cert_path
|
|
475
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
476
|
+
elsif !@tls_verify
|
|
477
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
478
|
+
else
|
|
479
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
http
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# @api private
|
|
487
|
+
def build_request(method, uri, body)
|
|
488
|
+
request_path = uri.request_uri
|
|
489
|
+
case method.upcase
|
|
490
|
+
when 'PUT'
|
|
491
|
+
req = Net::HTTP::Put.new(request_path)
|
|
492
|
+
when 'GET'
|
|
493
|
+
req = Net::HTTP::Get.new(request_path)
|
|
494
|
+
when 'POST'
|
|
495
|
+
req = Net::HTTP::Post.new(request_path)
|
|
496
|
+
when 'DELETE'
|
|
497
|
+
req = Net::HTTP::Delete.new(request_path)
|
|
498
|
+
else
|
|
499
|
+
req = Net::HTTP::Put.new(request_path)
|
|
500
|
+
end
|
|
501
|
+
req['Content-Type'] = 'application/json; charset=utf-8'
|
|
502
|
+
req.body = body if body
|
|
503
|
+
req
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# @api private
|
|
507
|
+
def execute_request(http, req)
|
|
508
|
+
response = http.request(req)
|
|
509
|
+
[response.code.to_i, response.body || '']
|
|
510
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
511
|
+
raise ConnectionError, "Request to MockServer at #{@base_url} timed out: #{e.message}"
|
|
512
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
513
|
+
raise ConnectionError, "TLS error connecting to MockServer at #{@base_url}: #{e.message}"
|
|
514
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError, IOError => e
|
|
515
|
+
raise ConnectionError, "Failed to connect to MockServer at #{@base_url}: #{e.message}"
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MockServer
|
|
4
|
+
# Base error class for all MockServer client errors.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the client cannot connect to the MockServer instance.
|
|
8
|
+
class ConnectionError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when a verification request fails (HTTP 406).
|
|
11
|
+
class VerificationError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when a WebSocket callback produces an invalid result.
|
|
14
|
+
class CallbackError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised for WebSocket protocol-level errors (connection, registration, etc.).
|
|
17
|
+
class WebSocketError < Error; end
|
|
18
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MockServer
|
|
4
|
+
# Fluent API for building expectations via the +when+ method.
|
|
5
|
+
#
|
|
6
|
+
# Returned by {Client#when} to allow chaining:
|
|
7
|
+
# client.when(request).respond(response)
|
|
8
|
+
# client.when(request).forward(forward)
|
|
9
|
+
# client.when(request).error(error)
|
|
10
|
+
class ForwardChainExpectation
|
|
11
|
+
def initialize(client, expectation)
|
|
12
|
+
@client = client
|
|
13
|
+
@expectation = expectation
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Set the expectation ID.
|
|
17
|
+
# @param id [String]
|
|
18
|
+
# @return [self]
|
|
19
|
+
def with_id(id)
|
|
20
|
+
@expectation.id = id
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Set the expectation priority.
|
|
25
|
+
# @param priority [Integer]
|
|
26
|
+
# @return [self]
|
|
27
|
+
def with_priority(priority)
|
|
28
|
+
@expectation.priority = priority
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Set the response action. Accepts an HttpResponse, HttpTemplate, or
|
|
33
|
+
# a Proc/lambda callback.
|
|
34
|
+
# @param response_or_callback [HttpResponse, HttpTemplate, Proc]
|
|
35
|
+
# @return [Array<Expectation>]
|
|
36
|
+
def respond(response_or_callback)
|
|
37
|
+
if response_or_callback.respond_to?(:call)
|
|
38
|
+
client_id = @client.send(:register_websocket_callback, 'response', response_or_callback)
|
|
39
|
+
@expectation.http_response_object_callback = HttpObjectCallback.new(client_id: client_id)
|
|
40
|
+
elsif response_or_callback.is_a?(HttpResponse)
|
|
41
|
+
@expectation.http_response = response_or_callback
|
|
42
|
+
elsif response_or_callback.is_a?(HttpTemplate)
|
|
43
|
+
@expectation.http_response_template = response_or_callback
|
|
44
|
+
else
|
|
45
|
+
raise TypeError,
|
|
46
|
+
"Expected HttpResponse, HttpTemplate, or callable, got #{response_or_callback.class.name}"
|
|
47
|
+
end
|
|
48
|
+
@client.upsert(@expectation)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Set the response action with a delay.
|
|
52
|
+
# @param response [HttpResponse]
|
|
53
|
+
# @param delay [Delay]
|
|
54
|
+
# @return [Array<Expectation>]
|
|
55
|
+
def respond_with_delay(response, delay)
|
|
56
|
+
response.delay = delay
|
|
57
|
+
@expectation.http_response = response
|
|
58
|
+
@client.upsert(@expectation)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Set the forward action. Accepts an HttpForward, HttpOverrideForwardedRequest,
|
|
62
|
+
# HttpTemplate, or a Proc/lambda callback.
|
|
63
|
+
# @param forward_or_callback [HttpForward, HttpOverrideForwardedRequest, HttpTemplate, Proc]
|
|
64
|
+
# @param response_callback [Proc, nil] optional response transform callback
|
|
65
|
+
# @return [Array<Expectation>]
|
|
66
|
+
def forward(forward_or_callback, response_callback = nil)
|
|
67
|
+
if forward_or_callback.respond_to?(:call)
|
|
68
|
+
client_id = @client.send(
|
|
69
|
+
:register_websocket_callback,
|
|
70
|
+
'forward', forward_or_callback, response_callback
|
|
71
|
+
)
|
|
72
|
+
obj_callback = HttpObjectCallback.new(client_id: client_id)
|
|
73
|
+
obj_callback.response_callback = true if response_callback
|
|
74
|
+
@expectation.http_forward_object_callback = obj_callback
|
|
75
|
+
elsif forward_or_callback.is_a?(HttpForward)
|
|
76
|
+
@expectation.http_forward = forward_or_callback
|
|
77
|
+
elsif forward_or_callback.is_a?(HttpOverrideForwardedRequest)
|
|
78
|
+
@expectation.http_override_forwarded_request = forward_or_callback
|
|
79
|
+
elsif forward_or_callback.is_a?(HttpTemplate)
|
|
80
|
+
@expectation.http_forward_template = forward_or_callback
|
|
81
|
+
else
|
|
82
|
+
raise TypeError,
|
|
83
|
+
"Expected HttpForward, HttpOverrideForwardedRequest, HttpTemplate, or callable, " \
|
|
84
|
+
"got #{forward_or_callback.class.name}"
|
|
85
|
+
end
|
|
86
|
+
@client.upsert(@expectation)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Set the forward action with a delay.
|
|
90
|
+
# @param forward [HttpForward]
|
|
91
|
+
# @param delay [Delay]
|
|
92
|
+
# @return [Array<Expectation>]
|
|
93
|
+
def forward_with_delay(forward, delay)
|
|
94
|
+
forward.delay = delay
|
|
95
|
+
@expectation.http_forward = forward
|
|
96
|
+
@client.upsert(@expectation)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Set the error action.
|
|
100
|
+
# @param error [HttpError]
|
|
101
|
+
# @return [Array<Expectation>]
|
|
102
|
+
def error(error)
|
|
103
|
+
@expectation.http_error = error
|
|
104
|
+
@client.upsert(@expectation)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def respond_with_sse(sse_response)
|
|
108
|
+
@expectation.http_sse_response = sse_response
|
|
109
|
+
@client.upsert(@expectation)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def respond_with_websocket(websocket_response)
|
|
113
|
+
@expectation.http_websocket_response = websocket_response
|
|
114
|
+
@client.upsert(@expectation)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|