mockserver-client 7.2.0 → 7.4.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.
@@ -204,6 +204,352 @@ 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
+ # Drift detection
283
+ # -------------------------------------------------------------------
284
+
285
+ # Retrieve the recorded mock drift report (GET /mockserver/drift).
286
+ #
287
+ # Returns the parsed report, a Hash of the form
288
+ # +{ "count" => <n>, "drifts" => [ ... ] }+, where each entry describes a
289
+ # difference detected between a mock's configured response and the live
290
+ # upstream response for the same request. When no drift has been recorded an
291
+ # empty Hash is returned.
292
+ #
293
+ # @return [Hash] the parsed drift report
294
+ def retrieve_drift
295
+ status, response_body = request('GET', '/mockserver/drift')
296
+ if status >= 400
297
+ raise Error, "Failed to retrieve drift (status=#{status}): #{response_body}"
298
+ end
299
+
300
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
301
+ end
302
+
303
+ # Clear all recorded mock drift (PUT /mockserver/drift/clear).
304
+ # @return [nil]
305
+ def clear_drift
306
+ status, response_body = request('PUT', '/mockserver/drift/clear')
307
+ if status >= 400
308
+ raise Error, "Failed to clear drift (status=#{status}): #{response_body}"
309
+ end
310
+
311
+ nil
312
+ end
313
+
314
+ # -------------------------------------------------------------------
315
+ # Pact (import / export / verify)
316
+ # -------------------------------------------------------------------
317
+
318
+ # Import a Pact v3 contract as expectations (PUT /mockserver/pact/import).
319
+ #
320
+ # @param json [String] the Pact v3 contract JSON (must not be blank)
321
+ # @return [String] the JSON array of upserted expectations
322
+ # @raise [ArgumentError] if +json+ is nil or blank
323
+ def pact_import(json)
324
+ if json.nil? || json.to_s.strip.empty?
325
+ raise ArgumentError, 'pact JSON must not be empty'
326
+ end
327
+
328
+ status, response_body = request('PUT', '/mockserver/pact/import', json.to_s)
329
+ if status >= 400
330
+ raise Error, "Failed to import pact (status=#{status}): #{response_body}"
331
+ end
332
+
333
+ response_body || ''
334
+ end
335
+
336
+ # Export the active expectations as a Pact v3 contract
337
+ # (PUT /mockserver/pact?consumer=&provider=).
338
+ #
339
+ # A query param is only added when its value is non-blank; otherwise the
340
+ # server defaults are used.
341
+ #
342
+ # @param consumer [String, nil] the Pact consumer name
343
+ # @param provider [String, nil] the Pact provider name
344
+ # @return [String] the generated Pact v3 contract JSON
345
+ def pact_export(consumer: nil, provider: nil)
346
+ query_params = {}
347
+ query_params['consumer'] = consumer if consumer && !consumer.to_s.empty?
348
+ query_params['provider'] = provider if provider && !provider.to_s.empty?
349
+ status, response_body = do_request(
350
+ 'PUT', '/mockserver/pact', nil, query_params.empty? ? nil : query_params
351
+ )
352
+ if status >= 400
353
+ raise Error, "Failed to export pact (status=#{status}): #{response_body}"
354
+ end
355
+
356
+ response_body || ''
357
+ end
358
+
359
+ # Verify a Pact v3 contract against the active expectations
360
+ # (PUT /mockserver/pact/verify).
361
+ #
362
+ # The server encodes the verdict in the HTTP status: 202 when every
363
+ # interaction matches (PASS), 406 when verification fails (FAIL). Both
364
+ # carry the verification report JSON, which is returned verbatim in both
365
+ # cases (a FAIL does not raise) so callers can inspect the report.
366
+ #
367
+ # @param json [String] the Pact v3 contract JSON to verify (must not be blank)
368
+ # @return [String] the verification report JSON
369
+ # @raise [ArgumentError] if +json+ is nil or blank
370
+ def pact_verify(json)
371
+ if json.nil? || json.to_s.strip.empty?
372
+ raise ArgumentError, 'pact JSON must not be empty'
373
+ end
374
+
375
+ status, response_body = request('PUT', '/mockserver/pact/verify', json.to_s)
376
+ if status == 202 || status == 406
377
+ return response_body || ''
378
+ end
379
+ if status >= 400
380
+ raise Error, "Failed to verify pact (status=#{status}): #{response_body}"
381
+ end
382
+
383
+ response_body || ''
384
+ end
385
+
386
+ # -------------------------------------------------------------------
387
+ # File store (store / retrieve / list / delete)
388
+ # -------------------------------------------------------------------
389
+
390
+ # Store a file in the in-memory file store (PUT /mockserver/files/store).
391
+ #
392
+ # @param name [String] the file name/key
393
+ # @param content [String] the UTF-8 file content
394
+ # @return [Hash] response of the form { "name" => ..., "size" => ... }
395
+ def store_file(name, content)
396
+ body = JSON.generate({ 'name' => name, 'content' => content })
397
+ status, response_body = request('PUT', '/mockserver/files/store', body)
398
+ if status >= 400
399
+ raise Error, "Failed to store file (status=#{status}): #{response_body}"
400
+ end
401
+
402
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
403
+ end
404
+
405
+ # Retrieve a file's raw content (PUT /mockserver/files/retrieve).
406
+ #
407
+ # Returns the raw 200 body verbatim. An unknown file yields a 404 which
408
+ # raises an {Error} (there is no null-on-404 fallback).
409
+ #
410
+ # @param name [String] the file name/key
411
+ # @return [String] the raw file content
412
+ def retrieve_file(name)
413
+ body = JSON.generate({ 'name' => name })
414
+ status, response_body = request('PUT', '/mockserver/files/retrieve', body)
415
+ if status == 404
416
+ raise Error, "File not found (status=404): #{name}"
417
+ end
418
+ if status >= 400
419
+ raise Error, "Failed to retrieve file (status=#{status}): #{response_body}"
420
+ end
421
+
422
+ response_body || ''
423
+ end
424
+
425
+ # List the names of all stored files (PUT /mockserver/files/list).
426
+ #
427
+ # @return [Array<String>] the file names
428
+ def list_files
429
+ status, response_body = request('PUT', '/mockserver/files/list')
430
+ if status >= 400
431
+ raise Error, "Failed to list files (status=#{status}): #{response_body}"
432
+ end
433
+
434
+ if response_body && !response_body.empty?
435
+ parsed = JSON.parse(response_body)
436
+ return parsed if parsed.is_a?(Array)
437
+ end
438
+ []
439
+ end
440
+
441
+ # Delete a stored file (PUT /mockserver/files/delete).
442
+ #
443
+ # An unknown file yields a 404 which raises an {Error}.
444
+ #
445
+ # @param name [String] the file name/key
446
+ # @return [nil]
447
+ def delete_file(name)
448
+ body = JSON.generate({ 'name' => name })
449
+ status, response_body = request('PUT', '/mockserver/files/delete', body)
450
+ if status == 404
451
+ raise Error, "File not found (status=404): #{name}"
452
+ end
453
+ if status >= 400
454
+ raise Error, "Failed to delete file (status=#{status}): #{response_body}"
455
+ end
456
+
457
+ nil
458
+ end
459
+
460
+ # -------------------------------------------------------------------
461
+ # Import (HAR / Postman)
462
+ # -------------------------------------------------------------------
463
+
464
+ # Import a HAR document as expectations (PUT /mockserver/import?format=har).
465
+ #
466
+ # @param har_json [String] the HAR JSON document (must not be blank)
467
+ # @return [Array<Expectation>] the upserted expectations
468
+ # @raise [ArgumentError] if +har_json+ is nil or blank
469
+ def import_har(har_json)
470
+ import_document(har_json, 'har')
471
+ end
472
+
473
+ # Import a Postman collection as expectations
474
+ # (PUT /mockserver/import?format=postman).
475
+ #
476
+ # @param collection_json [String] the Postman collection JSON (must not be blank)
477
+ # @return [Array<Expectation>] the upserted expectations
478
+ # @raise [ArgumentError] if +collection_json+ is nil or blank
479
+ def import_postman_collection(collection_json)
480
+ import_document(collection_json, 'postman')
481
+ end
482
+
483
+ # -------------------------------------------------------------------
484
+ # Operating mode (SIMULATE / SPY / CAPTURE)
485
+ # -------------------------------------------------------------------
486
+
487
+ # Valid operating modes (parallels +org.mockserver.mock.MockMode+).
488
+ MODE_SIMULATE = 'SIMULATE'
489
+ MODE_SPY = 'SPY'
490
+ MODE_CAPTURE = 'CAPTURE'
491
+
492
+ # Set the high-level operating mode (PUT /mockserver/mode?mode=<MODE>).
493
+ #
494
+ # +mode+ may be a Symbol or String (case-insensitive), one of
495
+ # +:simulate+/+:spy+/+:capture+ (or the {MODE_SIMULATE}, {MODE_SPY},
496
+ # {MODE_CAPTURE} constants). Setting the mode also flips the
497
+ # proxy-on-no-match behaviour on the server.
498
+ #
499
+ # @param mode [String, Symbol] the operating mode
500
+ # @return [Hash] response of the form
501
+ # { "mode" => ..., "proxyUnmatchedRequests" => ... }
502
+ def set_mode(mode)
503
+ mode_value = mode.to_s.upcase
504
+ status, response_body = do_request(
505
+ 'PUT', '/mockserver/mode', nil, { 'mode' => mode_value }
506
+ )
507
+ if status >= 400
508
+ raise Error, "Failed to set mode (status=#{status}): #{response_body}"
509
+ end
510
+
511
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
512
+ end
513
+
514
+ # Read the current operating mode (GET /mockserver/mode).
515
+ #
516
+ # @return [Hash] response of the form
517
+ # { "mode" => ..., "proxyUnmatchedRequests" => ... }
518
+ def retrieve_mode
519
+ status, response_body = request('GET', '/mockserver/mode')
520
+ if status >= 400
521
+ raise Error, "Failed to retrieve mode (status=#{status}): #{response_body}"
522
+ end
523
+
524
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
525
+ end
526
+
527
+ # -------------------------------------------------------------------
528
+ # WSDL -> expectations
529
+ # -------------------------------------------------------------------
530
+
531
+ # Generate expectations from a WSDL document (PUT /mockserver/wsdl).
532
+ #
533
+ # The WSDL XML is sent as the raw request body with an XML content type.
534
+ #
535
+ # @param wsdl [String] the WSDL document (XML, must not be blank)
536
+ # @return [Array<Expectation>] the generated (upserted) expectations
537
+ # @raise [ArgumentError] if +wsdl+ is nil or blank
538
+ def wsdl_expectation(wsdl)
539
+ if wsdl.nil? || wsdl.to_s.strip.empty?
540
+ raise ArgumentError, 'WSDL must not be empty'
541
+ end
542
+
543
+ status, response_body = request(
544
+ 'PUT', '/mockserver/wsdl', wsdl.to_s, content_type: 'application/xml'
545
+ )
546
+ if status >= 400
547
+ raise Error, "Failed to generate WSDL expectations (status=#{status}): #{response_body}"
548
+ end
549
+
550
+ expectations_from_response(response_body)
551
+ end
552
+
207
553
  # Register a service-scoped HTTP chaos profile for an upstream host. The profile
208
554
  # is applied to every matched forward expectation to that host that does not
209
555
  # define its own chaos (an expectation's own chaos always wins). The host is
@@ -416,6 +762,169 @@ module MockServer
416
762
  start_load_scenarios(name)
417
763
  end
418
764
 
765
+ # Fetch the end-of-run summary report for a load scenario run.
766
+ #
767
+ # Returns the report derived from the run's status snapshot (live while
768
+ # running, or the retained terminal snapshot once finished). With no +format+
769
+ # (or any value other than +"junit"+) the JSON report is parsed and returned
770
+ # as a Hash carrying counts, latency percentiles, the threshold verdict and
771
+ # per-threshold results. With +format: "junit"+ the raw JUnit-XML
772
+ # +<testsuite>+ document is returned as a String so a load run becomes a
773
+ # first-class CI test artifact.
774
+ #
775
+ # @param name [String] the unique scenario name
776
+ # @param format [String, nil] +"junit"+ for the JUnit-XML report, otherwise JSON
777
+ # @return [Hash, String] parsed JSON report (default) or the raw JUnit-XML String
778
+ # @raise [Error] if the scenario never ran (404) or another failure occurs
779
+ def get_load_scenario_report(name, format = nil)
780
+ query_params = {}
781
+ query_params['format'] = format if format
782
+ status, response_body = do_request(
783
+ 'GET', "/mockserver/loadScenario/#{encode_path_segment(name)}/report", nil,
784
+ query_params.empty? ? nil : query_params
785
+ )
786
+ if status == 404
787
+ raise Error, "Load scenario report not found (status=404): #{name}"
788
+ end
789
+ if status >= 400
790
+ raise Error, "Failed to get load scenario report (status=#{status}): #{response_body}"
791
+ end
792
+
793
+ return response_body if format == 'junit'
794
+
795
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
796
+ end
797
+
798
+ # Generate (and register) a load scenario from an OpenAPI specification.
799
+ #
800
+ # Produces an editable scenario - one step per OpenAPI operation - and loads
801
+ # it into the registry under +name+ in the LOADED state; it generates no
802
+ # traffic and is allowed even when load generation is disabled.
803
+ #
804
+ # @param name [String] the generated scenario name (the unique registry key)
805
+ # @param spec_url_or_payload [String] OpenAPI spec as inline JSON/YAML, a URL,
806
+ # or a file/classpath reference
807
+ # @param target [Hash, nil] explicit network target for every generated step
808
+ # (e.g. { "host" => ..., "port" => ..., "scheme" => "http" })
809
+ # @param profile [LoadProfile, Hash, nil] optional traffic profile (a
810
+ # conservative default is applied when omitted)
811
+ # @return [Hash] parsed response of the form
812
+ # { "status" => "loaded", "name" => ..., "state" => ..., "scenario" => {...} }
813
+ def generate_load_scenario_from_openapi(name, spec_url_or_payload, target: nil, profile: nil)
814
+ payload = { 'name' => name, 'specUrlOrPayload' => spec_url_or_payload }
815
+ payload['target'] = target if target
816
+ payload['profile'] = profile.respond_to?(:to_h) ? profile.to_h : profile if profile
817
+ body = JSON.generate(payload)
818
+ status, response_body = request('PUT', '/mockserver/loadScenario/generateFromOpenAPI', body)
819
+ if status >= 400
820
+ raise Error, "Failed to generate load scenario from OpenAPI (status=#{status}): #{response_body}"
821
+ end
822
+
823
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
824
+ end
825
+
826
+ # Generate (and register) a load scenario from recorded proxy traffic.
827
+ #
828
+ # Converts requests previously recorded by MockServer in proxy/recording mode
829
+ # into an editable scenario and loads it into the registry under +name+ in the
830
+ # LOADED state; it generates no traffic and is allowed even when load
831
+ # generation is disabled.
832
+ #
833
+ # @param name [String] the generated scenario name (the unique registry key)
834
+ # @param mode [String, nil] +VERBATIM+ (default) or +TEMPLATIZED+
835
+ # @param request_filter [HttpRequest, Hash, nil] optional matcher selecting
836
+ # which recorded requests to include (absent means all)
837
+ # @param target [Hash, nil] explicit network target applied to every step
838
+ # @param max_steps [Integer, nil] optional cap on the number of VERBATIM steps
839
+ # @return [Hash] parsed response of the form
840
+ # { "status" => "loaded", "name" => ..., "state" => ..., "scenario" => {...} }
841
+ def generate_load_scenario_from_recording(name, mode: nil, request_filter: nil,
842
+ target: nil, max_steps: nil)
843
+ payload = { 'name' => name }
844
+ payload['mode'] = mode if mode
845
+ payload['requestFilter'] = request_filter.respond_to?(:to_h) ? request_filter.to_h : request_filter if request_filter
846
+ payload['target'] = target if target
847
+ payload['maxSteps'] = max_steps if max_steps
848
+ body = JSON.generate(payload)
849
+ status, response_body = request('PUT', '/mockserver/loadScenario/generateFromRecording', body)
850
+ if status >= 400
851
+ raise Error, "Failed to generate load scenario from recording (status=#{status}): #{response_body}"
852
+ end
853
+
854
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
855
+ end
856
+
857
+ # -------------------------------------------------------------------
858
+ # SRE control plane: SLO verification + chaos experiments
859
+ # -------------------------------------------------------------------
860
+
861
+ # Evaluate a set of service-level objectives (SLOs) over a window
862
+ # (PUT /mockserver/verifySLO).
863
+ #
864
+ # The server encodes the verdict in the HTTP status: 200 for PASS or
865
+ # INCONCLUSIVE, 406 for FAIL, 400 for malformed criteria or when SLO tracking
866
+ # is disabled (+sloTrackingEnabled=false+). The decoded verdict Hash carries
867
+ # the overall +result+ and the per-objective +objectiveResults+ so callers can
868
+ # inspect why an SLO failed.
869
+ #
870
+ # +criteria+ may be any Hash already shaped to the +SloCriteria+ JSON contract
871
+ # (+name+, +window+, +minimumSampleCount+, +upstreamHosts+,
872
+ # +objectives+[{+sli+, +comparator+, +threshold+, +scope+}]) or an object that
873
+ # responds to +to_h+.
874
+ #
875
+ # @param criteria [Hash, #to_h] the SLO criteria
876
+ # @return [Hash] the SLO verdict (result PASS or INCONCLUSIVE)
877
+ # @raise [VerificationError] if the verdict is FAIL (HTTP 406)
878
+ # @raise [Error] if criteria are malformed or SLO tracking is disabled (HTTP 400),
879
+ # or on any other failure
880
+ def verify_slo(criteria)
881
+ payload = criteria.respond_to?(:to_h) ? criteria.to_h : criteria
882
+ body = JSON.generate(payload)
883
+ status, response_body = request('PUT', '/mockserver/verifySLO', body)
884
+ if status == 406
885
+ raise VerificationError, (response_body && !response_body.empty? ? response_body : 'SLO verdict: FAIL')
886
+ end
887
+ if status == 400
888
+ raise Error, 'Invalid SLO criteria (or SLO tracking disabled — set ' \
889
+ "sloTrackingEnabled=true on the server): #{response_body}"
890
+ end
891
+ if status >= 400
892
+ raise Error, "Failed to verify SLO (status=#{status}): #{response_body}"
893
+ end
894
+
895
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
896
+ end
897
+
898
+ # Start a scheduled multi-stage chaos experiment
899
+ # (PUT /mockserver/chaosExperiment).
900
+ #
901
+ # The experiment is an ordered sequence of stages, each applying
902
+ # service-scoped chaos profiles to one or more hosts for a duration; stages
903
+ # progress automatically. Only one experiment may be active at a time;
904
+ # starting a new one stops the previous one.
905
+ #
906
+ # +experiment+ may be any Hash already shaped to the +ChaosExperiment+ JSON
907
+ # contract (+name+, +loop+, +stages+[{+durationMillis+, +profiles+{host:
908
+ # profile}}]) or an object that responds to +to_h+.
909
+ #
910
+ # @param experiment [Hash, #to_h] the experiment definition
911
+ # @return [Hash] the started status, e.g. { "status" => "started", "name" => ... }
912
+ # @raise [Error] if the experiment definition is invalid or chaos is disabled
913
+ # (HTTP 400/403), or on any other failure
914
+ def start_chaos_experiment(experiment)
915
+ payload = experiment.respond_to?(:to_h) ? experiment.to_h : experiment
916
+ body = JSON.generate(payload)
917
+ status, response_body = request('PUT', '/mockserver/chaosExperiment', body)
918
+ if status == 403
919
+ raise Error, 'Chaos experiment rejected (status=403): chaos is disabled on the server'
920
+ end
921
+ if status >= 400
922
+ raise Error, "Failed to start chaos experiment (status=#{status}): #{response_body}"
923
+ end
924
+
925
+ response_body && !response_body.empty? ? JSON.parse(response_body) : {}
926
+ end
927
+
419
928
  # -------------------------------------------------------------------
420
929
  # Stateful scenarios (state machine control plane)
421
930
  # -------------------------------------------------------------------
@@ -1018,6 +1527,34 @@ module MockServer
1018
1527
  URI.encode_www_form_component(value.to_s).gsub('+', '%20')
1019
1528
  end
1020
1529
 
1530
+ # @api private
1531
+ # Import a document (HAR/Postman/Pact) via PUT /mockserver/import?format=<format>
1532
+ # and return the upserted expectations.
1533
+ def import_document(json, format)
1534
+ if json.nil? || json.to_s.strip.empty?
1535
+ raise ArgumentError, "import #{format} document must not be empty"
1536
+ end
1537
+
1538
+ status, response_body = do_request(
1539
+ 'PUT', '/mockserver/import', json.to_s, { 'format' => format }
1540
+ )
1541
+ if status >= 400
1542
+ raise Error, "Failed to import #{format} (status=#{status}): #{response_body}"
1543
+ end
1544
+
1545
+ expectations_from_response(response_body)
1546
+ end
1547
+
1548
+ # @api private
1549
+ # Parse a JSON array of expectations from a control-plane response body.
1550
+ def expectations_from_response(response_body)
1551
+ if response_body && !response_body.empty?
1552
+ parsed = JSON.parse(response_body)
1553
+ return parsed.map { |e| Expectation.from_hash(e) } if parsed.is_a?(Array)
1554
+ end
1555
+ []
1556
+ end
1557
+
1021
1558
  # @api private
1022
1559
  def build_http(uri)
1023
1560
  http = Net::HTTP.new(uri.host, uri.port)