mockserver-client 7.1.0 → 7.3.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 +173 -1
- data/lib/mockserver/a2a.rb +529 -0
- data/lib/mockserver/binary_launcher.rb +31 -1
- data/lib/mockserver/client.rb +815 -7
- 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 +681 -11
- data/lib/mockserver/rspec.rb +56 -0
- data/lib/mockserver/version.rb +1 -1
- data/lib/mockserver-client.rb +3 -0
- metadata +6 -2
data/lib/mockserver/client.rb
CHANGED
|
@@ -204,6 +204,319 @@ module MockServer
|
|
|
204
204
|
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
205
205
|
end
|
|
206
206
|
|
|
207
|
+
# -------------------------------------------------------------------
|
|
208
|
+
# Metrics
|
|
209
|
+
# -------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
# Retrieve the JSON metric counter snapshot
|
|
212
|
+
# (PUT /mockserver/retrieve?type=METRICS).
|
|
213
|
+
#
|
|
214
|
+
# Returns a Hash mapping each metric name to its long value. When metrics
|
|
215
|
+
# are disabled on the server the snapshot is an empty Hash.
|
|
216
|
+
#
|
|
217
|
+
# @return [Hash] metric name => value
|
|
218
|
+
def retrieve_metrics
|
|
219
|
+
status, response_body = do_request(
|
|
220
|
+
'PUT', '/mockserver/retrieve', '', { 'type' => 'METRICS' }
|
|
221
|
+
)
|
|
222
|
+
if status >= 400
|
|
223
|
+
raise Error, "Failed to retrieve metrics (status=#{status}): #{response_body}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Scrape the Prometheus exposition text (GET /mockserver/metrics).
|
|
230
|
+
#
|
|
231
|
+
# Returns the raw Prometheus/OpenMetrics exposition text. When metrics are
|
|
232
|
+
# disabled on the server this endpoint returns 404 and an {Error} is raised.
|
|
233
|
+
#
|
|
234
|
+
# @return [String] the Prometheus exposition text
|
|
235
|
+
def scrape_metrics
|
|
236
|
+
status, response_body = request('GET', '/mockserver/metrics')
|
|
237
|
+
if status == 404
|
|
238
|
+
raise Error, 'Failed to scrape metrics (status=404): metrics are disabled ' \
|
|
239
|
+
'(set metricsEnabled=true on the server to enable them)'
|
|
240
|
+
end
|
|
241
|
+
if status >= 400
|
|
242
|
+
raise Error, "Failed to scrape metrics (status=#{status}): #{response_body}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
response_body || ''
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# -------------------------------------------------------------------
|
|
249
|
+
# Configuration
|
|
250
|
+
# -------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
# Read the effective live configuration (GET /mockserver/configuration).
|
|
253
|
+
#
|
|
254
|
+
# @return [String] the serialized configuration JSON
|
|
255
|
+
def retrieve_configuration
|
|
256
|
+
status, response_body = request('GET', '/mockserver/configuration')
|
|
257
|
+
if status >= 400
|
|
258
|
+
raise Error, "Failed to retrieve configuration (status=#{status}): #{response_body}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
response_body || ''
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Update the live configuration (PUT /mockserver/configuration).
|
|
265
|
+
#
|
|
266
|
+
# +config_json+ is a ConfigurationDTO JSON document; only the fields present
|
|
267
|
+
# are applied (partial update). A nil value is sent as an empty body.
|
|
268
|
+
#
|
|
269
|
+
# @param config_json [String, nil] the ConfigurationDTO JSON
|
|
270
|
+
# @return [String] the serialized updated configuration JSON
|
|
271
|
+
def update_configuration(config_json)
|
|
272
|
+
body = config_json.nil? ? '' : config_json.to_s
|
|
273
|
+
status, response_body = request('PUT', '/mockserver/configuration', body)
|
|
274
|
+
if status >= 400
|
|
275
|
+
raise Error, "Failed to update configuration (status=#{status}): #{response_body}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
response_body || ''
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# -------------------------------------------------------------------
|
|
282
|
+
# Pact (import / export / verify)
|
|
283
|
+
# -------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
# Import a Pact v3 contract as expectations (PUT /mockserver/pact/import).
|
|
286
|
+
#
|
|
287
|
+
# @param json [String] the Pact v3 contract JSON (must not be blank)
|
|
288
|
+
# @return [String] the JSON array of upserted expectations
|
|
289
|
+
# @raise [ArgumentError] if +json+ is nil or blank
|
|
290
|
+
def pact_import(json)
|
|
291
|
+
if json.nil? || json.to_s.strip.empty?
|
|
292
|
+
raise ArgumentError, 'pact JSON must not be empty'
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
status, response_body = request('PUT', '/mockserver/pact/import', json.to_s)
|
|
296
|
+
if status >= 400
|
|
297
|
+
raise Error, "Failed to import pact (status=#{status}): #{response_body}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
response_body || ''
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Export the active expectations as a Pact v3 contract
|
|
304
|
+
# (PUT /mockserver/pact?consumer=&provider=).
|
|
305
|
+
#
|
|
306
|
+
# A query param is only added when its value is non-blank; otherwise the
|
|
307
|
+
# server defaults are used.
|
|
308
|
+
#
|
|
309
|
+
# @param consumer [String, nil] the Pact consumer name
|
|
310
|
+
# @param provider [String, nil] the Pact provider name
|
|
311
|
+
# @return [String] the generated Pact v3 contract JSON
|
|
312
|
+
def pact_export(consumer: nil, provider: nil)
|
|
313
|
+
query_params = {}
|
|
314
|
+
query_params['consumer'] = consumer if consumer && !consumer.to_s.empty?
|
|
315
|
+
query_params['provider'] = provider if provider && !provider.to_s.empty?
|
|
316
|
+
status, response_body = do_request(
|
|
317
|
+
'PUT', '/mockserver/pact', nil, query_params.empty? ? nil : query_params
|
|
318
|
+
)
|
|
319
|
+
if status >= 400
|
|
320
|
+
raise Error, "Failed to export pact (status=#{status}): #{response_body}"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
response_body || ''
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Verify a Pact v3 contract against the active expectations
|
|
327
|
+
# (PUT /mockserver/pact/verify).
|
|
328
|
+
#
|
|
329
|
+
# The server encodes the verdict in the HTTP status: 202 when every
|
|
330
|
+
# interaction matches (PASS), 406 when verification fails (FAIL). Both
|
|
331
|
+
# carry the verification report JSON, which is returned verbatim in both
|
|
332
|
+
# cases (a FAIL does not raise) so callers can inspect the report.
|
|
333
|
+
#
|
|
334
|
+
# @param json [String] the Pact v3 contract JSON to verify (must not be blank)
|
|
335
|
+
# @return [String] the verification report JSON
|
|
336
|
+
# @raise [ArgumentError] if +json+ is nil or blank
|
|
337
|
+
def pact_verify(json)
|
|
338
|
+
if json.nil? || json.to_s.strip.empty?
|
|
339
|
+
raise ArgumentError, 'pact JSON must not be empty'
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
status, response_body = request('PUT', '/mockserver/pact/verify', json.to_s)
|
|
343
|
+
if status == 202 || status == 406
|
|
344
|
+
return response_body || ''
|
|
345
|
+
end
|
|
346
|
+
if status >= 400
|
|
347
|
+
raise Error, "Failed to verify pact (status=#{status}): #{response_body}"
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
response_body || ''
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# -------------------------------------------------------------------
|
|
354
|
+
# File store (store / retrieve / list / delete)
|
|
355
|
+
# -------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
# Store a file in the in-memory file store (PUT /mockserver/files/store).
|
|
358
|
+
#
|
|
359
|
+
# @param name [String] the file name/key
|
|
360
|
+
# @param content [String] the UTF-8 file content
|
|
361
|
+
# @return [Hash] response of the form { "name" => ..., "size" => ... }
|
|
362
|
+
def store_file(name, content)
|
|
363
|
+
body = JSON.generate({ 'name' => name, 'content' => content })
|
|
364
|
+
status, response_body = request('PUT', '/mockserver/files/store', body)
|
|
365
|
+
if status >= 400
|
|
366
|
+
raise Error, "Failed to store file (status=#{status}): #{response_body}"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Retrieve a file's raw content (PUT /mockserver/files/retrieve).
|
|
373
|
+
#
|
|
374
|
+
# Returns the raw 200 body verbatim. An unknown file yields a 404 which
|
|
375
|
+
# raises an {Error} (there is no null-on-404 fallback).
|
|
376
|
+
#
|
|
377
|
+
# @param name [String] the file name/key
|
|
378
|
+
# @return [String] the raw file content
|
|
379
|
+
def retrieve_file(name)
|
|
380
|
+
body = JSON.generate({ 'name' => name })
|
|
381
|
+
status, response_body = request('PUT', '/mockserver/files/retrieve', body)
|
|
382
|
+
if status == 404
|
|
383
|
+
raise Error, "File not found (status=404): #{name}"
|
|
384
|
+
end
|
|
385
|
+
if status >= 400
|
|
386
|
+
raise Error, "Failed to retrieve file (status=#{status}): #{response_body}"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
response_body || ''
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# List the names of all stored files (PUT /mockserver/files/list).
|
|
393
|
+
#
|
|
394
|
+
# @return [Array<String>] the file names
|
|
395
|
+
def list_files
|
|
396
|
+
status, response_body = request('PUT', '/mockserver/files/list')
|
|
397
|
+
if status >= 400
|
|
398
|
+
raise Error, "Failed to list files (status=#{status}): #{response_body}"
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
if response_body && !response_body.empty?
|
|
402
|
+
parsed = JSON.parse(response_body)
|
|
403
|
+
return parsed if parsed.is_a?(Array)
|
|
404
|
+
end
|
|
405
|
+
[]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Delete a stored file (PUT /mockserver/files/delete).
|
|
409
|
+
#
|
|
410
|
+
# An unknown file yields a 404 which raises an {Error}.
|
|
411
|
+
#
|
|
412
|
+
# @param name [String] the file name/key
|
|
413
|
+
# @return [nil]
|
|
414
|
+
def delete_file(name)
|
|
415
|
+
body = JSON.generate({ 'name' => name })
|
|
416
|
+
status, response_body = request('PUT', '/mockserver/files/delete', body)
|
|
417
|
+
if status == 404
|
|
418
|
+
raise Error, "File not found (status=404): #{name}"
|
|
419
|
+
end
|
|
420
|
+
if status >= 400
|
|
421
|
+
raise Error, "Failed to delete file (status=#{status}): #{response_body}"
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
nil
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# -------------------------------------------------------------------
|
|
428
|
+
# Import (HAR / Postman)
|
|
429
|
+
# -------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
# Import a HAR document as expectations (PUT /mockserver/import?format=har).
|
|
432
|
+
#
|
|
433
|
+
# @param har_json [String] the HAR JSON document (must not be blank)
|
|
434
|
+
# @return [Array<Expectation>] the upserted expectations
|
|
435
|
+
# @raise [ArgumentError] if +har_json+ is nil or blank
|
|
436
|
+
def import_har(har_json)
|
|
437
|
+
import_document(har_json, 'har')
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Import a Postman collection as expectations
|
|
441
|
+
# (PUT /mockserver/import?format=postman).
|
|
442
|
+
#
|
|
443
|
+
# @param collection_json [String] the Postman collection JSON (must not be blank)
|
|
444
|
+
# @return [Array<Expectation>] the upserted expectations
|
|
445
|
+
# @raise [ArgumentError] if +collection_json+ is nil or blank
|
|
446
|
+
def import_postman_collection(collection_json)
|
|
447
|
+
import_document(collection_json, 'postman')
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# -------------------------------------------------------------------
|
|
451
|
+
# Operating mode (SIMULATE / SPY / CAPTURE)
|
|
452
|
+
# -------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
# Valid operating modes (parallels +org.mockserver.mock.MockMode+).
|
|
455
|
+
MODE_SIMULATE = 'SIMULATE'
|
|
456
|
+
MODE_SPY = 'SPY'
|
|
457
|
+
MODE_CAPTURE = 'CAPTURE'
|
|
458
|
+
|
|
459
|
+
# Set the high-level operating mode (PUT /mockserver/mode?mode=<MODE>).
|
|
460
|
+
#
|
|
461
|
+
# +mode+ may be a Symbol or String (case-insensitive), one of
|
|
462
|
+
# +:simulate+/+:spy+/+:capture+ (or the {MODE_SIMULATE}, {MODE_SPY},
|
|
463
|
+
# {MODE_CAPTURE} constants). Setting the mode also flips the
|
|
464
|
+
# proxy-on-no-match behaviour on the server.
|
|
465
|
+
#
|
|
466
|
+
# @param mode [String, Symbol] the operating mode
|
|
467
|
+
# @return [Hash] response of the form
|
|
468
|
+
# { "mode" => ..., "proxyUnmatchedRequests" => ... }
|
|
469
|
+
def set_mode(mode)
|
|
470
|
+
mode_value = mode.to_s.upcase
|
|
471
|
+
status, response_body = do_request(
|
|
472
|
+
'PUT', '/mockserver/mode', nil, { 'mode' => mode_value }
|
|
473
|
+
)
|
|
474
|
+
if status >= 400
|
|
475
|
+
raise Error, "Failed to set mode (status=#{status}): #{response_body}"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Read the current operating mode (GET /mockserver/mode).
|
|
482
|
+
#
|
|
483
|
+
# @return [Hash] response of the form
|
|
484
|
+
# { "mode" => ..., "proxyUnmatchedRequests" => ... }
|
|
485
|
+
def retrieve_mode
|
|
486
|
+
status, response_body = request('GET', '/mockserver/mode')
|
|
487
|
+
if status >= 400
|
|
488
|
+
raise Error, "Failed to retrieve mode (status=#{status}): #{response_body}"
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# -------------------------------------------------------------------
|
|
495
|
+
# WSDL -> expectations
|
|
496
|
+
# -------------------------------------------------------------------
|
|
497
|
+
|
|
498
|
+
# Generate expectations from a WSDL document (PUT /mockserver/wsdl).
|
|
499
|
+
#
|
|
500
|
+
# The WSDL XML is sent as the raw request body with an XML content type.
|
|
501
|
+
#
|
|
502
|
+
# @param wsdl [String] the WSDL document (XML, must not be blank)
|
|
503
|
+
# @return [Array<Expectation>] the generated (upserted) expectations
|
|
504
|
+
# @raise [ArgumentError] if +wsdl+ is nil or blank
|
|
505
|
+
def wsdl_expectation(wsdl)
|
|
506
|
+
if wsdl.nil? || wsdl.to_s.strip.empty?
|
|
507
|
+
raise ArgumentError, 'WSDL must not be empty'
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
status, response_body = request(
|
|
511
|
+
'PUT', '/mockserver/wsdl', wsdl.to_s, content_type: 'application/xml'
|
|
512
|
+
)
|
|
513
|
+
if status >= 400
|
|
514
|
+
raise Error, "Failed to generate WSDL expectations (status=#{status}): #{response_body}"
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
expectations_from_response(response_body)
|
|
518
|
+
end
|
|
519
|
+
|
|
207
520
|
# Register a service-scoped HTTP chaos profile for an upstream host. The profile
|
|
208
521
|
# is applied to every matched forward expectation to that host that does not
|
|
209
522
|
# define its own chaos (an expectation's own chaos always wins). The host is
|
|
@@ -260,6 +573,419 @@ module MockServer
|
|
|
260
573
|
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
261
574
|
end
|
|
262
575
|
|
|
576
|
+
# -------------------------------------------------------------------
|
|
577
|
+
# Load scenario registry (load injection)
|
|
578
|
+
# -------------------------------------------------------------------
|
|
579
|
+
#
|
|
580
|
+
# The load scenario registry decouples *registering* a scenario from
|
|
581
|
+
# *running* it:
|
|
582
|
+
#
|
|
583
|
+
# * registering (load_scenario) only stores the definition keyed by its
|
|
584
|
+
# unique +name+ and is allowed even when +loadGenerationEnabled+ is off;
|
|
585
|
+
# * starting (start_load_scenarios) is what actually drives traffic and
|
|
586
|
+
# requires +loadGenerationEnabled+ on the server (otherwise a 403).
|
|
587
|
+
#
|
|
588
|
+
# Scenario states are: LOADED, PENDING, RUNNING, COMPLETED, STOPPED.
|
|
589
|
+
|
|
590
|
+
# Register (load) a scenario into the registry without running it.
|
|
591
|
+
#
|
|
592
|
+
# +scenario+ may be a {LoadScenario} model (which responds to +to_h+) or a
|
|
593
|
+
# plain Hash already shaped to the +LoadScenario+ JSON contract. It must
|
|
594
|
+
# carry a unique +name+. Registering is permitted even when load generation
|
|
595
|
+
# is disabled on the server.
|
|
596
|
+
#
|
|
597
|
+
# @param scenario [LoadScenario, Hash] the scenario to register
|
|
598
|
+
# @return [Hash] parsed response of the form { "name" => ..., "state" => ... }
|
|
599
|
+
def load_scenario(scenario)
|
|
600
|
+
payload = scenario.respond_to?(:to_h) ? scenario.to_h : scenario
|
|
601
|
+
body = JSON.generate(payload)
|
|
602
|
+
status, response_body = request('PUT', '/mockserver/loadScenario', body)
|
|
603
|
+
if status >= 400
|
|
604
|
+
raise Error, "Failed to register load scenario (status=#{status}): #{response_body}"
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# List all registered load scenarios.
|
|
611
|
+
#
|
|
612
|
+
# @return [Hash] parsed response of the form
|
|
613
|
+
# { "scenarios" => [ { "name" => ..., "state" => ..., "definition" => ..., "status" => ... }, ... ] }
|
|
614
|
+
def load_scenarios
|
|
615
|
+
status, response_body = request('GET', '/mockserver/loadScenario')
|
|
616
|
+
if status >= 400
|
|
617
|
+
raise Error, "Failed to list load scenarios (status=#{status}): #{response_body}"
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# Fetch a single registered load scenario by name.
|
|
624
|
+
#
|
|
625
|
+
# @param name [String] the unique scenario name
|
|
626
|
+
# @return [Hash] parsed scenario entry { "name" => ..., "state" => ..., "definition" => ..., "status" => ... }
|
|
627
|
+
# @raise [Error] if the scenario does not exist (404) or another failure occurs
|
|
628
|
+
def get_load_scenario(name)
|
|
629
|
+
status, response_body = request('GET', "/mockserver/loadScenario/#{encode_path_segment(name)}")
|
|
630
|
+
if status == 404
|
|
631
|
+
raise Error, "Load scenario not found (status=404): #{name}"
|
|
632
|
+
end
|
|
633
|
+
if status >= 400
|
|
634
|
+
raise Error, "Failed to get load scenario (status=#{status}): #{response_body}"
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# Remove a single registered load scenario by name.
|
|
641
|
+
#
|
|
642
|
+
# @param name [String] the unique scenario name
|
|
643
|
+
# @return [Hash] parsed response (may be empty)
|
|
644
|
+
def delete_load_scenario(name)
|
|
645
|
+
status, response_body = request('DELETE', "/mockserver/loadScenario/#{encode_path_segment(name)}")
|
|
646
|
+
if status >= 400
|
|
647
|
+
raise Error, "Failed to delete load scenario (status=#{status}): #{response_body}"
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# Clear all registered load scenarios.
|
|
654
|
+
#
|
|
655
|
+
# @return [Hash] parsed response (may be empty)
|
|
656
|
+
def clear_load_scenarios
|
|
657
|
+
status, response_body = request('DELETE', '/mockserver/loadScenario')
|
|
658
|
+
if status >= 400
|
|
659
|
+
raise Error, "Failed to clear load scenarios (status=#{status}): #{response_body}"
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# Start one or more registered scenarios.
|
|
666
|
+
#
|
|
667
|
+
# +names+ may be a single scenario name (String) or an Array of names; it is
|
|
668
|
+
# always sent as { "names" => [...] }. Honours each scenario's
|
|
669
|
+
# +startDelayMillis+. Requires +loadGenerationEnabled+ on the server; a 403
|
|
670
|
+
# response raises a clear error explaining the feature is disabled.
|
|
671
|
+
#
|
|
672
|
+
# @param names [String, Array<String>] scenario name(s) to start
|
|
673
|
+
# @return [Hash] parsed response of the form
|
|
674
|
+
# { "started" => [ { "name" => ..., "state" => ... }, ... ], "status" => ... }
|
|
675
|
+
def start_load_scenarios(names)
|
|
676
|
+
payload = { 'names' => Array(names) }
|
|
677
|
+
body = JSON.generate(payload)
|
|
678
|
+
status, response_body = request('PUT', '/mockserver/loadScenario/start', body)
|
|
679
|
+
if status == 403
|
|
680
|
+
raise Error, 'Load scenario start rejected (status=403): load generation is disabled ' \
|
|
681
|
+
'(set loadGenerationEnabled=true on the server to enable it)'
|
|
682
|
+
end
|
|
683
|
+
if status == 404
|
|
684
|
+
raise Error, "Load scenario not found (status=404): #{response_body}"
|
|
685
|
+
end
|
|
686
|
+
if status >= 400
|
|
687
|
+
raise Error, "Failed to start load scenarios (status=#{status}): #{response_body}"
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# Stop running scenarios.
|
|
694
|
+
#
|
|
695
|
+
# +names+ may be:
|
|
696
|
+
# * a single scenario name (String) -> { "names" => ["a"] }
|
|
697
|
+
# * an Array of names -> { "names" => ["a", "b"] }
|
|
698
|
+
# * nil (the default) -> empty body, which stops all running scenarios
|
|
699
|
+
#
|
|
700
|
+
# @param names [String, Array<String>, nil] scenario name(s) to stop, or nil for all
|
|
701
|
+
# @return [Hash] parsed response of the form
|
|
702
|
+
# { "stopped" => [ ... ], "status" => ... }
|
|
703
|
+
def stop_load_scenarios(names = nil)
|
|
704
|
+
body = names.nil? ? nil : JSON.generate({ 'names' => Array(names) })
|
|
705
|
+
status, response_body = request('PUT', '/mockserver/loadScenario/stop', body)
|
|
706
|
+
if status >= 400
|
|
707
|
+
raise Error, "Failed to stop load scenarios (status=#{status}): #{response_body}"
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
# Convenience: register a scenario then immediately start it.
|
|
714
|
+
#
|
|
715
|
+
# Equivalent to calling {#load_scenario} followed by {#start_load_scenarios}
|
|
716
|
+
# for the scenario's name. Requires +loadGenerationEnabled+ on the server for
|
|
717
|
+
# the start step.
|
|
718
|
+
#
|
|
719
|
+
# @param scenario [LoadScenario, Hash] the scenario to register and run
|
|
720
|
+
# @return [Hash] parsed response from the start call
|
|
721
|
+
def run_load_scenario(scenario)
|
|
722
|
+
payload = scenario.respond_to?(:to_h) ? scenario.to_h : scenario
|
|
723
|
+
name = payload.respond_to?(:[]) ? (payload['name'] || payload[:name]) : nil
|
|
724
|
+
if name.nil? || name.to_s.empty?
|
|
725
|
+
raise ArgumentError, 'scenario must carry a non-empty name to run'
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
load_scenario(payload)
|
|
729
|
+
start_load_scenarios(name)
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# Fetch the end-of-run summary report for a load scenario run.
|
|
733
|
+
#
|
|
734
|
+
# Returns the report derived from the run's status snapshot (live while
|
|
735
|
+
# running, or the retained terminal snapshot once finished). With no +format+
|
|
736
|
+
# (or any value other than +"junit"+) the JSON report is parsed and returned
|
|
737
|
+
# as a Hash carrying counts, latency percentiles, the threshold verdict and
|
|
738
|
+
# per-threshold results. With +format: "junit"+ the raw JUnit-XML
|
|
739
|
+
# +<testsuite>+ document is returned as a String so a load run becomes a
|
|
740
|
+
# first-class CI test artifact.
|
|
741
|
+
#
|
|
742
|
+
# @param name [String] the unique scenario name
|
|
743
|
+
# @param format [String, nil] +"junit"+ for the JUnit-XML report, otherwise JSON
|
|
744
|
+
# @return [Hash, String] parsed JSON report (default) or the raw JUnit-XML String
|
|
745
|
+
# @raise [Error] if the scenario never ran (404) or another failure occurs
|
|
746
|
+
def get_load_scenario_report(name, format = nil)
|
|
747
|
+
query_params = {}
|
|
748
|
+
query_params['format'] = format if format
|
|
749
|
+
status, response_body = do_request(
|
|
750
|
+
'GET', "/mockserver/loadScenario/#{encode_path_segment(name)}/report", nil,
|
|
751
|
+
query_params.empty? ? nil : query_params
|
|
752
|
+
)
|
|
753
|
+
if status == 404
|
|
754
|
+
raise Error, "Load scenario report not found (status=404): #{name}"
|
|
755
|
+
end
|
|
756
|
+
if status >= 400
|
|
757
|
+
raise Error, "Failed to get load scenario report (status=#{status}): #{response_body}"
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
return response_body if format == 'junit'
|
|
761
|
+
|
|
762
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
# Generate (and register) a load scenario from an OpenAPI specification.
|
|
766
|
+
#
|
|
767
|
+
# Produces an editable scenario - one step per OpenAPI operation - and loads
|
|
768
|
+
# it into the registry under +name+ in the LOADED state; it generates no
|
|
769
|
+
# traffic and is allowed even when load generation is disabled.
|
|
770
|
+
#
|
|
771
|
+
# @param name [String] the generated scenario name (the unique registry key)
|
|
772
|
+
# @param spec_url_or_payload [String] OpenAPI spec as inline JSON/YAML, a URL,
|
|
773
|
+
# or a file/classpath reference
|
|
774
|
+
# @param target [Hash, nil] explicit network target for every generated step
|
|
775
|
+
# (e.g. { "host" => ..., "port" => ..., "scheme" => "http" })
|
|
776
|
+
# @param profile [LoadProfile, Hash, nil] optional traffic profile (a
|
|
777
|
+
# conservative default is applied when omitted)
|
|
778
|
+
# @return [Hash] parsed response of the form
|
|
779
|
+
# { "status" => "loaded", "name" => ..., "state" => ..., "scenario" => {...} }
|
|
780
|
+
def generate_load_scenario_from_openapi(name, spec_url_or_payload, target: nil, profile: nil)
|
|
781
|
+
payload = { 'name' => name, 'specUrlOrPayload' => spec_url_or_payload }
|
|
782
|
+
payload['target'] = target if target
|
|
783
|
+
payload['profile'] = profile.respond_to?(:to_h) ? profile.to_h : profile if profile
|
|
784
|
+
body = JSON.generate(payload)
|
|
785
|
+
status, response_body = request('PUT', '/mockserver/loadScenario/generateFromOpenAPI', body)
|
|
786
|
+
if status >= 400
|
|
787
|
+
raise Error, "Failed to generate load scenario from OpenAPI (status=#{status}): #{response_body}"
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# Generate (and register) a load scenario from recorded proxy traffic.
|
|
794
|
+
#
|
|
795
|
+
# Converts requests previously recorded by MockServer in proxy/recording mode
|
|
796
|
+
# into an editable scenario and loads it into the registry under +name+ in the
|
|
797
|
+
# LOADED state; it generates no traffic and is allowed even when load
|
|
798
|
+
# generation is disabled.
|
|
799
|
+
#
|
|
800
|
+
# @param name [String] the generated scenario name (the unique registry key)
|
|
801
|
+
# @param mode [String, nil] +VERBATIM+ (default) or +TEMPLATIZED+
|
|
802
|
+
# @param request_filter [HttpRequest, Hash, nil] optional matcher selecting
|
|
803
|
+
# which recorded requests to include (absent means all)
|
|
804
|
+
# @param target [Hash, nil] explicit network target applied to every step
|
|
805
|
+
# @param max_steps [Integer, nil] optional cap on the number of VERBATIM steps
|
|
806
|
+
# @return [Hash] parsed response of the form
|
|
807
|
+
# { "status" => "loaded", "name" => ..., "state" => ..., "scenario" => {...} }
|
|
808
|
+
def generate_load_scenario_from_recording(name, mode: nil, request_filter: nil,
|
|
809
|
+
target: nil, max_steps: nil)
|
|
810
|
+
payload = { 'name' => name }
|
|
811
|
+
payload['mode'] = mode if mode
|
|
812
|
+
payload['requestFilter'] = request_filter.respond_to?(:to_h) ? request_filter.to_h : request_filter if request_filter
|
|
813
|
+
payload['target'] = target if target
|
|
814
|
+
payload['maxSteps'] = max_steps if max_steps
|
|
815
|
+
body = JSON.generate(payload)
|
|
816
|
+
status, response_body = request('PUT', '/mockserver/loadScenario/generateFromRecording', body)
|
|
817
|
+
if status >= 400
|
|
818
|
+
raise Error, "Failed to generate load scenario from recording (status=#{status}): #{response_body}"
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# -------------------------------------------------------------------
|
|
825
|
+
# SRE control plane: SLO verification + chaos experiments
|
|
826
|
+
# -------------------------------------------------------------------
|
|
827
|
+
|
|
828
|
+
# Evaluate a set of service-level objectives (SLOs) over a window
|
|
829
|
+
# (PUT /mockserver/verifySLO).
|
|
830
|
+
#
|
|
831
|
+
# The server encodes the verdict in the HTTP status: 200 for PASS or
|
|
832
|
+
# INCONCLUSIVE, 406 for FAIL, 400 for malformed criteria or when SLO tracking
|
|
833
|
+
# is disabled (+sloTrackingEnabled=false+). The decoded verdict Hash carries
|
|
834
|
+
# the overall +result+ and the per-objective +objectiveResults+ so callers can
|
|
835
|
+
# inspect why an SLO failed.
|
|
836
|
+
#
|
|
837
|
+
# +criteria+ may be any Hash already shaped to the +SloCriteria+ JSON contract
|
|
838
|
+
# (+name+, +window+, +minimumSampleCount+, +upstreamHosts+,
|
|
839
|
+
# +objectives+[{+sli+, +comparator+, +threshold+, +scope+}]) or an object that
|
|
840
|
+
# responds to +to_h+.
|
|
841
|
+
#
|
|
842
|
+
# @param criteria [Hash, #to_h] the SLO criteria
|
|
843
|
+
# @return [Hash] the SLO verdict (result PASS or INCONCLUSIVE)
|
|
844
|
+
# @raise [VerificationError] if the verdict is FAIL (HTTP 406)
|
|
845
|
+
# @raise [Error] if criteria are malformed or SLO tracking is disabled (HTTP 400),
|
|
846
|
+
# or on any other failure
|
|
847
|
+
def verify_slo(criteria)
|
|
848
|
+
payload = criteria.respond_to?(:to_h) ? criteria.to_h : criteria
|
|
849
|
+
body = JSON.generate(payload)
|
|
850
|
+
status, response_body = request('PUT', '/mockserver/verifySLO', body)
|
|
851
|
+
if status == 406
|
|
852
|
+
raise VerificationError, (response_body && !response_body.empty? ? response_body : 'SLO verdict: FAIL')
|
|
853
|
+
end
|
|
854
|
+
if status == 400
|
|
855
|
+
raise Error, 'Invalid SLO criteria (or SLO tracking disabled — set ' \
|
|
856
|
+
"sloTrackingEnabled=true on the server): #{response_body}"
|
|
857
|
+
end
|
|
858
|
+
if status >= 400
|
|
859
|
+
raise Error, "Failed to verify SLO (status=#{status}): #{response_body}"
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
# Start a scheduled multi-stage chaos experiment
|
|
866
|
+
# (PUT /mockserver/chaosExperiment).
|
|
867
|
+
#
|
|
868
|
+
# The experiment is an ordered sequence of stages, each applying
|
|
869
|
+
# service-scoped chaos profiles to one or more hosts for a duration; stages
|
|
870
|
+
# progress automatically. Only one experiment may be active at a time;
|
|
871
|
+
# starting a new one stops the previous one.
|
|
872
|
+
#
|
|
873
|
+
# +experiment+ may be any Hash already shaped to the +ChaosExperiment+ JSON
|
|
874
|
+
# contract (+name+, +loop+, +stages+[{+durationMillis+, +profiles+{host:
|
|
875
|
+
# profile}}]) or an object that responds to +to_h+.
|
|
876
|
+
#
|
|
877
|
+
# @param experiment [Hash, #to_h] the experiment definition
|
|
878
|
+
# @return [Hash] the started status, e.g. { "status" => "started", "name" => ... }
|
|
879
|
+
# @raise [Error] if the experiment definition is invalid or chaos is disabled
|
|
880
|
+
# (HTTP 400/403), or on any other failure
|
|
881
|
+
def start_chaos_experiment(experiment)
|
|
882
|
+
payload = experiment.respond_to?(:to_h) ? experiment.to_h : experiment
|
|
883
|
+
body = JSON.generate(payload)
|
|
884
|
+
status, response_body = request('PUT', '/mockserver/chaosExperiment', body)
|
|
885
|
+
if status == 403
|
|
886
|
+
raise Error, 'Chaos experiment rejected (status=403): chaos is disabled on the server'
|
|
887
|
+
end
|
|
888
|
+
if status >= 400
|
|
889
|
+
raise Error, "Failed to start chaos experiment (status=#{status}): #{response_body}"
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
# -------------------------------------------------------------------
|
|
896
|
+
# Stateful scenarios (state machine control plane)
|
|
897
|
+
# -------------------------------------------------------------------
|
|
898
|
+
|
|
899
|
+
# Return a handle to the named stateful scenario, wrapping the
|
|
900
|
+
# +/mockserver/scenario/{name}+ control-plane endpoints.
|
|
901
|
+
#
|
|
902
|
+
# @param name [String] the scenario (state-machine) name
|
|
903
|
+
# @return [ScenarioHandle]
|
|
904
|
+
def scenario(name)
|
|
905
|
+
ScenarioHandle.new(self, name)
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
# List every known scenario and its current state.
|
|
909
|
+
#
|
|
910
|
+
# @return [Array<ScenarioState>] each with +scenario_name+ and +current_state+
|
|
911
|
+
def scenarios
|
|
912
|
+
result = scenario_request('GET', '/mockserver/scenario')
|
|
913
|
+
list = result.is_a?(Hash) ? (result['scenarios'] || []) : []
|
|
914
|
+
list.map { |s| ScenarioState.from_hash(s) }
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
# @api private
|
|
918
|
+
# Issue a control-plane scenario request, parsing the JSON response and
|
|
919
|
+
# raising {Error} on any >= 400 status. Reuses the same transport
|
|
920
|
+
# (+request+) as the other +/mockserver/...+ control endpoints.
|
|
921
|
+
def scenario_request(method, path, body = nil)
|
|
922
|
+
status, response_body = request(method, path, body)
|
|
923
|
+
if status >= 400
|
|
924
|
+
raise Error, "Scenario request failed (status=#{status}): #{response_body}"
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
response_body && !response_body.empty? ? JSON.parse(response_body) : {}
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
# -------------------------------------------------------------------
|
|
931
|
+
# gRPC descriptor management
|
|
932
|
+
# -------------------------------------------------------------------
|
|
933
|
+
|
|
934
|
+
# Upload a compiled protobuf descriptor set so gRPC requests can be matched.
|
|
935
|
+
#
|
|
936
|
+
# +descriptor_bytes+ must be the raw bytes of a +FileDescriptorSet+ (e.g. the
|
|
937
|
+
# output of +protoc --descriptor_set_out=... --include_imports+). The bytes are
|
|
938
|
+
# sent verbatim as +application/octet-stream+ (NOT base64-encoded).
|
|
939
|
+
#
|
|
940
|
+
# @param descriptor_bytes [String] raw descriptor set bytes (binary string)
|
|
941
|
+
# @return [nil]
|
|
942
|
+
def upload_grpc_descriptor(descriptor_bytes)
|
|
943
|
+
if descriptor_bytes.nil? || descriptor_bytes.empty?
|
|
944
|
+
raise ArgumentError, 'descriptor bytes must not be empty'
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
status, response_body = request(
|
|
948
|
+
'PUT', '/mockserver/grpc/descriptors', descriptor_bytes,
|
|
949
|
+
content_type: 'application/octet-stream'
|
|
950
|
+
)
|
|
951
|
+
if status >= 400
|
|
952
|
+
raise Error, "Failed to upload gRPC descriptor (status=#{status}): #{response_body}"
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
nil
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
# Retrieve the gRPC services registered from uploaded descriptor sets.
|
|
959
|
+
#
|
|
960
|
+
# Returns an array of service hashes, each with a +"name"+ and a list of
|
|
961
|
+
# +"methods"+ (+"name"+, +"inputType"+, +"outputType"+, +"clientStreaming"+,
|
|
962
|
+
# +"serverStreaming"+).
|
|
963
|
+
#
|
|
964
|
+
# @return [Array<Hash>]
|
|
965
|
+
def retrieve_grpc_services
|
|
966
|
+
status, response_body = request('PUT', '/mockserver/grpc/services')
|
|
967
|
+
if status >= 400
|
|
968
|
+
raise Error, "Failed to retrieve gRPC services (status=#{status}): #{response_body}"
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
if response_body && !response_body.empty?
|
|
972
|
+
parsed = JSON.parse(response_body)
|
|
973
|
+
return parsed if parsed.is_a?(Array)
|
|
974
|
+
end
|
|
975
|
+
[]
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# Clear all uploaded gRPC descriptor sets and registered services.
|
|
979
|
+
# @return [nil]
|
|
980
|
+
def clear_grpc_descriptors
|
|
981
|
+
status, response_body = request('PUT', '/mockserver/grpc/clear')
|
|
982
|
+
if status >= 400
|
|
983
|
+
raise Error, "Failed to clear gRPC descriptors (status=#{status}): #{response_body}"
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
nil
|
|
987
|
+
end
|
|
988
|
+
|
|
263
989
|
# Verify that a request (and optionally a response) was received.
|
|
264
990
|
# @param request [HttpRequest, nil]
|
|
265
991
|
# @param times [VerificationTimes, nil]
|
|
@@ -370,6 +1096,44 @@ module MockServer
|
|
|
370
1096
|
[]
|
|
371
1097
|
end
|
|
372
1098
|
|
|
1099
|
+
# Retrieve the active expectations as MockServer SDK setup code (the builder
|
|
1100
|
+
# code that recreates the expectations) in the requested language.
|
|
1101
|
+
# @param format [String] one of "java", "javascript", "python", "go",
|
|
1102
|
+
# "csharp", "ruby", "rust" or "php" (case-insensitive)
|
|
1103
|
+
# @param request [HttpRequest, nil]
|
|
1104
|
+
# @return [String] the generated code
|
|
1105
|
+
def retrieve_expectations_as_code(format: 'java', request: nil)
|
|
1106
|
+
body = request ? JSON.generate(request.to_h) : ''
|
|
1107
|
+
status, response_body = do_request(
|
|
1108
|
+
'PUT', '/mockserver/retrieve', body,
|
|
1109
|
+
{ 'type' => 'ACTIVE_EXPECTATIONS', 'format' => format.to_s.upcase }
|
|
1110
|
+
)
|
|
1111
|
+
if status >= 400
|
|
1112
|
+
raise Error, "Failed to retrieve expectations as code (status=#{status}): #{response_body}"
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
response_body || ''
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
# Retrieve the recorded (proxied) request/response pairs as MockServer SDK
|
|
1119
|
+
# setup code in the requested language.
|
|
1120
|
+
# @param format [String] one of "java", "javascript", "python", "go",
|
|
1121
|
+
# "csharp", "ruby", "rust" or "php" (case-insensitive)
|
|
1122
|
+
# @param request [HttpRequest, nil]
|
|
1123
|
+
# @return [String] the generated code
|
|
1124
|
+
def retrieve_recorded_expectations_as_code(format: 'java', request: nil)
|
|
1125
|
+
body = request ? JSON.generate(request.to_h) : ''
|
|
1126
|
+
status, response_body = do_request(
|
|
1127
|
+
'PUT', '/mockserver/retrieve', body,
|
|
1128
|
+
{ 'type' => 'RECORDED_EXPECTATIONS', 'format' => format.to_s.upcase }
|
|
1129
|
+
)
|
|
1130
|
+
if status >= 400
|
|
1131
|
+
raise Error, "Failed to retrieve recorded expectations as code (status=#{status}): #{response_body}"
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
response_body || ''
|
|
1135
|
+
end
|
|
1136
|
+
|
|
373
1137
|
# Retrieve recorded requests and responses.
|
|
374
1138
|
# @param request [HttpRequest, nil]
|
|
375
1139
|
# @return [Array<HttpRequestAndHttpResponse>]
|
|
@@ -700,7 +1464,8 @@ module MockServer
|
|
|
700
1464
|
|
|
701
1465
|
# Perform an HTTP request with optional query parameters.
|
|
702
1466
|
# @api private
|
|
703
|
-
def do_request(method, path, body = nil, query_params = nil
|
|
1467
|
+
def do_request(method, path, body = nil, query_params = nil,
|
|
1468
|
+
content_type: 'application/json; charset=utf-8')
|
|
704
1469
|
url = "#{@base_url}#{path}"
|
|
705
1470
|
if query_params && !query_params.empty?
|
|
706
1471
|
url = "#{url}?#{URI.encode_www_form(query_params)}"
|
|
@@ -709,14 +1474,52 @@ module MockServer
|
|
|
709
1474
|
uri = URI.parse(url)
|
|
710
1475
|
http = build_http(uri)
|
|
711
1476
|
|
|
712
|
-
req = build_request(method, uri, body)
|
|
1477
|
+
req = build_request(method, uri, body, content_type)
|
|
713
1478
|
execute_request(http, req)
|
|
714
1479
|
end
|
|
715
1480
|
|
|
716
1481
|
# Perform an HTTP request (no query params).
|
|
717
1482
|
# @api private
|
|
718
|
-
def request(method, path, body = nil
|
|
719
|
-
|
|
1483
|
+
def request(method, path, body = nil,
|
|
1484
|
+
content_type: 'application/json; charset=utf-8')
|
|
1485
|
+
do_request(method, path, body, nil, content_type: content_type)
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1488
|
+
# @api private
|
|
1489
|
+
# Percent-encode a single URL path segment (e.g. a scenario name) so that
|
|
1490
|
+
# spaces, slashes, and other reserved characters are transmitted safely.
|
|
1491
|
+
def encode_path_segment(value)
|
|
1492
|
+
raise ArgumentError, 'name must not be nil or empty' if value.nil? || value.to_s.empty?
|
|
1493
|
+
|
|
1494
|
+
URI.encode_www_form_component(value.to_s).gsub('+', '%20')
|
|
1495
|
+
end
|
|
1496
|
+
|
|
1497
|
+
# @api private
|
|
1498
|
+
# Import a document (HAR/Postman/Pact) via PUT /mockserver/import?format=<format>
|
|
1499
|
+
# and return the upserted expectations.
|
|
1500
|
+
def import_document(json, format)
|
|
1501
|
+
if json.nil? || json.to_s.strip.empty?
|
|
1502
|
+
raise ArgumentError, "import #{format} document must not be empty"
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
status, response_body = do_request(
|
|
1506
|
+
'PUT', '/mockserver/import', json.to_s, { 'format' => format }
|
|
1507
|
+
)
|
|
1508
|
+
if status >= 400
|
|
1509
|
+
raise Error, "Failed to import #{format} (status=#{status}): #{response_body}"
|
|
1510
|
+
end
|
|
1511
|
+
|
|
1512
|
+
expectations_from_response(response_body)
|
|
1513
|
+
end
|
|
1514
|
+
|
|
1515
|
+
# @api private
|
|
1516
|
+
# Parse a JSON array of expectations from a control-plane response body.
|
|
1517
|
+
def expectations_from_response(response_body)
|
|
1518
|
+
if response_body && !response_body.empty?
|
|
1519
|
+
parsed = JSON.parse(response_body)
|
|
1520
|
+
return parsed.map { |e| Expectation.from_hash(e) } if parsed.is_a?(Array)
|
|
1521
|
+
end
|
|
1522
|
+
[]
|
|
720
1523
|
end
|
|
721
1524
|
|
|
722
1525
|
# @api private
|
|
@@ -741,7 +1544,7 @@ module MockServer
|
|
|
741
1544
|
end
|
|
742
1545
|
|
|
743
1546
|
# @api private
|
|
744
|
-
def build_request(method, uri, body)
|
|
1547
|
+
def build_request(method, uri, body, content_type = 'application/json; charset=utf-8')
|
|
745
1548
|
request_path = uri.request_uri
|
|
746
1549
|
case method.upcase
|
|
747
1550
|
when 'PUT'
|
|
@@ -755,14 +1558,19 @@ module MockServer
|
|
|
755
1558
|
else
|
|
756
1559
|
req = Net::HTTP::Put.new(request_path)
|
|
757
1560
|
end
|
|
758
|
-
req['Content-Type'] =
|
|
1561
|
+
req['Content-Type'] = content_type
|
|
759
1562
|
req.body = body if body
|
|
760
1563
|
req
|
|
761
1564
|
end
|
|
762
1565
|
|
|
763
1566
|
# @api private
|
|
764
1567
|
def execute_request(http, req)
|
|
765
|
-
|
|
1568
|
+
# Use the block form of #start so the underlying TCP/TLS connection is
|
|
1569
|
+
# always closed (#finish) when the request completes, rather than being
|
|
1570
|
+
# left open until garbage collection. All connection options (use_ssl,
|
|
1571
|
+
# ca_file, verify_mode, read_timeout, open_timeout) configured on +http+
|
|
1572
|
+
# by #build_http are preserved because #start operates on this instance.
|
|
1573
|
+
response = http.start { |conn| conn.request(req) }
|
|
766
1574
|
[response.code.to_i, response.body || '']
|
|
767
1575
|
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
768
1576
|
raise ConnectionError, "Request to MockServer at #{@base_url} timed out: #{e.message}"
|