mockserver-client 7.1.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',
@@ -84,7 +86,16 @@ module MockServer
84
86
  'http_class_callback' => 'httpClassCallback',
85
87
  'http_object_callback' => 'httpObjectCallback',
86
88
  'failure_policy' => 'failurePolicy',
87
- 'http_responses' => 'httpResponses'
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'
88
99
  }.freeze
89
100
 
90
101
  REVERSE_FIELD_MAP = FIELD_MAP.invert.freeze
@@ -119,6 +130,30 @@ module MockServer
119
130
  hash.reject { |_k, v| v.nil? }
120
131
  end
121
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
+
122
157
  # @api private
123
158
  def self.serialize_value(value)
124
159
  case value
@@ -188,6 +223,29 @@ module MockServer
188
223
  end
189
224
  end
190
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
+
191
249
  # -------------------------------------------------------------------
192
250
  # Model classes
193
251
  # -------------------------------------------------------------------
@@ -564,7 +622,7 @@ module MockServer
564
622
  'path' => @path,
565
623
  'queryStringParameters' => MockServer.serialize_key_multi_values(@query_string_parameters),
566
624
  'headers' => MockServer.serialize_key_multi_values(@headers),
567
- 'cookies' => MockServer.serialize_key_multi_values(@cookies),
625
+ 'cookies' => MockServer.serialize_cookies(@cookies),
568
626
  'body' => MockServer.serialize_body(@body),
569
627
  'secure' => @secure,
570
628
  'keepAlive' => @keep_alive,
@@ -582,7 +640,7 @@ module MockServer
582
640
  path: data['path'],
583
641
  query_string_parameters: MockServer.deserialize_key_multi_values(data['queryStringParameters']),
584
642
  headers: MockServer.deserialize_key_multi_values(data['headers']),
585
- cookies: MockServer.deserialize_key_multi_values(data['cookies']),
643
+ cookies: MockServer.deserialize_cookies(data['cookies']),
586
644
  body: MockServer.deserialize_body(data['body']),
587
645
  secure: data['secure'],
588
646
  keep_alive: data['keepAlive'],
@@ -714,7 +772,7 @@ module MockServer
714
772
  'statusCode' => @status_code,
715
773
  'reasonPhrase' => @reason_phrase,
716
774
  'headers' => MockServer.serialize_key_multi_values(@headers),
717
- 'cookies' => MockServer.serialize_key_multi_values(@cookies),
775
+ 'cookies' => MockServer.serialize_cookies(@cookies),
718
776
  'body' => MockServer.serialize_body(@body),
719
777
  'delay' => @delay&.to_h,
720
778
  'connectionOptions' => @connection_options&.to_h,
@@ -729,7 +787,7 @@ module MockServer
729
787
  status_code: data['statusCode'],
730
788
  reason_phrase: data['reasonPhrase'],
731
789
  headers: MockServer.deserialize_key_multi_values(data['headers']),
732
- cookies: MockServer.deserialize_key_multi_values(data['cookies']),
790
+ cookies: MockServer.deserialize_cookies(data['cookies']),
733
791
  body: MockServer.deserialize_body(data['body']),
734
792
  delay: Delay.from_hash(data['delay']),
735
793
  connection_options: ConnectionOptions.from_hash(data['connectionOptions']),
@@ -935,11 +993,15 @@ module MockServer
935
993
  end
936
994
 
937
995
  class HttpError
938
- 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
939
1000
 
940
- 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)
941
1002
  @drop_connection = drop_connection
942
1003
  @response_bytes = response_bytes
1004
+ @stream_error = stream_error
943
1005
  @delay = delay
944
1006
  @primary = primary
945
1007
  end
@@ -948,6 +1010,7 @@ module MockServer
948
1010
  MockServer.strip_none({
949
1011
  'dropConnection' => @drop_connection,
950
1012
  'responseBytes' => @response_bytes,
1013
+ 'streamError' => @stream_error,
951
1014
  'delay' => @delay&.to_h,
952
1015
  'primary' => @primary
953
1016
  })
@@ -959,6 +1022,7 @@ module MockServer
959
1022
  new(
960
1023
  drop_connection: data['dropConnection'],
961
1024
  response_bytes: data['responseBytes'],
1025
+ stream_error: data['streamError'],
962
1026
  delay: Delay.from_hash(data['delay']),
963
1027
  primary: data['primary']
964
1028
  )
@@ -1680,6 +1744,146 @@ module MockServer
1680
1744
  end
1681
1745
  end
1682
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
+
1683
1887
  class Expectation
1684
1888
  attr_accessor :id, :priority, :percentage, :http_request, :http_response,
1685
1889
  :http_response_template, :http_response_class_callback,
@@ -1691,7 +1895,8 @@ module MockServer
1691
1895
  :grpc_stream_response, :grpc_bidi_response,
1692
1896
  :binary_response, :dns_response,
1693
1897
  :before_actions, :after_actions,
1694
- :http_responses, :response_mode, :steps,
1898
+ :http_responses, :response_mode, :response_weights, :switch_after,
1899
+ :cross_protocol_scenarios, :steps,
1695
1900
  :scenario_name, :scenario_state, :new_scenario_state
1696
1901
 
1697
1902
  def initialize(id: nil, priority: nil, percentage: nil, http_request: nil, http_response: nil,
@@ -1704,7 +1909,8 @@ module MockServer
1704
1909
  grpc_stream_response: nil, grpc_bidi_response: nil,
1705
1910
  binary_response: nil, dns_response: nil,
1706
1911
  before_actions: nil, after_actions: nil,
1707
- 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,
1708
1914
  scenario_name: nil, scenario_state: nil, new_scenario_state: nil)
1709
1915
  @id = id
1710
1916
  @priority = priority
@@ -1712,11 +1918,14 @@ module MockServer
1712
1918
  @http_request = http_request
1713
1919
  @http_response = http_response
1714
1920
  @http_response_template = http_response_template
1715
- @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
1716
1925
  @http_response_object_callback = http_response_object_callback
1717
1926
  @http_forward = http_forward
1718
1927
  @http_forward_template = http_forward_template
1719
- @http_forward_class_callback = http_forward_class_callback
1928
+ self.http_forward_class_callback = http_forward_class_callback
1720
1929
  @http_forward_object_callback = http_forward_object_callback
1721
1930
  @http_override_forwarded_request = http_override_forwarded_request
1722
1931
  @http_error = http_error
@@ -1733,12 +1942,31 @@ module MockServer
1733
1942
  @after_actions = after_actions
1734
1943
  @http_responses = http_responses
1735
1944
  @response_mode = response_mode
1945
+ @response_weights = response_weights
1946
+ @switch_after = switch_after
1947
+ @cross_protocol_scenarios = cross_protocol_scenarios
1736
1948
  @steps = steps
1737
1949
  @scenario_name = scenario_name
1738
1950
  @scenario_state = scenario_state
1739
1951
  @new_scenario_state = new_scenario_state
1740
1952
  end
1741
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
+
1742
1970
  def to_h
1743
1971
  before_actions_h = nil
1744
1972
  if @before_actions.is_a?(Array)
@@ -1779,6 +2007,9 @@ module MockServer
1779
2007
  'afterActions' => after_actions_h,
1780
2008
  'httpResponses' => @http_responses&.map(&:to_h),
1781
2009
  'responseMode' => @response_mode,
2010
+ 'responseWeights' => @response_weights,
2011
+ 'switchAfter' => @switch_after,
2012
+ 'crossProtocolScenarios' => @cross_protocol_scenarios&.map(&:to_h),
1782
2013
  'steps' => @steps&.map(&:to_h),
1783
2014
  'times' => @times&.to_h,
1784
2015
  'timeToLive' => @time_to_live&.to_h,
@@ -1831,6 +2062,9 @@ module MockServer
1831
2062
  after_actions: after_actions,
1832
2063
  http_responses: data['httpResponses']&.map { |r| HttpResponse.from_hash(r) },
1833
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) },
1834
2068
  steps: data['steps']&.map { |s| ExpectationStep.from_hash(s) },
1835
2069
  times: Times.from_hash(data['times']),
1836
2070
  time_to_live: TimeToLive.from_hash(data['timeToLive']),
@@ -2053,6 +2287,188 @@ module MockServer
2053
2287
  end
2054
2288
  end
2055
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
+
2056
2472
  # Alias matching the Python client
2057
2473
  RequestDefinition = HttpRequest
2058
2474
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client'
4
+
5
+ module MockServer
6
+ # RSpec integration helpers for MockServer.
7
+ #
8
+ # Require this file from your `spec_helper.rb` to get a shared context that
9
+ # provides a fresh {MockServer::Client} per example and automatically resets
10
+ # the server between examples, so recorded requests, expectations and logs
11
+ # never leak from one example to the next:
12
+ #
13
+ # require 'mockserver/rspec'
14
+ #
15
+ # RSpec.describe 'my integration', :mockserver do
16
+ # it 'records the request' do
17
+ # # `mockserver` is the shared, reset client
18
+ # mockserver.when(MockServer::HttpRequest.request(path: '/hello'))
19
+ # .respond(MockServer::HttpResponse.response(body: 'world'))
20
+ # end
21
+ # end
22
+ #
23
+ # The host and port default to `127.0.0.1:1080` and can be overridden with the
24
+ # `MOCKSERVER_HOST` / `MOCKSERVER_PORT` environment variables, or by defining a
25
+ # `mockserver_host` / `mockserver_port` `let` in your example group.
26
+ module RSpec
27
+ SHARED_CONTEXT_NAME = 'mockserver client'
28
+
29
+ if defined?(::RSpec)
30
+ ::RSpec.shared_context SHARED_CONTEXT_NAME do
31
+ let(:mockserver_host) { ENV.fetch('MOCKSERVER_HOST', '127.0.0.1') }
32
+ let(:mockserver_port) { Integer(ENV.fetch('MOCKSERVER_PORT', '1080')) }
33
+
34
+ # A fresh client per example. Memoised so the same instance is returned
35
+ # within a single example, but rebuilt for the next one.
36
+ let(:mockserver) { MockServer::Client.new(mockserver_host, mockserver_port) }
37
+
38
+ # Start each example from a clean server and tear down again afterwards,
39
+ # closing any websocket connections opened by the client.
40
+ before do
41
+ mockserver.reset
42
+ end
43
+
44
+ after do
45
+ mockserver.reset
46
+ end
47
+ end
48
+
49
+ # Make the shared context available to any example group tagged
50
+ # `:mockserver` without an explicit `include_context`.
51
+ ::RSpec.configure do |config|
52
+ config.include_context SHARED_CONTEXT_NAME, :mockserver
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MockServer
4
- VERSION = '7.1.0'
4
+ VERSION = '7.2.0'
5
5
  end
@@ -6,4 +6,6 @@ require_relative 'mockserver/models'
6
6
  require_relative 'mockserver/websocket_client'
7
7
  require_relative 'mockserver/forward_chain_expectation'
8
8
  require_relative 'mockserver/client'
9
+ require_relative 'mockserver/llm'
10
+ require_relative 'mockserver/mcp'
9
11
  require_relative 'mockserver/binary_launcher'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mockserver-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.0
4
+ version: 7.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Bloom
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-15 00:00:00.000000000 Z
11
+ date: 2026-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger
@@ -95,7 +95,10 @@ files:
95
95
  - lib/mockserver/client.rb
96
96
  - lib/mockserver/errors.rb
97
97
  - lib/mockserver/forward_chain_expectation.rb
98
+ - lib/mockserver/llm.rb
99
+ - lib/mockserver/mcp.rb
98
100
  - lib/mockserver/models.rb
101
+ - lib/mockserver/rspec.rb
99
102
  - lib/mockserver/version.rb
100
103
  - lib/mockserver/websocket_client.rb
101
104
  - mockserver-client.gemspec