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.
- checksums.yaml +4 -4
- data/README.md +216 -0
- data/lib/mockserver/binary_launcher.rb +664 -0
- data/lib/mockserver/client.rb +462 -13
- data/lib/mockserver/forward_chain_expectation.rb +16 -6
- data/lib/mockserver/llm.rb +855 -0
- data/lib/mockserver/mcp.rb +453 -0
- data/lib/mockserver/models.rb +488 -37
- data/lib/mockserver/rspec.rb +56 -0
- data/lib/mockserver/version.rb +1 -1
- data/lib/mockserver/websocket_client.rb +129 -2
- data/lib/mockserver-client.rb +3 -0
- metadata +6 -2
data/lib/mockserver/models.rb
CHANGED
|
@@ -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,
|
|
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']
|
|
377
|
-
result['string']
|
|
378
|
-
result['json']
|
|
379
|
-
result['base64Bytes']
|
|
380
|
-
result['not']
|
|
381
|
-
result['contentType']
|
|
382
|
-
result['charset']
|
|
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:
|
|
391
|
-
string:
|
|
392
|
-
json:
|
|
393
|
-
base64_bytes:
|
|
394
|
-
not_body:
|
|
395
|
-
content_type:
|
|
396
|
-
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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, :
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|