mockserver-client 9.1.0 → 9.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 +102 -1
- data/lib/mockserver/client.rb +131 -13
- data/lib/mockserver/forward_chain_expectation.rb +8 -0
- data/lib/mockserver/models.rb +543 -43
- data/lib/mockserver/version.rb +1 -1
- data/lib/mockserver/websocket_client.rb +5 -1
- metadata +2 -2
data/lib/mockserver/models.rb
CHANGED
|
@@ -13,6 +13,7 @@ module MockServer
|
|
|
13
13
|
'reason_phrase' => 'reasonPhrase',
|
|
14
14
|
'keep_alive' => 'keepAlive',
|
|
15
15
|
'respond_before_body' => 'respondBeforeBody',
|
|
16
|
+
'client_ip' => 'clientIp',
|
|
16
17
|
'query_string_parameters' => 'queryStringParameters',
|
|
17
18
|
'path_parameters' => 'pathParameters',
|
|
18
19
|
'socket_address' => 'socketAddress',
|
|
@@ -32,6 +33,7 @@ module MockServer
|
|
|
32
33
|
'drop_connection' => 'dropConnection',
|
|
33
34
|
'response_bytes' => 'responseBytes',
|
|
34
35
|
'stream_error' => 'streamError',
|
|
36
|
+
'reset_connection' => 'resetConnection',
|
|
35
37
|
'http_request' => 'httpRequest',
|
|
36
38
|
'http_response' => 'httpResponse',
|
|
37
39
|
'http_response_template' => 'httpResponseTemplate',
|
|
@@ -48,6 +50,7 @@ module MockServer
|
|
|
48
50
|
'template_type' => 'templateType',
|
|
49
51
|
'template_file' => 'templateFile',
|
|
50
52
|
'file_path' => 'filePath',
|
|
53
|
+
'templated_file_path' => 'templatedFilePath',
|
|
51
54
|
'base64_bytes' => 'base64Bytes',
|
|
52
55
|
'not_body' => 'not',
|
|
53
56
|
'content_type' => 'contentType',
|
|
@@ -85,6 +88,7 @@ module MockServer
|
|
|
85
88
|
'degradation_ramp_millis' => 'degradationRampMillis',
|
|
86
89
|
'http_class_callback' => 'httpClassCallback',
|
|
87
90
|
'http_object_callback' => 'httpObjectCallback',
|
|
91
|
+
'http_request_template' => 'httpRequestTemplate',
|
|
88
92
|
'failure_policy' => 'failurePolicy',
|
|
89
93
|
'http_responses' => 'httpResponses',
|
|
90
94
|
'response_mode' => 'responseMode',
|
|
@@ -242,7 +246,7 @@ module MockServer
|
|
|
242
246
|
def self.serialize_key_multi_values(items, key_match_style = nil)
|
|
243
247
|
if key_match_style
|
|
244
248
|
map = { 'keyMatchStyle' => key_match_style }
|
|
245
|
-
(items || []).each { |item| (map[item.name] ||= []).concat(Array(item.values)) }
|
|
249
|
+
(items || []).each { |item| (map[item.name] ||= []).concat(serialize_matcher_values(Array(item.values))) }
|
|
246
250
|
return map
|
|
247
251
|
end
|
|
248
252
|
return nil if items.nil?
|
|
@@ -251,10 +255,11 @@ module MockServer
|
|
|
251
255
|
end
|
|
252
256
|
|
|
253
257
|
# @api private
|
|
254
|
-
# An httpRequest holding
|
|
255
|
-
#
|
|
256
|
-
# DNS) request matcher.
|
|
258
|
+
# An httpRequest holding anyOf is an anyOf matcher, one holding if a
|
|
259
|
+
# conditional matcher, one naming an OpenAPI spec (specUrlOrPayload) an
|
|
260
|
+
# OpenAPI matcher; anything else is an HTTP (or DNS) request matcher.
|
|
257
261
|
def self.deserialize_request_definition(data)
|
|
262
|
+
return AnyOfRequestDefinition.from_hash(data) if data.is_a?(Hash) && data.key?('anyOf')
|
|
258
263
|
return ConditionalRequestDefinition.from_hash(data) if data.is_a?(Hash) && data.key?('if')
|
|
259
264
|
return OpenAPIDefinition.from_hash(data) if data.is_a?(Hash) && data.key?('specUrlOrPayload')
|
|
260
265
|
|
|
@@ -289,10 +294,43 @@ module MockServer
|
|
|
289
294
|
next item unless item.is_a?(Hash)
|
|
290
295
|
|
|
291
296
|
item = stringify_keys(item)
|
|
292
|
-
|
|
297
|
+
values = item.key?('value') ? [item['value']] : item.fetch('values', [])
|
|
298
|
+
KeyToMultiValue.new(name: item.fetch('name', ''), values: deserialize_matcher_values(values))
|
|
293
299
|
end
|
|
294
300
|
end
|
|
295
301
|
|
|
302
|
+
# @api private
|
|
303
|
+
# A header, query, path-parameter or cookie value is a plain String, the
|
|
304
|
+
# nottable / schema object form (kept as a Hash) or a DateTimeMatcher. Only a
|
|
305
|
+
# Hash holding exactly one date-time operator and nothing but date-time keys
|
|
306
|
+
# becomes a DateTimeMatcher, so any other object form round-trips unchanged.
|
|
307
|
+
def self.date_time_matcher_hash?(value)
|
|
308
|
+
return false unless value.is_a?(Hash)
|
|
309
|
+
|
|
310
|
+
keys = value.keys.map(&:to_s)
|
|
311
|
+
(keys & DateTimeMatcher::OPERATOR_FIELDS).length == 1 && (keys - DateTimeMatcher::FIELDS).empty?
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# @api private
|
|
315
|
+
def self.deserialize_matcher_value(value)
|
|
316
|
+
date_time_matcher_hash?(value) ? DateTimeMatcher.from_hash(stringify_keys(value)) : value
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# @api private
|
|
320
|
+
def self.deserialize_matcher_values(values)
|
|
321
|
+
values.is_a?(Array) ? values.map { |v| deserialize_matcher_value(v) } : deserialize_matcher_value(values)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# @api private
|
|
325
|
+
def self.serialize_matcher_value(value)
|
|
326
|
+
value.is_a?(DateTimeMatcher) ? value.to_h : value
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# @api private
|
|
330
|
+
def self.serialize_matcher_values(values)
|
|
331
|
+
values.is_a?(Array) ? values.map { |v| serialize_matcher_value(v) } : serialize_matcher_value(values)
|
|
332
|
+
end
|
|
333
|
+
|
|
296
334
|
# @api private
|
|
297
335
|
# Serialise a keyToMultiValue collection in the { name => [values] } object form
|
|
298
336
|
# (rather than the [{name, values}] array form). MockServer accepts both wire
|
|
@@ -302,7 +340,7 @@ module MockServer
|
|
|
302
340
|
def self.serialize_key_multi_values_object(items)
|
|
303
341
|
return nil if items.nil?
|
|
304
342
|
|
|
305
|
-
items.each_with_object({}) { |item, map| map[item.name] = item.values }
|
|
343
|
+
items.each_with_object({}) { |item, map| map[item.name] = serialize_matcher_values(item.values) }
|
|
306
344
|
end
|
|
307
345
|
|
|
308
346
|
# @api private
|
|
@@ -311,7 +349,7 @@ module MockServer
|
|
|
311
349
|
|
|
312
350
|
if data.is_a?(Hash)
|
|
313
351
|
return data.reject { |k, _v| k == 'keyMatchStyle' }
|
|
314
|
-
.map { |k, v| KeyToMultiValue.new(name: k, values: v.is_a?(Array) ? v : [v]) }
|
|
352
|
+
.map { |k, v| KeyToMultiValue.new(name: k, values: deserialize_matcher_values(v.is_a?(Array) ? v : [v])) }
|
|
315
353
|
end
|
|
316
354
|
|
|
317
355
|
data.map do |item|
|
|
@@ -332,7 +370,7 @@ module MockServer
|
|
|
332
370
|
return nil if items.nil?
|
|
333
371
|
|
|
334
372
|
items.each_with_object({}) do |item, map|
|
|
335
|
-
map[item.name] = item.values.is_a?(Array) ? item.values.first : item.values
|
|
373
|
+
map[item.name] = serialize_matcher_value(item.values.is_a?(Array) ? item.values.first : item.values)
|
|
336
374
|
end
|
|
337
375
|
end
|
|
338
376
|
|
|
@@ -341,7 +379,7 @@ module MockServer
|
|
|
341
379
|
return nil if data.nil?
|
|
342
380
|
|
|
343
381
|
if data.is_a?(Hash)
|
|
344
|
-
return data.map { |k, v| KeyToMultiValue.new(name: k, values: v.is_a?(Array) ? v : [v]) }
|
|
382
|
+
return data.map { |k, v| KeyToMultiValue.new(name: k, values: deserialize_matcher_values(v.is_a?(Array) ? v : [v])) }
|
|
345
383
|
end
|
|
346
384
|
|
|
347
385
|
# tolerate the legacy array form on read
|
|
@@ -515,7 +553,7 @@ module MockServer
|
|
|
515
553
|
def to_h
|
|
516
554
|
{
|
|
517
555
|
'name' => @name,
|
|
518
|
-
'values' => @values
|
|
556
|
+
'values' => MockServer.serialize_matcher_values(@values)
|
|
519
557
|
}
|
|
520
558
|
end
|
|
521
559
|
|
|
@@ -524,11 +562,136 @@ module MockServer
|
|
|
524
562
|
|
|
525
563
|
new(
|
|
526
564
|
name: data.fetch('name', ''),
|
|
527
|
-
values: data.fetch('values', [])
|
|
565
|
+
values: MockServer.deserialize_matcher_values(data.fetch('values', []))
|
|
528
566
|
)
|
|
529
567
|
end
|
|
530
568
|
end
|
|
531
569
|
|
|
570
|
+
# A date-time matcher for a header, query-string parameter, path parameter or
|
|
571
|
+
# cookie value, written on the wire as an object such as
|
|
572
|
+
# { "after": "now -1 hours", "actualFormat": "rfc1123" }. Instances are frozen;
|
|
573
|
+
# every with_* method returns a new matcher.
|
|
574
|
+
#
|
|
575
|
+
# request.with_header('X-Issued-At', MockServer::DateTimeMatcher.after('now -1 hours'))
|
|
576
|
+
class DateTimeMatcher
|
|
577
|
+
OPERATOR_FIELDS = %w[before after equalToDateTime].freeze
|
|
578
|
+
FIELDS = (%w[not] + OPERATOR_FIELDS +
|
|
579
|
+
%w[actualFormat truncateExpected truncateActual applyTruncationLast zone]).freeze
|
|
580
|
+
|
|
581
|
+
# +operator+ is the wire field name: 'before', 'after' or 'equalToDateTime'.
|
|
582
|
+
attr_reader :operator, :expected, :actual_format, :truncate_expected, :truncate_actual,
|
|
583
|
+
:apply_truncation_last, :zone, :not_matcher
|
|
584
|
+
|
|
585
|
+
def initialize(operator:, expected:, actual_format: nil, truncate_expected: nil, truncate_actual: nil,
|
|
586
|
+
apply_truncation_last: nil, zone: nil, not_matcher: nil)
|
|
587
|
+
unless operator.nil? || OPERATOR_FIELDS.include?(operator)
|
|
588
|
+
raise ArgumentError, "Unknown date-time operator #{operator.inspect}; expected one of #{OPERATOR_FIELDS.join(', ')}"
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
@operator = operator
|
|
592
|
+
@expected = expected
|
|
593
|
+
@actual_format = actual_format
|
|
594
|
+
@truncate_expected = truncate_expected
|
|
595
|
+
@truncate_actual = truncate_actual
|
|
596
|
+
@apply_truncation_last = apply_truncation_last
|
|
597
|
+
@zone = zone
|
|
598
|
+
@not_matcher = not_matcher
|
|
599
|
+
freeze
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def self.before(expected)
|
|
603
|
+
new(operator: 'before', expected: expected)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def self.after(expected)
|
|
607
|
+
new(operator: 'after', expected: expected)
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def self.equal_to_date_time(expected)
|
|
611
|
+
new(operator: 'equalToDateTime', expected: expected)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def with_actual_format(actual_format)
|
|
615
|
+
copy(actual_format: actual_format)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def with_truncate_expected(truncate_expected)
|
|
619
|
+
copy(truncate_expected: truncate_expected)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def with_truncate_actual(truncate_actual)
|
|
623
|
+
copy(truncate_actual: truncate_actual)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def with_apply_truncation_last(apply_truncation_last)
|
|
627
|
+
copy(apply_truncation_last: apply_truncation_last)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def with_zone(zone)
|
|
631
|
+
copy(zone: zone)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def with_not(not_matcher)
|
|
635
|
+
copy(not_matcher: not_matcher)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Field order and omission follow the server: not only when true, every
|
|
639
|
+
# other optional field only when set.
|
|
640
|
+
def to_h
|
|
641
|
+
result = {}
|
|
642
|
+
result['not'] = true if @not_matcher == true
|
|
643
|
+
result[@operator] = @expected unless @operator.nil?
|
|
644
|
+
result['actualFormat'] = @actual_format unless @actual_format.nil?
|
|
645
|
+
result['truncateExpected'] = @truncate_expected unless @truncate_expected.nil?
|
|
646
|
+
result['truncateActual'] = @truncate_actual unless @truncate_actual.nil?
|
|
647
|
+
result['applyTruncationLast'] = @apply_truncation_last unless @apply_truncation_last.nil?
|
|
648
|
+
result['zone'] = @zone unless @zone.nil?
|
|
649
|
+
result
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def to_json(*args)
|
|
653
|
+
to_h.to_json(*args)
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def ==(other)
|
|
657
|
+
other.is_a?(DateTimeMatcher) && other.to_h == to_h
|
|
658
|
+
end
|
|
659
|
+
alias eql? ==
|
|
660
|
+
|
|
661
|
+
def hash
|
|
662
|
+
to_h.hash
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def self.from_hash(data)
|
|
666
|
+
return nil if data.nil?
|
|
667
|
+
|
|
668
|
+
operators = OPERATOR_FIELDS.select { |field| data.key?(field) }
|
|
669
|
+
if operators.length > 1
|
|
670
|
+
raise ArgumentError, "A date-time matcher takes one of #{OPERATOR_FIELDS.join(', ')}, got #{operators.join(', ')}"
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
new(
|
|
674
|
+
operator: operators.first,
|
|
675
|
+
expected: operators.first && data[operators.first],
|
|
676
|
+
actual_format: data['actualFormat'],
|
|
677
|
+
truncate_expected: data['truncateExpected'],
|
|
678
|
+
truncate_actual: data['truncateActual'],
|
|
679
|
+
apply_truncation_last: data['applyTruncationLast'],
|
|
680
|
+
zone: data['zone'],
|
|
681
|
+
not_matcher: data['not']
|
|
682
|
+
)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
private
|
|
686
|
+
|
|
687
|
+
def copy(**changes)
|
|
688
|
+
fields = { operator: @operator, expected: @expected, actual_format: @actual_format,
|
|
689
|
+
truncate_expected: @truncate_expected, truncate_actual: @truncate_actual,
|
|
690
|
+
apply_truncation_last: @apply_truncation_last, zone: @zone, not_matcher: @not_matcher }
|
|
691
|
+
self.class.new(**fields.merge(changes))
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
532
695
|
class Body
|
|
533
696
|
# sub_string / match_type / match_numbers_as_strings / json_schema /
|
|
534
697
|
# parameter_styles / xml_schema / parameters / module_name / optional are the
|
|
@@ -540,16 +703,19 @@ module MockServer
|
|
|
540
703
|
# from +string+: the legacy Body.xml(...) factory populates +string+ (pinned by a
|
|
541
704
|
# test), whereas a server-produced XML body serialises its content under +xml+.
|
|
542
705
|
attr_accessor :type, :string, :xml, :json, :regex, :json_path, :xpath, :base64_bytes,
|
|
543
|
-
:not_body, :content_type, :charset, :file_path, :template_type, :body_all_of,
|
|
706
|
+
:not_body, :content_type, :charset, :file_path, :template_type, :templated_file_path, :body_all_of,
|
|
544
707
|
:sub_string, :match_type, :match_numbers_as_strings, :json_schema,
|
|
545
|
-
:parameter_styles, :xml_schema, :parameters, :module_name, :optional
|
|
708
|
+
:parameter_styles, :xml_schema, :parameters, :module_name, :optional, :date_time
|
|
546
709
|
|
|
710
|
+
# +date_time+ (a DateTimeMatcher) applies to a JSON_PATH or XPATH body: it then
|
|
711
|
+
# matches only when a value the JSONPath selects, or the text of a node the
|
|
712
|
+
# XPath selects, is a date-time satisfying it.
|
|
547
713
|
def initialize(type: nil, string: nil, xml: nil, json: nil, regex: nil, json_path: nil, xpath: nil,
|
|
548
714
|
base64_bytes: nil, not_body: nil, content_type: nil, charset: nil,
|
|
549
|
-
file_path: nil, template_type: nil, body_all_of: nil,
|
|
715
|
+
file_path: nil, template_type: nil, templated_file_path: nil, body_all_of: nil,
|
|
550
716
|
sub_string: nil, match_type: nil, match_numbers_as_strings: nil,
|
|
551
717
|
json_schema: nil, parameter_styles: nil, xml_schema: nil,
|
|
552
|
-
parameters: nil, module_name: nil, optional: nil)
|
|
718
|
+
parameters: nil, module_name: nil, optional: nil, date_time: nil)
|
|
553
719
|
@type = type
|
|
554
720
|
@string = string
|
|
555
721
|
@xml = xml
|
|
@@ -563,6 +729,7 @@ module MockServer
|
|
|
563
729
|
@charset = charset
|
|
564
730
|
@file_path = file_path
|
|
565
731
|
@template_type = template_type
|
|
732
|
+
@templated_file_path = templated_file_path
|
|
566
733
|
@body_all_of = body_all_of
|
|
567
734
|
@sub_string = sub_string
|
|
568
735
|
@match_type = match_type
|
|
@@ -573,6 +740,7 @@ module MockServer
|
|
|
573
740
|
@parameters = parameters
|
|
574
741
|
@module_name = module_name
|
|
575
742
|
@optional = optional
|
|
743
|
+
@date_time = date_time
|
|
576
744
|
end
|
|
577
745
|
|
|
578
746
|
def to_h
|
|
@@ -584,12 +752,15 @@ module MockServer
|
|
|
584
752
|
result['regex'] = @regex unless @regex.nil?
|
|
585
753
|
result['jsonPath'] = @json_path unless @json_path.nil?
|
|
586
754
|
result['xpath'] = @xpath unless @xpath.nil?
|
|
755
|
+
result['dateTime'] = MockServer.serialize_matcher_value(@date_time) unless @date_time.nil?
|
|
587
756
|
result['base64Bytes'] = @base64_bytes unless @base64_bytes.nil?
|
|
588
757
|
result['not'] = @not_body unless @not_body.nil?
|
|
589
758
|
result['contentType'] = @content_type unless @content_type.nil?
|
|
590
759
|
result['charset'] = @charset unless @charset.nil?
|
|
591
760
|
result['filePath'] = @file_path unless @file_path.nil?
|
|
592
761
|
result['templateType'] = @template_type unless @template_type.nil?
|
|
762
|
+
# FILE body: render filePath as a template; omitted unless true.
|
|
763
|
+
result['templatedFilePath'] = true if @templated_file_path == true
|
|
593
764
|
result['subString'] = @sub_string unless @sub_string.nil?
|
|
594
765
|
result['matchType'] = @match_type unless @match_type.nil?
|
|
595
766
|
result['matchNumbersAsStrings'] = @match_numbers_as_strings unless @match_numbers_as_strings.nil?
|
|
@@ -623,6 +794,7 @@ module MockServer
|
|
|
623
794
|
charset: data['charset'],
|
|
624
795
|
file_path: data['filePath'],
|
|
625
796
|
template_type: data['templateType'],
|
|
797
|
+
templated_file_path: data['templatedFilePath'],
|
|
626
798
|
sub_string: data['subString'],
|
|
627
799
|
match_type: data['matchType'],
|
|
628
800
|
match_numbers_as_strings: data['matchNumbersAsStrings'],
|
|
@@ -632,6 +804,7 @@ module MockServer
|
|
|
632
804
|
parameters: data['parameters'],
|
|
633
805
|
module_name: data['moduleName'],
|
|
634
806
|
optional: data['optional'],
|
|
807
|
+
date_time: MockServer.deserialize_matcher_value(data['dateTime']),
|
|
635
808
|
body_all_of: all_of&.map { |b| MockServer.deserialize_body(b) }
|
|
636
809
|
)
|
|
637
810
|
end
|
|
@@ -664,12 +837,12 @@ module MockServer
|
|
|
664
837
|
new(type: 'ALL_OF', body_all_of: bodies.flatten)
|
|
665
838
|
end
|
|
666
839
|
|
|
667
|
-
def self.json_path(value)
|
|
668
|
-
new(type: 'JSON_PATH', json_path: value)
|
|
840
|
+
def self.json_path(value, date_time: nil)
|
|
841
|
+
new(type: 'JSON_PATH', json_path: value, date_time: date_time)
|
|
669
842
|
end
|
|
670
843
|
|
|
671
|
-
def self.xpath(value)
|
|
672
|
-
new(type: 'XPATH', xpath: value)
|
|
844
|
+
def self.xpath(value, date_time: nil)
|
|
845
|
+
new(type: 'XPATH', xpath: value, date_time: date_time)
|
|
673
846
|
end
|
|
674
847
|
|
|
675
848
|
# A JSON_SCHEMA body matcher. +parameter_styles+ is an optional
|
|
@@ -700,8 +873,11 @@ module MockServer
|
|
|
700
873
|
new(type: 'WASM', module_name: module_name)
|
|
701
874
|
end
|
|
702
875
|
|
|
703
|
-
|
|
704
|
-
|
|
876
|
+
# A FILE body. +templated_file_path: true+ (with a +template_type+) also renders
|
|
877
|
+
# +file_path+ as a template against the request before the file is read.
|
|
878
|
+
def self.file(file_path, content_type: nil, template_type: nil, templated_file_path: nil)
|
|
879
|
+
new(type: 'FILE', file_path: file_path, content_type: content_type, template_type: template_type,
|
|
880
|
+
templated_file_path: templated_file_path)
|
|
705
881
|
end
|
|
706
882
|
|
|
707
883
|
def self.json_rpc(method_name, params_schema: nil)
|
|
@@ -716,6 +892,11 @@ module MockServer
|
|
|
716
892
|
@template_type = template_type
|
|
717
893
|
self
|
|
718
894
|
end
|
|
895
|
+
|
|
896
|
+
def with_templated_file_path(templated_file_path = true)
|
|
897
|
+
@templated_file_path = templated_file_path
|
|
898
|
+
self
|
|
899
|
+
end
|
|
719
900
|
end
|
|
720
901
|
|
|
721
902
|
class JsonRpcBody
|
|
@@ -891,7 +1072,7 @@ module MockServer
|
|
|
891
1072
|
attr_accessor :method, :path, :body, :secure, :protocol, :keep_alive, :respond_before_body,
|
|
892
1073
|
:socket_address, :jwt, :dns_name, :dns_type, :dns_class,
|
|
893
1074
|
:headers_key_match_style, :query_string_parameters_key_match_style,
|
|
894
|
-
:path_parameters_key_match_style
|
|
1075
|
+
:path_parameters_key_match_style, :client_ip
|
|
895
1076
|
attr_reader :query_string_parameters, :headers, :cookies, :path_parameters
|
|
896
1077
|
|
|
897
1078
|
def initialize(method: nil, path: nil, query_string_parameters: nil, headers: nil,
|
|
@@ -899,7 +1080,7 @@ module MockServer
|
|
|
899
1080
|
respond_before_body: nil, path_parameters: nil, socket_address: nil,
|
|
900
1081
|
jwt: nil, dns_name: nil, dns_type: nil, dns_class: nil,
|
|
901
1082
|
headers_key_match_style: nil, query_string_parameters_key_match_style: nil,
|
|
902
|
-
path_parameters_key_match_style: nil)
|
|
1083
|
+
path_parameters_key_match_style: nil, client_ip: nil)
|
|
903
1084
|
@method = method
|
|
904
1085
|
@path = path
|
|
905
1086
|
self.query_string_parameters = query_string_parameters
|
|
@@ -916,6 +1097,7 @@ module MockServer
|
|
|
916
1097
|
@dns_name = dns_name
|
|
917
1098
|
@dns_type = dns_type
|
|
918
1099
|
@dns_class = dns_class
|
|
1100
|
+
@client_ip = client_ip
|
|
919
1101
|
@headers_key_match_style = headers_key_match_style if headers_key_match_style
|
|
920
1102
|
@query_string_parameters_key_match_style = query_string_parameters_key_match_style if query_string_parameters_key_match_style
|
|
921
1103
|
@path_parameters_key_match_style = path_parameters_key_match_style if path_parameters_key_match_style
|
|
@@ -961,6 +1143,7 @@ module MockServer
|
|
|
961
1143
|
'protocol' => @protocol,
|
|
962
1144
|
'keepAlive' => @keep_alive,
|
|
963
1145
|
'respondBeforeBody' => @respond_before_body,
|
|
1146
|
+
'clientIp' => @client_ip,
|
|
964
1147
|
'pathParameters' => path_parameters,
|
|
965
1148
|
'socketAddress' => @socket_address&.to_h,
|
|
966
1149
|
'jwt' => @jwt&.to_h,
|
|
@@ -984,6 +1167,7 @@ module MockServer
|
|
|
984
1167
|
protocol: data['protocol'],
|
|
985
1168
|
keep_alive: data['keepAlive'],
|
|
986
1169
|
respond_before_body: data['respondBeforeBody'],
|
|
1170
|
+
client_ip: data['clientIp'],
|
|
987
1171
|
path_parameters: MockServer.deserialize_key_multi_values(data['pathParameters']),
|
|
988
1172
|
socket_address: SocketAddress.from_hash(data['socketAddress']),
|
|
989
1173
|
jwt: Jwt.from_hash(data['jwt']),
|
|
@@ -1057,6 +1241,13 @@ module MockServer
|
|
|
1057
1241
|
@respond_before_body = respond_before_body
|
|
1058
1242
|
self
|
|
1059
1243
|
end
|
|
1244
|
+
|
|
1245
|
+
# Match only requests from this client IP address or CIDR range (e.g. '10.0.0.0/8'); prefix with '!'
|
|
1246
|
+
# to match any other address. It is the TCP peer address, not X-Forwarded-For.
|
|
1247
|
+
def with_client_ip(client_ip)
|
|
1248
|
+
@client_ip = client_ip
|
|
1249
|
+
self
|
|
1250
|
+
end
|
|
1060
1251
|
end
|
|
1061
1252
|
|
|
1062
1253
|
class ConnectionOptions
|
|
@@ -1322,11 +1513,12 @@ module MockServer
|
|
|
1322
1513
|
class HttpTemplate
|
|
1323
1514
|
# response_override (an {HttpResponse}) is applied over the rendered output;
|
|
1324
1515
|
# response_modifier is the server's responseModifier object, as a Hash.
|
|
1516
|
+
# parameters is a Hash of values the template reads as parameters (parameters.greeting), kept exactly.
|
|
1325
1517
|
attr_accessor :template_type, :template, :template_file, :delay, :primary,
|
|
1326
|
-
:response_override, :response_modifier
|
|
1518
|
+
:response_override, :response_modifier, :parameters
|
|
1327
1519
|
|
|
1328
1520
|
def initialize(template_type: 'JAVASCRIPT', template: nil, template_file: nil, delay: nil, primary: nil,
|
|
1329
|
-
response_override: nil, response_modifier: nil)
|
|
1521
|
+
response_override: nil, response_modifier: nil, parameters: nil)
|
|
1330
1522
|
@template_type = template_type
|
|
1331
1523
|
@template = template
|
|
1332
1524
|
@template_file = template_file
|
|
@@ -1334,6 +1526,7 @@ module MockServer
|
|
|
1334
1526
|
@primary = primary
|
|
1335
1527
|
@response_override = response_override
|
|
1336
1528
|
@response_modifier = response_modifier
|
|
1529
|
+
@parameters = parameters
|
|
1337
1530
|
end
|
|
1338
1531
|
|
|
1339
1532
|
def to_h
|
|
@@ -1344,7 +1537,8 @@ module MockServer
|
|
|
1344
1537
|
'delay' => @delay&.to_h,
|
|
1345
1538
|
'primary' => @primary,
|
|
1346
1539
|
'responseOverride' => @response_override&.to_h,
|
|
1347
|
-
'responseModifier' => @response_modifier
|
|
1540
|
+
'responseModifier' => @response_modifier,
|
|
1541
|
+
'parameters' => (@parameters.nil? || @parameters.empty? ? nil : @parameters)
|
|
1348
1542
|
})
|
|
1349
1543
|
end
|
|
1350
1544
|
|
|
@@ -1358,7 +1552,8 @@ module MockServer
|
|
|
1358
1552
|
delay: Delay.from_hash(data['delay']),
|
|
1359
1553
|
primary: data['primary'],
|
|
1360
1554
|
response_override: HttpResponse.from_hash(data['responseOverride']),
|
|
1361
|
-
response_modifier: data['responseModifier']
|
|
1555
|
+
response_modifier: data['responseModifier'],
|
|
1556
|
+
parameters: data['parameters']
|
|
1362
1557
|
)
|
|
1363
1558
|
end
|
|
1364
1559
|
|
|
@@ -1380,6 +1575,12 @@ module MockServer
|
|
|
1380
1575
|
@response_modifier = response_modifier
|
|
1381
1576
|
self
|
|
1382
1577
|
end
|
|
1578
|
+
|
|
1579
|
+
# Adds a value the template reads as parameters.<name>.
|
|
1580
|
+
def with_parameter(name, value)
|
|
1581
|
+
@parameters = (@parameters || {}).merge(name.to_s => value)
|
|
1582
|
+
self
|
|
1583
|
+
end
|
|
1383
1584
|
end
|
|
1384
1585
|
|
|
1385
1586
|
class HttpClassCallback
|
|
@@ -1449,10 +1650,14 @@ module MockServer
|
|
|
1449
1650
|
# stream_error: reset the matched request stream with this error code (HTTP/2 RST_STREAM /
|
|
1450
1651
|
# HTTP/3 RESET_STREAM) instead of returning a response; HTTP/1.1 has no stream concept so this
|
|
1451
1652
|
# falls back to dropping the connection. Takes precedence over drop_connection when both are set.
|
|
1452
|
-
|
|
1653
|
+
# reset_connection: reset the connection with a TCP RST so the client sees "connection reset by
|
|
1654
|
+
# peer". Takes precedence over stream_error and drop_connection.
|
|
1655
|
+
attr_accessor :drop_connection, :reset_connection, :response_bytes, :stream_error, :delay, :primary
|
|
1453
1656
|
|
|
1454
|
-
def initialize(drop_connection: nil, response_bytes: nil, stream_error: nil, delay: nil, primary: nil
|
|
1657
|
+
def initialize(drop_connection: nil, response_bytes: nil, stream_error: nil, delay: nil, primary: nil,
|
|
1658
|
+
reset_connection: nil)
|
|
1455
1659
|
@drop_connection = drop_connection
|
|
1660
|
+
@reset_connection = reset_connection
|
|
1456
1661
|
@response_bytes = response_bytes
|
|
1457
1662
|
@stream_error = stream_error
|
|
1458
1663
|
@delay = delay
|
|
@@ -1462,6 +1667,7 @@ module MockServer
|
|
|
1462
1667
|
def to_h
|
|
1463
1668
|
MockServer.strip_none({
|
|
1464
1669
|
'dropConnection' => @drop_connection,
|
|
1670
|
+
'resetConnection' => @reset_connection,
|
|
1465
1671
|
'responseBytes' => @response_bytes,
|
|
1466
1672
|
'streamError' => @stream_error,
|
|
1467
1673
|
'delay' => @delay&.to_h,
|
|
@@ -1474,6 +1680,7 @@ module MockServer
|
|
|
1474
1680
|
|
|
1475
1681
|
new(
|
|
1476
1682
|
drop_connection: data['dropConnection'],
|
|
1683
|
+
reset_connection: data['resetConnection'],
|
|
1477
1684
|
response_bytes: data['responseBytes'],
|
|
1478
1685
|
stream_error: data['streamError'],
|
|
1479
1686
|
delay: Delay.from_hash(data['delay']),
|
|
@@ -2254,13 +2461,16 @@ module MockServer
|
|
|
2254
2461
|
end
|
|
2255
2462
|
|
|
2256
2463
|
class AfterAction
|
|
2257
|
-
# blocking, timeout and failure_policy are only meaningful for before-actions
|
|
2464
|
+
# blocking, timeout and failure_policy are only meaningful for before-actions.
|
|
2465
|
+
# http_request_template (an {HttpTemplate}) renders the webhook request from the triggering
|
|
2466
|
+
# request; escape request values in it and keep them out of its destination.
|
|
2258
2467
|
attr_accessor :http_request, :http_class_callback, :http_object_callback, :delay,
|
|
2259
|
-
:blocking, :timeout, :failure_policy
|
|
2468
|
+
:blocking, :timeout, :failure_policy, :http_request_template
|
|
2260
2469
|
|
|
2261
2470
|
def initialize(http_request: nil, http_class_callback: nil, http_object_callback: nil, delay: nil,
|
|
2262
|
-
blocking: nil, timeout: nil, failure_policy: nil)
|
|
2471
|
+
blocking: nil, timeout: nil, failure_policy: nil, http_request_template: nil)
|
|
2263
2472
|
@http_request = http_request
|
|
2473
|
+
@http_request_template = http_request_template
|
|
2264
2474
|
@http_class_callback = http_class_callback
|
|
2265
2475
|
@http_object_callback = http_object_callback
|
|
2266
2476
|
@delay = delay
|
|
@@ -2271,9 +2481,10 @@ module MockServer
|
|
|
2271
2481
|
|
|
2272
2482
|
def to_h
|
|
2273
2483
|
MockServer.strip_none({
|
|
2274
|
-
'httpRequest'
|
|
2275
|
-
'
|
|
2276
|
-
'
|
|
2484
|
+
'httpRequest' => @http_request&.to_h,
|
|
2485
|
+
'httpRequestTemplate' => @http_request_template&.to_h,
|
|
2486
|
+
'httpClassCallback' => @http_class_callback&.to_h,
|
|
2487
|
+
'httpObjectCallback' => @http_object_callback&.to_h,
|
|
2277
2488
|
'delay' => @delay&.to_h,
|
|
2278
2489
|
'blocking' => @blocking,
|
|
2279
2490
|
'timeout' => @timeout&.to_h,
|
|
@@ -2291,7 +2502,8 @@ module MockServer
|
|
|
2291
2502
|
delay: Delay.from_hash(data['delay']),
|
|
2292
2503
|
blocking: data['blocking'],
|
|
2293
2504
|
timeout: Delay.from_hash(data['timeout']),
|
|
2294
|
-
failure_policy: data['failurePolicy']
|
|
2505
|
+
failure_policy: data['failurePolicy'],
|
|
2506
|
+
http_request_template: HttpTemplate.from_hash(data['httpRequestTemplate'])
|
|
2295
2507
|
)
|
|
2296
2508
|
end
|
|
2297
2509
|
end
|
|
@@ -2710,6 +2922,7 @@ module MockServer
|
|
|
2710
2922
|
:http_responses, :response_mode, :response_weights, :switch_after,
|
|
2711
2923
|
:cross_protocol_scenarios, :steps, :capture, :namespace,
|
|
2712
2924
|
:scenario_name, :scenario_state, :new_scenario_state, :timestamp
|
|
2925
|
+
attr_reader :metadata
|
|
2713
2926
|
|
|
2714
2927
|
def initialize(id: nil, priority: nil, percentage: nil, http_request: nil, http_response: nil,
|
|
2715
2928
|
http_response_template: nil, http_response_class_callback: nil,
|
|
@@ -2726,7 +2939,7 @@ module MockServer
|
|
|
2726
2939
|
switch_after: nil, cross_protocol_scenarios: nil, steps: nil,
|
|
2727
2940
|
capture: nil, namespace: nil,
|
|
2728
2941
|
scenario_name: nil, scenario_state: nil, new_scenario_state: nil,
|
|
2729
|
-
timestamp: nil)
|
|
2942
|
+
timestamp: nil, metadata: nil)
|
|
2730
2943
|
@id = id
|
|
2731
2944
|
@priority = priority
|
|
2732
2945
|
@percentage = percentage
|
|
@@ -2771,6 +2984,18 @@ module MockServer
|
|
|
2771
2984
|
@scenario_state = scenario_state
|
|
2772
2985
|
@new_scenario_state = new_scenario_state
|
|
2773
2986
|
@timestamp = timestamp
|
|
2987
|
+
self.metadata = metadata
|
|
2988
|
+
end
|
|
2989
|
+
|
|
2990
|
+
# Free-form labels (owner, ticket, tags, ...) used to retrieve or clear
|
|
2991
|
+
# expectations by metadata. Keys are stored as Strings (Symbol keys are
|
|
2992
|
+
# converted, at any depth); values may be any JSON value, including nil.
|
|
2993
|
+
def metadata=(value)
|
|
2994
|
+
unless value.nil? || value.is_a?(Hash)
|
|
2995
|
+
raise ArgumentError, "metadata must be a Hash, got #{value.class}"
|
|
2996
|
+
end
|
|
2997
|
+
|
|
2998
|
+
@metadata = value.nil? ? nil : MockServer.stringify_keys(value)
|
|
2774
2999
|
end
|
|
2775
3000
|
|
|
2776
3001
|
# Set the response class-callback action. Accepts either a fully-qualified
|
|
@@ -2845,7 +3070,8 @@ module MockServer
|
|
|
2845
3070
|
'scenarioName' => @scenario_name,
|
|
2846
3071
|
'scenarioState' => @scenario_state,
|
|
2847
3072
|
'newScenarioState' => @new_scenario_state,
|
|
2848
|
-
'timestamp' => @timestamp
|
|
3073
|
+
'timestamp' => @timestamp,
|
|
3074
|
+
'metadata' => @metadata.nil? || @metadata.empty? ? nil : @metadata
|
|
2849
3075
|
})
|
|
2850
3076
|
end
|
|
2851
3077
|
|
|
@@ -2907,11 +3133,232 @@ module MockServer
|
|
|
2907
3133
|
scenario_name: data['scenarioName'],
|
|
2908
3134
|
scenario_state: data['scenarioState'],
|
|
2909
3135
|
new_scenario_state: data['newScenarioState'],
|
|
2910
|
-
timestamp: data['timestamp']
|
|
3136
|
+
timestamp: data['timestamp'],
|
|
3137
|
+
metadata: data['metadata']
|
|
2911
3138
|
)
|
|
2912
3139
|
end
|
|
2913
3140
|
end
|
|
2914
3141
|
|
|
3142
|
+
# Selects expectations to retrieve or clear by metadata, by whether they have
|
|
3143
|
+
# ever matched a request, and pages a retrieved list. Sent as query parameters
|
|
3144
|
+
# on +PUT /mockserver/retrieve+ and +PUT /mockserver/clear+; every selection
|
|
3145
|
+
# that is set must hold. Invalid arguments raise ArgumentError immediately, so
|
|
3146
|
+
# a broken filter can never be sent as "no filter" (which, on clear, clears
|
|
3147
|
+
# every expectation).
|
|
3148
|
+
#
|
|
3149
|
+
# MockServer::ExpectationFilter.new.with_metadata('owner', 'team-a').with_limit(10)
|
|
3150
|
+
class ExpectationFilter
|
|
3151
|
+
# Pass as the value to {#with_metadata} to select a stored JSON null; Ruby
|
|
3152
|
+
# +nil+ is rejected so it can never read as "any value".
|
|
3153
|
+
JSON_NULL = Object.new.tap do |null|
|
|
3154
|
+
def null.inspect
|
|
3155
|
+
'MockServer::ExpectationFilter::JSON_NULL'
|
|
3156
|
+
end
|
|
3157
|
+
end.freeze
|
|
3158
|
+
|
|
3159
|
+
attr_reader :metadata_key, :metadata_value, :metadata_json_path, :unmatched, :offset, :limit
|
|
3160
|
+
|
|
3161
|
+
def initialize
|
|
3162
|
+
@metadata_key = nil
|
|
3163
|
+
@metadata_value = nil
|
|
3164
|
+
@metadata_json_path = nil
|
|
3165
|
+
@unmatched = false
|
|
3166
|
+
@offset = nil
|
|
3167
|
+
@limit = nil
|
|
3168
|
+
end
|
|
3169
|
+
|
|
3170
|
+
# Select expectations whose metadata has +key+ with a value equal to
|
|
3171
|
+
# +value+. A String is sent as text; any other value (number, boolean,
|
|
3172
|
+
# Array, Hash, {JSON_NULL}) is sent as its compact JSON.
|
|
3173
|
+
# @raise [ArgumentError] if +key+ is blank or +value+ is nil
|
|
3174
|
+
def with_metadata(key, value)
|
|
3175
|
+
if value.nil?
|
|
3176
|
+
raise ArgumentError, 'the metadata value must not be nil; use with_metadata_key(key) to select any value, ' \
|
|
3177
|
+
'or ExpectationFilter::JSON_NULL to select a JSON null'
|
|
3178
|
+
end
|
|
3179
|
+
|
|
3180
|
+
@metadata_key = ExpectationFilter.require_not_blank(key, 'metadata key')
|
|
3181
|
+
@metadata_value = ExpectationFilter.encode_value(value)
|
|
3182
|
+
self
|
|
3183
|
+
end
|
|
3184
|
+
|
|
3185
|
+
# Select expectations whose metadata has +key+, with any value.
|
|
3186
|
+
# @raise [ArgumentError] if +key+ is blank
|
|
3187
|
+
def with_metadata_key(key)
|
|
3188
|
+
@metadata_key = ExpectationFilter.require_not_blank(key, 'metadata key')
|
|
3189
|
+
@metadata_value = nil
|
|
3190
|
+
self
|
|
3191
|
+
end
|
|
3192
|
+
|
|
3193
|
+
# Select expectations whose metadata object the JSONPath selects at least
|
|
3194
|
+
# one value from, e.g. <tt>$[?(@.owner == 'team-a')]</tt>.
|
|
3195
|
+
# @raise [ArgumentError] if +json_path+ is blank
|
|
3196
|
+
def with_metadata_json_path(json_path)
|
|
3197
|
+
@metadata_json_path = ExpectationFilter.require_not_blank(json_path, 'metadata JSONPath')
|
|
3198
|
+
self
|
|
3199
|
+
end
|
|
3200
|
+
|
|
3201
|
+
# Select only expectations that have not matched any request.
|
|
3202
|
+
def with_unmatched(unmatched = true)
|
|
3203
|
+
unless [true, false].include?(unmatched)
|
|
3204
|
+
raise ArgumentError, "unmatched must be true or false, got #{unmatched.inspect}"
|
|
3205
|
+
end
|
|
3206
|
+
|
|
3207
|
+
@unmatched = unmatched
|
|
3208
|
+
self
|
|
3209
|
+
end
|
|
3210
|
+
|
|
3211
|
+
# Skip this many results (retrieve only).
|
|
3212
|
+
def with_offset(offset)
|
|
3213
|
+
@offset = ExpectationFilter.require_non_negative(offset, 'offset')
|
|
3214
|
+
self
|
|
3215
|
+
end
|
|
3216
|
+
|
|
3217
|
+
# Return at most this many results (retrieve only).
|
|
3218
|
+
def with_limit(limit)
|
|
3219
|
+
@limit = ExpectationFilter.require_non_negative(limit, 'limit')
|
|
3220
|
+
self
|
|
3221
|
+
end
|
|
3222
|
+
|
|
3223
|
+
# @return [Boolean] true when a metadata or unmatched selection is set
|
|
3224
|
+
def selection?
|
|
3225
|
+
!@metadata_key.nil? || !@metadata_json_path.nil? || @unmatched
|
|
3226
|
+
end
|
|
3227
|
+
|
|
3228
|
+
# @return [Boolean] true when offset or limit is set
|
|
3229
|
+
def paging?
|
|
3230
|
+
!@offset.nil? || !@limit.nil?
|
|
3231
|
+
end
|
|
3232
|
+
|
|
3233
|
+
# The query parameters for this filter, names and values exactly as sent.
|
|
3234
|
+
# @return [Hash{String => String}]
|
|
3235
|
+
def to_query_params
|
|
3236
|
+
params = {}
|
|
3237
|
+
unless @metadata_key.nil?
|
|
3238
|
+
params['metadataKey'] = @metadata_key
|
|
3239
|
+
params['metadataValue'] = @metadata_value unless @metadata_value.nil?
|
|
3240
|
+
end
|
|
3241
|
+
params['metadataJsonPath'] = @metadata_json_path unless @metadata_json_path.nil?
|
|
3242
|
+
params['unmatched'] = 'true' if @unmatched
|
|
3243
|
+
params['offset'] = @offset.to_s unless @offset.nil?
|
|
3244
|
+
params['limit'] = @limit.to_s unless @limit.nil?
|
|
3245
|
+
params
|
|
3246
|
+
end
|
|
3247
|
+
|
|
3248
|
+
# @api private
|
|
3249
|
+
def self.require_not_blank(value, description)
|
|
3250
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
|
3251
|
+
raise ArgumentError, "the #{description} must be a String, got #{value.inspect}"
|
|
3252
|
+
end
|
|
3253
|
+
raise ArgumentError, "the #{description} must not be blank" if value.to_s.strip.empty?
|
|
3254
|
+
|
|
3255
|
+
value.to_s
|
|
3256
|
+
end
|
|
3257
|
+
|
|
3258
|
+
# @api private
|
|
3259
|
+
def self.require_non_negative(value, description)
|
|
3260
|
+
unless value.is_a?(Integer) && value >= 0
|
|
3261
|
+
raise ArgumentError, "#{description} must be an Integer of zero or more, got #{value.inspect}"
|
|
3262
|
+
end
|
|
3263
|
+
|
|
3264
|
+
value
|
|
3265
|
+
end
|
|
3266
|
+
|
|
3267
|
+
# @api private
|
|
3268
|
+
def self.encode_value(value)
|
|
3269
|
+
return 'null' if value.equal?(JSON_NULL)
|
|
3270
|
+
return value if value.is_a?(String)
|
|
3271
|
+
|
|
3272
|
+
JSON.generate(MockServer.stringify_keys(value))
|
|
3273
|
+
rescue JSON::GeneratorError => e
|
|
3274
|
+
raise ArgumentError, "the metadata value #{value.inspect} cannot be sent as JSON: #{e.message}"
|
|
3275
|
+
end
|
|
3276
|
+
end
|
|
3277
|
+
|
|
3278
|
+
# Options that shape the expectations returned by
|
|
3279
|
+
# {Client#retrieve_recorded_expectations}. Sent as query parameters on
|
|
3280
|
+
# +PUT /mockserver/retrieve?type=RECORDED_EXPECTATIONS+; each option is off
|
|
3281
|
+
# unless set. Invalid arguments raise ArgumentError immediately, so nothing
|
|
3282
|
+
# is sent.
|
|
3283
|
+
#
|
|
3284
|
+
# MockServer::RecordedExpectationsOptions.new
|
|
3285
|
+
# .with_repeats_as_scenarios(true)
|
|
3286
|
+
# .with_capture_headers('X-Tenant')
|
|
3287
|
+
# .with_extract_bodies_larger_than(1024)
|
|
3288
|
+
class RecordedExpectationsOptions
|
|
3289
|
+
attr_reader :consolidate, :parameterize, :repeats_as_scenarios, :capture_headers, :extract_bodies_larger_than
|
|
3290
|
+
|
|
3291
|
+
def initialize
|
|
3292
|
+
@consolidate = nil
|
|
3293
|
+
@parameterize = nil
|
|
3294
|
+
@repeats_as_scenarios = nil
|
|
3295
|
+
@capture_headers = nil
|
|
3296
|
+
@extract_bodies_larger_than = nil
|
|
3297
|
+
end
|
|
3298
|
+
|
|
3299
|
+
# Collapse recorded exchanges that share a request shape into one
|
|
3300
|
+
# reusable, unlimited-times expectation.
|
|
3301
|
+
def with_consolidate(consolidate = true)
|
|
3302
|
+
@consolidate = RecordedExpectationsOptions.require_boolean(consolidate, 'consolidate')
|
|
3303
|
+
self
|
|
3304
|
+
end
|
|
3305
|
+
|
|
3306
|
+
# Also generalise volatile path, query, header and body values into
|
|
3307
|
+
# matchers (implies consolidate).
|
|
3308
|
+
def with_parameterize(parameterize = true)
|
|
3309
|
+
@parameterize = RecordedExpectationsOptions.require_boolean(parameterize, 'parameterize')
|
|
3310
|
+
self
|
|
3311
|
+
end
|
|
3312
|
+
|
|
3313
|
+
# Replay a request recorded several times with different responses in the
|
|
3314
|
+
# recorded order, then keep returning the last one.
|
|
3315
|
+
def with_repeats_as_scenarios(repeats_as_scenarios = true)
|
|
3316
|
+
@repeats_as_scenarios = RecordedExpectationsOptions.require_boolean(repeats_as_scenarios, 'repeats_as_scenarios')
|
|
3317
|
+
self
|
|
3318
|
+
end
|
|
3319
|
+
|
|
3320
|
+
# Keep only these request headers (case-insensitive) in each recorded
|
|
3321
|
+
# matcher; no names keeps none.
|
|
3322
|
+
# @raise [ArgumentError] if a name is blank or contains a comma
|
|
3323
|
+
def with_capture_headers(*header_names)
|
|
3324
|
+
header_names.each do |name|
|
|
3325
|
+
unless name.is_a?(String) && !name.strip.empty? && !name.include?(',')
|
|
3326
|
+
raise ArgumentError,
|
|
3327
|
+
"each capture header must be a non-blank header name without a comma, got #{name.inspect}"
|
|
3328
|
+
end
|
|
3329
|
+
end
|
|
3330
|
+
@capture_headers = header_names
|
|
3331
|
+
self
|
|
3332
|
+
end
|
|
3333
|
+
|
|
3334
|
+
# Write each response body larger than this many bytes to the server's
|
|
3335
|
+
# +recordedBodyFilesDirectory+ and return a FILE body that points at it.
|
|
3336
|
+
# @raise [ArgumentError] unless an Integer of zero or more
|
|
3337
|
+
def with_extract_bodies_larger_than(threshold_bytes)
|
|
3338
|
+
@extract_bodies_larger_than = ExpectationFilter.require_non_negative(threshold_bytes, 'extract_bodies_larger_than')
|
|
3339
|
+
self
|
|
3340
|
+
end
|
|
3341
|
+
|
|
3342
|
+
# The query parameters for these options, names and values exactly as sent.
|
|
3343
|
+
# @return [Hash{String => String}]
|
|
3344
|
+
def to_query_params
|
|
3345
|
+
params = {}
|
|
3346
|
+
params['consolidate'] = 'true' if @consolidate
|
|
3347
|
+
params['parameterize'] = 'true' if @parameterize
|
|
3348
|
+
params['repeatsAsScenarios'] = 'true' if @repeats_as_scenarios
|
|
3349
|
+
params['captureHeaders'] = @capture_headers.join(',') unless @capture_headers.nil?
|
|
3350
|
+
params['extractBodiesLargerThan'] = @extract_bodies_larger_than.to_s unless @extract_bodies_larger_than.nil?
|
|
3351
|
+
params
|
|
3352
|
+
end
|
|
3353
|
+
|
|
3354
|
+
# @api private
|
|
3355
|
+
def self.require_boolean(value, description)
|
|
3356
|
+
raise ArgumentError, "#{description} must be true or false, got #{value.inspect}" unless [true, false].include?(value)
|
|
3357
|
+
|
|
3358
|
+
value
|
|
3359
|
+
end
|
|
3360
|
+
end
|
|
3361
|
+
|
|
2915
3362
|
# An OpenAPI request matcher: as an expectation's or verification's
|
|
2916
3363
|
# httpRequest it matches the requests an OpenAPI operation accepts.
|
|
2917
3364
|
class OpenAPIDefinition
|
|
@@ -2983,6 +3430,37 @@ module MockServer
|
|
|
2983
3430
|
end
|
|
2984
3431
|
end
|
|
2985
3432
|
|
|
3433
|
+
# A logical OR of request matchers, written as an httpRequest holding an
|
|
3434
|
+
# anyOf array. It matches a request when any branch matches it, trying them
|
|
3435
|
+
# in order. Each branch is an HttpRequest, an OpenAPIDefinition, a
|
|
3436
|
+
# ConditionalRequestDefinition or another AnyOfRequestDefinition.
|
|
3437
|
+
class AnyOfRequestDefinition
|
|
3438
|
+
# not_any_of inverts the match (the server's +not+ field): it then matches
|
|
3439
|
+
# when no branch does.
|
|
3440
|
+
attr_accessor :any_of, :not_any_of
|
|
3441
|
+
|
|
3442
|
+
def initialize(any_of: [], not_any_of: nil)
|
|
3443
|
+
@any_of = any_of
|
|
3444
|
+
@not_any_of = not_any_of
|
|
3445
|
+
end
|
|
3446
|
+
|
|
3447
|
+
def to_h
|
|
3448
|
+
MockServer.strip_none({
|
|
3449
|
+
'not' => @not_any_of,
|
|
3450
|
+
'anyOf' => (@any_of || []).map(&:to_h)
|
|
3451
|
+
})
|
|
3452
|
+
end
|
|
3453
|
+
|
|
3454
|
+
def self.from_hash(data)
|
|
3455
|
+
return nil if data.nil?
|
|
3456
|
+
|
|
3457
|
+
new(
|
|
3458
|
+
any_of: (data['anyOf'] || []).map { |branch| MockServer.deserialize_request_definition(branch) },
|
|
3459
|
+
not_any_of: data['not']
|
|
3460
|
+
)
|
|
3461
|
+
end
|
|
3462
|
+
end
|
|
3463
|
+
|
|
2986
3464
|
class OpenAPIExpectation
|
|
2987
3465
|
attr_accessor :spec_url_or_payload, :operations_and_responses
|
|
2988
3466
|
|
|
@@ -3680,20 +4158,40 @@ module MockServer
|
|
|
3680
4158
|
end
|
|
3681
4159
|
end
|
|
3682
4160
|
|
|
4161
|
+
# For an anyOf request matcher that does not match the request, which branch
|
|
4162
|
+
# came closest (+closest_branch+, 0-based, the earlier on a tie) and how each
|
|
4163
|
+
# differed: each of +branches+ has branch, matches, differingFieldCount and
|
|
4164
|
+
# differingFields.
|
|
4165
|
+
class AnyOfBranchComparison
|
|
4166
|
+
attr_reader :closest_branch, :branches
|
|
4167
|
+
|
|
4168
|
+
def initialize(closest_branch: nil, branches: [])
|
|
4169
|
+
@closest_branch = closest_branch
|
|
4170
|
+
@branches = branches
|
|
4171
|
+
end
|
|
4172
|
+
|
|
4173
|
+
def self.from_hash(data)
|
|
4174
|
+
return nil if data.nil?
|
|
4175
|
+
|
|
4176
|
+
new(closest_branch: data['closestBranch'], branches: data['branches'] || [])
|
|
4177
|
+
end
|
|
4178
|
+
end
|
|
4179
|
+
|
|
3683
4180
|
# One expectation as explain, debug_mismatch or explain_unmatched evaluated
|
|
3684
4181
|
# it. The matched expectation has no +reason+; a skipped or shadowed one has
|
|
3685
4182
|
# a +reason+ (NAMESPACE, INACTIVE, FIELDS_DIFFER, NOT_OPERATOR, CONVERSATION,
|
|
3686
4183
|
# SCENARIO_STATE, PERCENTAGE or LOWER_PRIORITY) and an +explanation+.
|
|
3687
|
-
# +suggested_fix+ is set only by explain_unmatched with +suggest_fixes
|
|
4184
|
+
# +suggested_fix+ is set only by explain_unmatched with +suggest_fixes+;
|
|
4185
|
+
# +any_of+ only for an anyOf request matcher that does not match.
|
|
3688
4186
|
class ExplainedExpectation
|
|
3689
4187
|
attr_reader :expectation_id, :expectation_method, :expectation_path, :position, :priority,
|
|
3690
4188
|
:reason, :explanation, :matches, :matched_field_count, :total_field_count,
|
|
3691
|
-
:differing_field_count, :differences, :remediation, :suggested_fix
|
|
4189
|
+
:differing_field_count, :differences, :remediation, :suggested_fix, :any_of
|
|
3692
4190
|
|
|
3693
4191
|
def initialize(expectation_id: nil, expectation_method: nil, expectation_path: nil, position: nil,
|
|
3694
4192
|
priority: nil, reason: nil, explanation: nil, matches: nil, matched_field_count: nil,
|
|
3695
4193
|
total_field_count: nil, differing_field_count: nil, differences: {}, remediation: {},
|
|
3696
|
-
suggested_fix: nil)
|
|
4194
|
+
suggested_fix: nil, any_of: nil)
|
|
3697
4195
|
@expectation_id = expectation_id
|
|
3698
4196
|
@expectation_method = expectation_method
|
|
3699
4197
|
@expectation_path = expectation_path
|
|
@@ -3708,6 +4206,7 @@ module MockServer
|
|
|
3708
4206
|
@differences = differences
|
|
3709
4207
|
@remediation = remediation
|
|
3710
4208
|
@suggested_fix = suggested_fix
|
|
4209
|
+
@any_of = any_of
|
|
3711
4210
|
end
|
|
3712
4211
|
|
|
3713
4212
|
def self.from_hash(data)
|
|
@@ -3727,7 +4226,8 @@ module MockServer
|
|
|
3727
4226
|
differing_field_count: data['differingFieldCount'],
|
|
3728
4227
|
differences: data['differences'] || {},
|
|
3729
4228
|
remediation: data['remediation'] || {},
|
|
3730
|
-
suggested_fix: SuggestedFix.from_hash(data['suggestedFix'])
|
|
4229
|
+
suggested_fix: SuggestedFix.from_hash(data['suggestedFix']),
|
|
4230
|
+
any_of: AnyOfBranchComparison.from_hash(data['anyOf'])
|
|
3731
4231
|
)
|
|
3732
4232
|
end
|
|
3733
4233
|
|