mockserver-client 7.0.0 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  require 'base64'
4
4
  require 'json'
5
5
  require 'set'
6
+ require 'erb'
6
7
 
7
8
  module MockServer
8
9
  # Explicit mapping from Ruby snake_case field names to the camelCase
@@ -30,6 +31,7 @@ module MockServer
30
31
  'response_callback' => 'responseCallback',
31
32
  'drop_connection' => 'dropConnection',
32
33
  'response_bytes' => 'responseBytes',
34
+ 'stream_error' => 'streamError',
33
35
  'http_request' => 'httpRequest',
34
36
  'http_response' => 'httpResponse',
35
37
  'http_response_template' => 'httpResponseTemplate',
@@ -44,6 +46,8 @@ module MockServer
44
46
  'http_sse_response' => 'httpSseResponse',
45
47
  'http_websocket_response' => 'httpWebSocketResponse',
46
48
  'template_type' => 'templateType',
49
+ 'template_file' => 'templateFile',
50
+ 'file_path' => 'filePath',
47
51
  'base64_bytes' => 'base64Bytes',
48
52
  'not_body' => 'not',
49
53
  'content_type' => 'contentType',
@@ -81,7 +85,17 @@ module MockServer
81
85
  'degradation_ramp_millis' => 'degradationRampMillis',
82
86
  'http_class_callback' => 'httpClassCallback',
83
87
  'http_object_callback' => 'httpObjectCallback',
84
- 'failure_policy' => 'failurePolicy'
88
+ 'failure_policy' => 'failurePolicy',
89
+ 'http_responses' => 'httpResponses',
90
+ 'response_mode' => 'responseMode',
91
+ 'response_weights' => 'responseWeights',
92
+ 'switch_after' => 'switchAfter',
93
+ 'cross_protocol_scenarios' => 'crossProtocolScenarios',
94
+ 'scenario_name' => 'scenarioName',
95
+ 'scenario_state' => 'scenarioState',
96
+ 'new_scenario_state' => 'newScenarioState',
97
+ 'match_pattern' => 'matchPattern',
98
+ 'target_state' => 'targetState'
85
99
  }.freeze
86
100
 
87
101
  REVERSE_FIELD_MAP = FIELD_MAP.invert.freeze
@@ -89,7 +103,7 @@ module MockServer
89
103
  # Known Body type strings used to distinguish Body objects from plain hashes
90
104
  # during deserialization.
91
105
  BODY_TYPES = Set.new(%w[
92
- STRING JSON REGEX XML BINARY JSON_SCHEMA JSON_PATH XPATH XML_SCHEMA JSON_RPC GRAPHQL
106
+ STRING JSON REGEX XML BINARY JSON_SCHEMA JSON_PATH XPATH XML_SCHEMA JSON_RPC GRAPHQL FILE
93
107
  ]).freeze
94
108
 
95
109
  # -------------------------------------------------------------------
@@ -116,6 +130,30 @@ module MockServer
116
130
  hash.reject { |_k, v| v.nil? }
117
131
  end
118
132
 
133
+ # @api private
134
+ # Coerce a class-callback value into an {HttpClassCallback}. Accepts:
135
+ # * +nil+ -> +nil+
136
+ # * a +String+ -> +HttpClassCallback.new(callback_class: <string>)+
137
+ # * an HttpClassCallback -> returned unchanged
138
+ # Any other type raises a TypeError. This lets the Expectation setters and the
139
+ # fluent builder accept either a fully-qualified class-name String or a
140
+ # pre-built {HttpClassCallback} (carrying +delay+ / +primary+).
141
+ def self.coerce_class_callback(value)
142
+ return nil if value.nil?
143
+ return HttpClassCallback.new(callback_class: value) if value.is_a?(String)
144
+ return value if value.is_a?(HttpClassCallback)
145
+
146
+ raise TypeError,
147
+ "Expected a class-name String or HttpClassCallback, got #{value.class.name}"
148
+ end
149
+
150
+ # @api private
151
+ # Percent-encode a single URL path segment (e.g. a scenario name), encoding
152
+ # spaces as %20 (not +) so the segment is safe inside +/mockserver/scenario/{name}+.
153
+ def self.encode_path_segment(value)
154
+ ERB::Util.url_encode(value.to_s)
155
+ end
156
+
119
157
  # @api private
120
158
  def self.serialize_value(value)
121
159
  case value
@@ -185,6 +223,29 @@ module MockServer
185
223
  end
186
224
  end
187
225
 
226
+ # @api private
227
+ # Unlike headers / query parameters, MockServer represents cookies as a
228
+ # single-value {name => value} object map, not a [{name, values}] array.
229
+ def self.serialize_cookies(items)
230
+ return nil if items.nil?
231
+
232
+ items.each_with_object({}) do |item, map|
233
+ map[item.name] = item.values.is_a?(Array) ? item.values.first : item.values
234
+ end
235
+ end
236
+
237
+ # @api private
238
+ def self.deserialize_cookies(data)
239
+ return nil if data.nil?
240
+
241
+ if data.is_a?(Hash)
242
+ return data.map { |k, v| KeyToMultiValue.new(name: k, values: v.is_a?(Array) ? v : [v]) }
243
+ end
244
+
245
+ # tolerate the legacy array form on read
246
+ deserialize_key_multi_values(data)
247
+ end
248
+
188
249
  # -------------------------------------------------------------------
189
250
  # Model classes
190
251
  # -------------------------------------------------------------------
@@ -359,9 +420,11 @@ module MockServer
359
420
  end
360
421
 
361
422
  class Body
362
- attr_accessor :type, :string, :json, :base64_bytes, :not_body, :content_type, :charset
423
+ attr_accessor :type, :string, :json, :base64_bytes, :not_body, :content_type, :charset,
424
+ :file_path, :template_type
363
425
 
364
- def initialize(type: nil, string: nil, json: nil, base64_bytes: nil, not_body: nil, content_type: nil, charset: nil)
426
+ def initialize(type: nil, string: nil, json: nil, base64_bytes: nil, not_body: nil,
427
+ content_type: nil, charset: nil, file_path: nil, template_type: nil)
365
428
  @type = type
366
429
  @string = string
367
430
  @json = json
@@ -369,17 +432,21 @@ module MockServer
369
432
  @not_body = not_body
370
433
  @content_type = content_type
371
434
  @charset = charset
435
+ @file_path = file_path
436
+ @template_type = template_type
372
437
  end
373
438
 
374
439
  def to_h
375
440
  result = {}
376
- result['type'] = @type unless @type.nil?
377
- result['string'] = @string unless @string.nil?
378
- result['json'] = @json unless @json.nil?
379
- result['base64Bytes'] = @base64_bytes unless @base64_bytes.nil?
380
- result['not'] = @not_body unless @not_body.nil?
381
- result['contentType'] = @content_type unless @content_type.nil?
382
- result['charset'] = @charset unless @charset.nil?
441
+ result['type'] = @type unless @type.nil?
442
+ result['string'] = @string unless @string.nil?
443
+ result['json'] = @json unless @json.nil?
444
+ result['base64Bytes'] = @base64_bytes unless @base64_bytes.nil?
445
+ result['not'] = @not_body unless @not_body.nil?
446
+ result['contentType'] = @content_type unless @content_type.nil?
447
+ result['charset'] = @charset unless @charset.nil?
448
+ result['filePath'] = @file_path unless @file_path.nil?
449
+ result['templateType'] = @template_type unless @template_type.nil?
383
450
  result
384
451
  end
385
452
 
@@ -387,13 +454,15 @@ module MockServer
387
454
  return nil if data.nil?
388
455
 
389
456
  new(
390
- type: data['type'],
391
- string: data['string'],
392
- json: data['json'],
393
- base64_bytes: data['base64Bytes'],
394
- not_body: data['not'],
395
- content_type: data['contentType'],
396
- charset: data['charset']
457
+ type: data['type'],
458
+ string: data['string'],
459
+ json: data['json'],
460
+ base64_bytes: data['base64Bytes'],
461
+ not_body: data['not'],
462
+ content_type: data['contentType'],
463
+ charset: data['charset'],
464
+ file_path: data['filePath'],
465
+ template_type: data['templateType']
397
466
  )
398
467
  end
399
468
 
@@ -417,6 +486,10 @@ module MockServer
417
486
  new(type: 'XML', string: value)
418
487
  end
419
488
 
489
+ def self.file(file_path, content_type: nil, template_type: nil)
490
+ new(type: 'FILE', file_path: file_path, content_type: content_type, template_type: template_type)
491
+ end
492
+
420
493
  def self.json_rpc(method_name, params_schema: nil)
421
494
  JsonRpcBody.new(method_name: method_name, params_schema: params_schema)
422
495
  end
@@ -424,6 +497,11 @@ module MockServer
424
497
  def self.graphql(query, operation_name: nil, variables_schema: nil)
425
498
  GraphQLBody.new(query: query, operation_name: operation_name, variables_schema: variables_schema)
426
499
  end
500
+
501
+ def with_template_type(template_type)
502
+ @template_type = template_type
503
+ self
504
+ end
427
505
  end
428
506
 
429
507
  class JsonRpcBody
@@ -544,7 +622,7 @@ module MockServer
544
622
  'path' => @path,
545
623
  'queryStringParameters' => MockServer.serialize_key_multi_values(@query_string_parameters),
546
624
  'headers' => MockServer.serialize_key_multi_values(@headers),
547
- 'cookies' => MockServer.serialize_key_multi_values(@cookies),
625
+ 'cookies' => MockServer.serialize_cookies(@cookies),
548
626
  'body' => MockServer.serialize_body(@body),
549
627
  'secure' => @secure,
550
628
  'keepAlive' => @keep_alive,
@@ -562,7 +640,7 @@ module MockServer
562
640
  path: data['path'],
563
641
  query_string_parameters: MockServer.deserialize_key_multi_values(data['queryStringParameters']),
564
642
  headers: MockServer.deserialize_key_multi_values(data['headers']),
565
- cookies: MockServer.deserialize_key_multi_values(data['cookies']),
643
+ cookies: MockServer.deserialize_cookies(data['cookies']),
566
644
  body: MockServer.deserialize_body(data['body']),
567
645
  secure: data['secure'],
568
646
  keep_alive: data['keepAlive'],
@@ -694,7 +772,7 @@ module MockServer
694
772
  'statusCode' => @status_code,
695
773
  'reasonPhrase' => @reason_phrase,
696
774
  'headers' => MockServer.serialize_key_multi_values(@headers),
697
- 'cookies' => MockServer.serialize_key_multi_values(@cookies),
775
+ 'cookies' => MockServer.serialize_cookies(@cookies),
698
776
  'body' => MockServer.serialize_body(@body),
699
777
  'delay' => @delay&.to_h,
700
778
  'connectionOptions' => @connection_options&.to_h,
@@ -709,7 +787,7 @@ module MockServer
709
787
  status_code: data['statusCode'],
710
788
  reason_phrase: data['reasonPhrase'],
711
789
  headers: MockServer.deserialize_key_multi_values(data['headers']),
712
- cookies: MockServer.deserialize_key_multi_values(data['cookies']),
790
+ cookies: MockServer.deserialize_cookies(data['cookies']),
713
791
  body: MockServer.deserialize_body(data['body']),
714
792
  delay: Delay.from_hash(data['delay']),
715
793
  connection_options: ConnectionOptions.from_hash(data['connectionOptions']),
@@ -809,11 +887,12 @@ module MockServer
809
887
  end
810
888
 
811
889
  class HttpTemplate
812
- attr_accessor :template_type, :template, :delay, :primary
890
+ attr_accessor :template_type, :template, :template_file, :delay, :primary
813
891
 
814
- def initialize(template_type: 'JAVASCRIPT', template: nil, delay: nil, primary: nil)
892
+ def initialize(template_type: 'JAVASCRIPT', template: nil, template_file: nil, delay: nil, primary: nil)
815
893
  @template_type = template_type
816
894
  @template = template
895
+ @template_file = template_file
817
896
  @delay = delay
818
897
  @primary = primary
819
898
  end
@@ -822,6 +901,7 @@ module MockServer
822
901
  MockServer.strip_none({
823
902
  'templateType' => @template_type,
824
903
  'template' => @template,
904
+ 'templateFile' => @template_file,
825
905
  'delay' => @delay&.to_h,
826
906
  'primary' => @primary
827
907
  })
@@ -833,13 +913,19 @@ module MockServer
833
913
  new(
834
914
  template_type: data.fetch('templateType', 'JAVASCRIPT'),
835
915
  template: data['template'],
916
+ template_file: data['templateFile'],
836
917
  delay: Delay.from_hash(data['delay']),
837
918
  primary: data['primary']
838
919
  )
839
920
  end
840
921
 
841
- def self.template(template_type, template = nil)
842
- new(template_type: template_type, template: template)
922
+ def self.template(template_type, template = nil, template_file: nil)
923
+ new(template_type: template_type, template: template, template_file: template_file)
924
+ end
925
+
926
+ def with_template_file(template_file)
927
+ @template_file = template_file
928
+ self
843
929
  end
844
930
  end
845
931
 
@@ -907,11 +993,15 @@ module MockServer
907
993
  end
908
994
 
909
995
  class HttpError
910
- attr_accessor :drop_connection, :response_bytes, :delay, :primary
996
+ # stream_error: reset the matched request stream with this error code (HTTP/2 RST_STREAM /
997
+ # HTTP/3 RESET_STREAM) instead of returning a response; HTTP/1.1 has no stream concept so this
998
+ # falls back to dropping the connection. Takes precedence over drop_connection when both are set.
999
+ attr_accessor :drop_connection, :response_bytes, :stream_error, :delay, :primary
911
1000
 
912
- def initialize(drop_connection: nil, response_bytes: nil, delay: nil, primary: nil)
1001
+ def initialize(drop_connection: nil, response_bytes: nil, stream_error: nil, delay: nil, primary: nil)
913
1002
  @drop_connection = drop_connection
914
1003
  @response_bytes = response_bytes
1004
+ @stream_error = stream_error
915
1005
  @delay = delay
916
1006
  @primary = primary
917
1007
  end
@@ -920,6 +1010,7 @@ module MockServer
920
1010
  MockServer.strip_none({
921
1011
  'dropConnection' => @drop_connection,
922
1012
  'responseBytes' => @response_bytes,
1013
+ 'streamError' => @stream_error,
923
1014
  'delay' => @delay&.to_h,
924
1015
  'primary' => @primary
925
1016
  })
@@ -931,6 +1022,7 @@ module MockServer
931
1022
  new(
932
1023
  drop_connection: data['dropConnection'],
933
1024
  response_bytes: data['responseBytes'],
1025
+ stream_error: data['streamError'],
934
1026
  delay: Delay.from_hash(data['delay']),
935
1027
  primary: data['primary']
936
1028
  )
@@ -1652,6 +1744,146 @@ module MockServer
1652
1744
  end
1653
1745
  end
1654
1746
 
1747
+ # The strategy used to pick which of an expectation's +http_responses+ is
1748
+ # returned for each matching request. Mirrors the core +ResponseMode+ enum.
1749
+ module ResponseMode
1750
+ SEQUENTIAL = 'SEQUENTIAL'
1751
+ RANDOM = 'RANDOM'
1752
+ WEIGHTED = 'WEIGHTED'
1753
+ SWITCH = 'SWITCH'
1754
+ end
1755
+
1756
+ # The protocol event that advances a cross-protocol scenario. Mirrors the
1757
+ # core +CrossProtocolTrigger+ enum.
1758
+ module CrossProtocolTrigger
1759
+ DNS_QUERY = 'DNS_QUERY'
1760
+ WEBSOCKET_CONNECT = 'WEBSOCKET_CONNECT'
1761
+ GRPC_REQUEST = 'GRPC_REQUEST'
1762
+ HTTP_REQUEST = 'HTTP_REQUEST'
1763
+ end
1764
+
1765
+ # Describes a cross-protocol scenario correlation: when a protocol event
1766
+ # matching +trigger+ (and optionally +match_pattern+) is observed, the named
1767
+ # scenario +scenario_name+ is advanced to +target_state+.
1768
+ class CrossProtocolScenario
1769
+ attr_accessor :trigger, :match_pattern, :scenario_name, :target_state
1770
+
1771
+ def initialize(trigger: nil, match_pattern: nil, scenario_name: nil, target_state: nil)
1772
+ @trigger = trigger
1773
+ @match_pattern = match_pattern
1774
+ @scenario_name = scenario_name
1775
+ @target_state = target_state
1776
+ end
1777
+
1778
+ def to_h
1779
+ MockServer.strip_none({
1780
+ 'trigger' => @trigger,
1781
+ 'matchPattern' => @match_pattern,
1782
+ 'scenarioName' => @scenario_name,
1783
+ 'targetState' => @target_state
1784
+ })
1785
+ end
1786
+
1787
+ def self.from_hash(data)
1788
+ return nil if data.nil?
1789
+
1790
+ new(
1791
+ trigger: data['trigger'],
1792
+ match_pattern: data['matchPattern'],
1793
+ scenario_name: data['scenarioName'],
1794
+ target_state: data['targetState']
1795
+ )
1796
+ end
1797
+ end
1798
+
1799
+ # The state of a single named scenario as reported by the server's scenario
1800
+ # control plane. +scenario_name+ is the state-machine name and +current_state+
1801
+ # is its current state (+nil+ if not yet set).
1802
+ class ScenarioState
1803
+ attr_accessor :scenario_name, :current_state, :next_state, :transition_after_ms
1804
+
1805
+ def initialize(scenario_name: nil, current_state: nil, next_state: nil, transition_after_ms: nil)
1806
+ @scenario_name = scenario_name
1807
+ @current_state = current_state
1808
+ @next_state = next_state
1809
+ @transition_after_ms = transition_after_ms
1810
+ end
1811
+
1812
+ def to_h
1813
+ MockServer.strip_none({
1814
+ 'scenarioName' => @scenario_name,
1815
+ 'currentState' => @current_state,
1816
+ 'nextState' => @next_state,
1817
+ 'transitionAfterMs' => @transition_after_ms
1818
+ })
1819
+ end
1820
+
1821
+ def self.from_hash(data)
1822
+ return nil if data.nil?
1823
+
1824
+ new(
1825
+ scenario_name: data['scenarioName'],
1826
+ current_state: data['currentState'],
1827
+ next_state: data['nextState'],
1828
+ transition_after_ms: data['transitionAfterMs']
1829
+ )
1830
+ end
1831
+ end
1832
+
1833
+ # A handle to a single named stateful scenario on the server, wrapping the
1834
+ # +/mockserver/scenario/{name}+ control-plane endpoints.
1835
+ #
1836
+ # Obtained via {Client#scenario}:
1837
+ # client.scenario('Deploy').set('Deploying', transition_after_ms: 5000, next_state: 'Deployed')
1838
+ # client.scenario('Deploy').trigger('Failed')
1839
+ # client.scenario('Deploy').state # => "Failed"
1840
+ class ScenarioHandle
1841
+ attr_reader :name
1842
+
1843
+ def initialize(client, name)
1844
+ @client = client
1845
+ @name = name
1846
+ end
1847
+
1848
+ # GET the current state of this scenario (+nil+ if not yet set).
1849
+ # @return [String, nil]
1850
+ def state
1851
+ result = @client.scenario_request('GET', scenario_path)
1852
+ result['currentState']
1853
+ end
1854
+
1855
+ # PUT to set this scenario's state, optionally scheduling a timed transition
1856
+ # to +next_state+ after +transition_after_ms+ milliseconds.
1857
+ #
1858
+ # @param state [String] the state to set
1859
+ # @param transition_after_ms [Integer, nil] delay before auto-transitioning
1860
+ # @param next_state [String, nil] the state to auto-transition to
1861
+ # @return [ScenarioState]
1862
+ def set(state, transition_after_ms: nil, next_state: nil)
1863
+ payload = { 'state' => state }
1864
+ payload['transitionAfterMs'] = transition_after_ms unless transition_after_ms.nil?
1865
+ payload['nextState'] = next_state unless next_state.nil?
1866
+ result = @client.scenario_request('PUT', scenario_path, JSON.generate(payload))
1867
+ ScenarioState.from_hash(result)
1868
+ end
1869
+
1870
+ # PUT an external trigger advancing this scenario to +new_state+.
1871
+ #
1872
+ # @param new_state [String]
1873
+ # @return [ScenarioState]
1874
+ def trigger(new_state)
1875
+ body = JSON.generate({ 'newState' => new_state })
1876
+ result = @client.scenario_request('PUT', "#{scenario_path}/trigger", body)
1877
+ ScenarioState.from_hash(result)
1878
+ end
1879
+
1880
+ private
1881
+
1882
+ def scenario_path
1883
+ "/mockserver/scenario/#{MockServer.encode_path_segment(@name)}"
1884
+ end
1885
+ end
1886
+
1655
1887
  class Expectation
1656
1888
  attr_accessor :id, :priority, :percentage, :http_request, :http_response,
1657
1889
  :http_response_template, :http_response_class_callback,
@@ -1663,7 +1895,8 @@ module MockServer
1663
1895
  :grpc_stream_response, :grpc_bidi_response,
1664
1896
  :binary_response, :dns_response,
1665
1897
  :before_actions, :after_actions,
1666
- :http_responses, :response_mode, :steps,
1898
+ :http_responses, :response_mode, :response_weights, :switch_after,
1899
+ :cross_protocol_scenarios, :steps,
1667
1900
  :scenario_name, :scenario_state, :new_scenario_state
1668
1901
 
1669
1902
  def initialize(id: nil, priority: nil, percentage: nil, http_request: nil, http_response: nil,
@@ -1676,7 +1909,8 @@ module MockServer
1676
1909
  grpc_stream_response: nil, grpc_bidi_response: nil,
1677
1910
  binary_response: nil, dns_response: nil,
1678
1911
  before_actions: nil, after_actions: nil,
1679
- http_responses: nil, response_mode: nil, steps: nil,
1912
+ http_responses: nil, response_mode: nil, response_weights: nil,
1913
+ switch_after: nil, cross_protocol_scenarios: nil, steps: nil,
1680
1914
  scenario_name: nil, scenario_state: nil, new_scenario_state: nil)
1681
1915
  @id = id
1682
1916
  @priority = priority
@@ -1684,11 +1918,14 @@ module MockServer
1684
1918
  @http_request = http_request
1685
1919
  @http_response = http_response
1686
1920
  @http_response_template = http_response_template
1687
- @http_response_class_callback = http_response_class_callback
1921
+ # Route the two class-callback fields through their setters so a bare
1922
+ # class-name String passed to the constructor is coerced to an
1923
+ # HttpClassCallback (matching the setter / fluent-builder behaviour).
1924
+ self.http_response_class_callback = http_response_class_callback
1688
1925
  @http_response_object_callback = http_response_object_callback
1689
1926
  @http_forward = http_forward
1690
1927
  @http_forward_template = http_forward_template
1691
- @http_forward_class_callback = http_forward_class_callback
1928
+ self.http_forward_class_callback = http_forward_class_callback
1692
1929
  @http_forward_object_callback = http_forward_object_callback
1693
1930
  @http_override_forwarded_request = http_override_forwarded_request
1694
1931
  @http_error = http_error
@@ -1705,12 +1942,31 @@ module MockServer
1705
1942
  @after_actions = after_actions
1706
1943
  @http_responses = http_responses
1707
1944
  @response_mode = response_mode
1945
+ @response_weights = response_weights
1946
+ @switch_after = switch_after
1947
+ @cross_protocol_scenarios = cross_protocol_scenarios
1708
1948
  @steps = steps
1709
1949
  @scenario_name = scenario_name
1710
1950
  @scenario_state = scenario_state
1711
1951
  @new_scenario_state = new_scenario_state
1712
1952
  end
1713
1953
 
1954
+ # Set the response class-callback action. Accepts either a fully-qualified
1955
+ # class-name String (e.g. "com.example.MyResponseCallback") or a pre-built
1956
+ # {HttpClassCallback} (carrying an optional +delay+ / +primary+). A String is
1957
+ # wrapped into an {HttpClassCallback} so +to_h+ always emits
1958
+ # +httpResponseClassCallback.callbackClass+.
1959
+ def http_response_class_callback=(value)
1960
+ @http_response_class_callback = MockServer.coerce_class_callback(value)
1961
+ end
1962
+
1963
+ # Set the forward class-callback action. Accepts either a fully-qualified
1964
+ # class-name String or a pre-built {HttpClassCallback}; serialized as
1965
+ # +httpForwardClassCallback+.
1966
+ def http_forward_class_callback=(value)
1967
+ @http_forward_class_callback = MockServer.coerce_class_callback(value)
1968
+ end
1969
+
1714
1970
  def to_h
1715
1971
  before_actions_h = nil
1716
1972
  if @before_actions.is_a?(Array)
@@ -1751,6 +2007,9 @@ module MockServer
1751
2007
  'afterActions' => after_actions_h,
1752
2008
  'httpResponses' => @http_responses&.map(&:to_h),
1753
2009
  'responseMode' => @response_mode,
2010
+ 'responseWeights' => @response_weights,
2011
+ 'switchAfter' => @switch_after,
2012
+ 'crossProtocolScenarios' => @cross_protocol_scenarios&.map(&:to_h),
1754
2013
  'steps' => @steps&.map(&:to_h),
1755
2014
  'times' => @times&.to_h,
1756
2015
  'timeToLive' => @time_to_live&.to_h,
@@ -1803,6 +2062,9 @@ module MockServer
1803
2062
  after_actions: after_actions,
1804
2063
  http_responses: data['httpResponses']&.map { |r| HttpResponse.from_hash(r) },
1805
2064
  response_mode: data['responseMode'],
2065
+ response_weights: data['responseWeights'],
2066
+ switch_after: data['switchAfter'],
2067
+ cross_protocol_scenarios: data['crossProtocolScenarios']&.map { |c| CrossProtocolScenario.from_hash(c) },
1806
2068
  steps: data['steps']&.map { |s| ExpectationStep.from_hash(s) },
1807
2069
  times: Times.from_hash(data['times']),
1808
2070
  time_to_live: TimeToLive.from_hash(data['timeToLive']),
@@ -1910,12 +2172,13 @@ module MockServer
1910
2172
  end
1911
2173
 
1912
2174
  class Verification
1913
- attr_accessor :http_request, :expectation_id, :times,
2175
+ attr_accessor :http_request, :http_response, :expectation_id, :times,
1914
2176
  :maximum_number_of_request_to_return_in_verification_failure
1915
2177
 
1916
- def initialize(http_request: nil, expectation_id: nil, times: nil,
2178
+ def initialize(http_request: nil, http_response: nil, expectation_id: nil, times: nil,
1917
2179
  maximum_number_of_request_to_return_in_verification_failure: nil)
1918
2180
  @http_request = http_request
2181
+ @http_response = http_response
1919
2182
  @expectation_id = expectation_id
1920
2183
  @times = times
1921
2184
  @maximum_number_of_request_to_return_in_verification_failure = maximum_number_of_request_to_return_in_verification_failure
@@ -1924,6 +2187,7 @@ module MockServer
1924
2187
  def to_h
1925
2188
  MockServer.strip_none({
1926
2189
  'httpRequest' => @http_request&.to_h,
2190
+ 'httpResponse' => @http_response&.to_h,
1927
2191
  'expectationId' => @expectation_id&.to_h,
1928
2192
  'times' => @times&.to_h,
1929
2193
  'maximumNumberOfRequestToReturnInVerificationFailure' => @maximum_number_of_request_to_return_in_verification_failure
@@ -1935,6 +2199,7 @@ module MockServer
1935
2199
 
1936
2200
  new(
1937
2201
  http_request: HttpRequest.from_hash(data['httpRequest']),
2202
+ http_response: HttpResponse.from_hash(data['httpResponse']),
1938
2203
  expectation_id: ExpectationId.from_hash(data['expectationId']),
1939
2204
  times: VerificationTimes.from_hash(data['times']),
1940
2205
  maximum_number_of_request_to_return_in_verification_failure: data['maximumNumberOfRequestToReturnInVerificationFailure']
@@ -1943,16 +2208,18 @@ module MockServer
1943
2208
  end
1944
2209
 
1945
2210
  class VerificationSequence
1946
- attr_accessor :http_requests, :expectation_ids
2211
+ attr_accessor :http_requests, :http_responses, :expectation_ids
1947
2212
 
1948
- def initialize(http_requests: nil, expectation_ids: nil)
2213
+ def initialize(http_requests: nil, http_responses: nil, expectation_ids: nil)
1949
2214
  @http_requests = http_requests
2215
+ @http_responses = http_responses
1950
2216
  @expectation_ids = expectation_ids
1951
2217
  end
1952
2218
 
1953
2219
  def to_h
1954
2220
  MockServer.strip_none({
1955
2221
  'httpRequests' => @http_requests&.map(&:to_h),
2222
+ 'httpResponses' => @http_responses&.map(&:to_h),
1956
2223
  'expectationIds' => @expectation_ids&.map(&:to_h)
1957
2224
  })
1958
2225
  end
@@ -1961,9 +2228,11 @@ module MockServer
1961
2228
  return nil if data.nil?
1962
2229
 
1963
2230
  http_requests_data = data['httpRequests']
2231
+ http_responses_data = data['httpResponses']
1964
2232
  expectation_ids_data = data['expectationIds']
1965
2233
  new(
1966
- http_requests: http_requests_data&.map { |r| HttpRequest.from_hash(r) },
2234
+ http_requests: http_requests_data&.map { |r| HttpRequest.from_hash(r) },
2235
+ http_responses: http_responses_data&.map { |r| HttpResponse.from_hash(r) },
1967
2236
  expectation_ids: expectation_ids_data&.map { |e| ExpectationId.from_hash(e) }
1968
2237
  )
1969
2238
  end
@@ -2018,6 +2287,188 @@ module MockServer
2018
2287
  end
2019
2288
  end
2020
2289
 
2290
+ # One stage of a {LoadProfile}, run in sequence. Each stage holds or ramps a
2291
+ # setpoint for +duration_millis+:
2292
+ #
2293
+ # * +VU+ (closed model) - hold +vus+ virtual users, or ramp +start_vus+ to
2294
+ # +end_vus+ along +curve+.
2295
+ # * +RATE+ (open model) - hold +rate+ iterations/second, or ramp +start_rate+
2296
+ # to +end_rate+ along +curve+, optionally capping the auto-scaling
2297
+ # virtual-user pool at +max_vus+.
2298
+ # * +PAUSE+ - drive no load for +duration_millis+.
2299
+ #
2300
+ # Prefer the {.vu}, {.rate} and {.pause} factories, which emit only the fields
2301
+ # relevant to the stage type and mode.
2302
+ class LoadStage
2303
+ attr_accessor :type, :duration_millis, :curve, :vus, :start_vus, :end_vus,
2304
+ :rate, :start_rate, :end_rate, :max_vus
2305
+
2306
+ def initialize(type:, duration_millis:, curve: nil, vus: nil, start_vus: nil,
2307
+ end_vus: nil, rate: nil, start_rate: nil, end_rate: nil, max_vus: nil)
2308
+ @type = type
2309
+ @duration_millis = duration_millis
2310
+ @curve = curve
2311
+ @vus = vus
2312
+ @start_vus = start_vus
2313
+ @end_vus = end_vus
2314
+ @rate = rate
2315
+ @start_rate = start_rate
2316
+ @end_rate = end_rate
2317
+ @max_vus = max_vus
2318
+ end
2319
+
2320
+ # A +VU+ (closed-model) stage - hold +vus+ or ramp +start_vus+ to +end_vus+.
2321
+ def self.vu(duration_millis, vus: nil, start_vus: nil, end_vus: nil, curve: nil)
2322
+ new(type: 'VU', duration_millis: duration_millis, vus: vus,
2323
+ start_vus: start_vus, end_vus: end_vus, curve: curve)
2324
+ end
2325
+
2326
+ # A +RATE+ (open-model) stage - hold +rate+ or ramp +start_rate+ to
2327
+ # +end_rate+ (iterations/second).
2328
+ def self.rate(duration_millis, rate: nil, start_rate: nil, end_rate: nil, max_vus: nil, curve: nil)
2329
+ new(type: 'RATE', duration_millis: duration_millis, rate: rate,
2330
+ start_rate: start_rate, end_rate: end_rate, max_vus: max_vus, curve: curve)
2331
+ end
2332
+
2333
+ # A +PAUSE+ stage - drive no load for +duration_millis+.
2334
+ def self.pause(duration_millis)
2335
+ new(type: 'PAUSE', duration_millis: duration_millis)
2336
+ end
2337
+
2338
+ def to_h
2339
+ MockServer.strip_none({
2340
+ 'type' => @type,
2341
+ 'durationMillis' => @duration_millis,
2342
+ 'curve' => @curve,
2343
+ 'vus' => @vus,
2344
+ 'startVus' => @start_vus,
2345
+ 'endVus' => @end_vus,
2346
+ 'rate' => @rate,
2347
+ 'startRate' => @start_rate,
2348
+ 'endRate' => @end_rate,
2349
+ 'maxVus' => @max_vus
2350
+ })
2351
+ end
2352
+
2353
+ def self.from_hash(data)
2354
+ return nil if data.nil?
2355
+
2356
+ new(
2357
+ type: data['type'],
2358
+ duration_millis: data['durationMillis'],
2359
+ curve: data['curve'],
2360
+ vus: data['vus'],
2361
+ start_vus: data['startVus'],
2362
+ end_vus: data['endVus'],
2363
+ rate: data['rate'],
2364
+ start_rate: data['startRate'],
2365
+ end_rate: data['endRate'],
2366
+ max_vus: data['maxVus']
2367
+ )
2368
+ end
2369
+ end
2370
+
2371
+ # The traffic-shaping profile of a load scenario: an ordered list of {LoadStage}
2372
+ # objects run in sequence, each holding or ramping a setpoint (virtual users, an
2373
+ # arrival rate, or a pause) for its duration. The total run length is the sum of
2374
+ # the stage durations.
2375
+ class LoadProfile
2376
+ attr_accessor :stages
2377
+
2378
+ def initialize(stages: [])
2379
+ @stages = stages || []
2380
+ end
2381
+
2382
+ def to_h
2383
+ { 'stages' => @stages.map(&:to_h) }
2384
+ end
2385
+
2386
+ def self.from_hash(data)
2387
+ return nil if data.nil?
2388
+
2389
+ stages_data = data['stages'] || []
2390
+ new(stages: stages_data.map { |s| LoadStage.from_hash(s) })
2391
+ end
2392
+ end
2393
+
2394
+ # A single step within a load scenario. Each step fires +request+ (an HttpRequest)
2395
+ # against the target, optionally pausing for +think_time+ (a Delay) afterwards.
2396
+ class LoadStep
2397
+ attr_accessor :name, :labels, :think_time, :request
2398
+
2399
+ def initialize(request:, name: nil, labels: nil, think_time: nil)
2400
+ @request = request
2401
+ @name = name
2402
+ @labels = labels
2403
+ @think_time = think_time
2404
+ end
2405
+
2406
+ def to_h
2407
+ MockServer.strip_none({
2408
+ 'name' => @name,
2409
+ 'labels' => @labels,
2410
+ 'thinkTime' => @think_time&.to_h,
2411
+ 'request' => @request&.to_h
2412
+ })
2413
+ end
2414
+
2415
+ def self.from_hash(data)
2416
+ return nil if data.nil?
2417
+
2418
+ new(
2419
+ name: data['name'],
2420
+ labels: data['labels'],
2421
+ think_time: Delay.from_hash(data['thinkTime']),
2422
+ request: HttpRequest.from_hash(data['request'])
2423
+ )
2424
+ end
2425
+ end
2426
+
2427
+ # A load-injection scenario: a named set of +steps+ driven by a traffic +profile+.
2428
+ # +template_type+ selects the templating engine (+VELOCITY+ or +MUSTACHE+) used to
2429
+ # render step requests; +max_requests+ caps the total requests issued.
2430
+ class LoadScenario
2431
+ attr_accessor :name, :template_type, :labels, :max_requests, :start_delay_millis, :profile, :steps
2432
+
2433
+ def initialize(name:, profile:, steps:, template_type: nil, labels: nil, max_requests: nil,
2434
+ start_delay_millis: nil)
2435
+ @name = name
2436
+ @profile = profile
2437
+ @steps = steps
2438
+ @template_type = template_type
2439
+ @labels = labels
2440
+ @max_requests = max_requests
2441
+ @start_delay_millis = start_delay_millis
2442
+ end
2443
+
2444
+ def to_h
2445
+ MockServer.strip_none({
2446
+ 'name' => @name,
2447
+ 'templateType' => @template_type,
2448
+ 'labels' => @labels,
2449
+ 'maxRequests' => @max_requests,
2450
+ 'startDelayMillis' => @start_delay_millis,
2451
+ 'profile' => @profile&.to_h,
2452
+ 'steps' => @steps&.map(&:to_h)
2453
+ })
2454
+ end
2455
+
2456
+ def self.from_hash(data)
2457
+ return nil if data.nil?
2458
+
2459
+ steps_data = data['steps']
2460
+ new(
2461
+ name: data['name'],
2462
+ template_type: data['templateType'],
2463
+ labels: data['labels'],
2464
+ max_requests: data['maxRequests'],
2465
+ start_delay_millis: data['startDelayMillis'],
2466
+ profile: LoadProfile.from_hash(data['profile']),
2467
+ steps: steps_data ? steps_data.map { |s| LoadStep.from_hash(s) } : nil
2468
+ )
2469
+ end
2470
+ end
2471
+
2021
2472
  # Alias matching the Python client
2022
2473
  RequestDefinition = HttpRequest
2023
2474
  end