mockserver-client 7.2.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 +61 -1
- data/lib/mockserver/a2a.rb +529 -0
- data/lib/mockserver/client.rb +504 -0
- data/lib/mockserver/models.rb +270 -16
- data/lib/mockserver/version.rb +1 -1
- data/lib/mockserver-client.rb +1 -0
- metadata +3 -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
|
|
@@ -416,6 +729,169 @@ module MockServer
|
|
|
416
729
|
start_load_scenarios(name)
|
|
417
730
|
end
|
|
418
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
|
+
|
|
419
895
|
# -------------------------------------------------------------------
|
|
420
896
|
# Stateful scenarios (state machine control plane)
|
|
421
897
|
# -------------------------------------------------------------------
|
|
@@ -1018,6 +1494,34 @@ module MockServer
|
|
|
1018
1494
|
URI.encode_www_form_component(value.to_s).gsub('+', '%20')
|
|
1019
1495
|
end
|
|
1020
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
|
+
[]
|
|
1523
|
+
end
|
|
1524
|
+
|
|
1021
1525
|
# @api private
|
|
1022
1526
|
def build_http(uri)
|
|
1023
1527
|
http = Net::HTTP.new(uri.host, uri.port)
|