hs-pact-support 1.17.1

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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +620 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +5 -0
  5. data/lib/pact/array_like.rb +49 -0
  6. data/lib/pact/configuration.rb +193 -0
  7. data/lib/pact/consumer/request.rb +27 -0
  8. data/lib/pact/consumer_contract/consumer_contract.rb +97 -0
  9. data/lib/pact/consumer_contract/file_name.rb +22 -0
  10. data/lib/pact/consumer_contract/headers.rb +51 -0
  11. data/lib/pact/consumer_contract/http_consumer_contract_parser.rb +37 -0
  12. data/lib/pact/consumer_contract/interaction.rb +81 -0
  13. data/lib/pact/consumer_contract/interaction_parser.rb +23 -0
  14. data/lib/pact/consumer_contract/interaction_v2_parser.rb +57 -0
  15. data/lib/pact/consumer_contract/interaction_v3_parser.rb +92 -0
  16. data/lib/pact/consumer_contract/pact_file.rb +157 -0
  17. data/lib/pact/consumer_contract/provider_state.rb +34 -0
  18. data/lib/pact/consumer_contract/query.rb +138 -0
  19. data/lib/pact/consumer_contract/query_hash.rb +89 -0
  20. data/lib/pact/consumer_contract/query_string.rb +51 -0
  21. data/lib/pact/consumer_contract/request.rb +83 -0
  22. data/lib/pact/consumer_contract/response.rb +58 -0
  23. data/lib/pact/consumer_contract/service_consumer.rb +28 -0
  24. data/lib/pact/consumer_contract/service_provider.rb +28 -0
  25. data/lib/pact/consumer_contract/string_with_matching_rules.rb +17 -0
  26. data/lib/pact/consumer_contract.rb +1 -0
  27. data/lib/pact/errors.rb +21 -0
  28. data/lib/pact/helpers.rb +60 -0
  29. data/lib/pact/http/authorization_header_redactor.rb +32 -0
  30. data/lib/pact/logging.rb +14 -0
  31. data/lib/pact/matchers/actual_type.rb +16 -0
  32. data/lib/pact/matchers/base_difference.rb +39 -0
  33. data/lib/pact/matchers/differ.rb +153 -0
  34. data/lib/pact/matchers/difference.rb +13 -0
  35. data/lib/pact/matchers/difference_indicator.rb +26 -0
  36. data/lib/pact/matchers/embedded_diff_formatter.rb +60 -0
  37. data/lib/pact/matchers/expected_type.rb +35 -0
  38. data/lib/pact/matchers/extract_diff_messages.rb +76 -0
  39. data/lib/pact/matchers/index_not_found.rb +15 -0
  40. data/lib/pact/matchers/list_diff_formatter.rb +103 -0
  41. data/lib/pact/matchers/matchers.rb +285 -0
  42. data/lib/pact/matchers/multipart_form_diff_formatter.rb +41 -0
  43. data/lib/pact/matchers/no_diff_at_index.rb +18 -0
  44. data/lib/pact/matchers/regexp_difference.rb +13 -0
  45. data/lib/pact/matchers/type_difference.rb +16 -0
  46. data/lib/pact/matchers/unexpected_index.rb +11 -0
  47. data/lib/pact/matchers/unexpected_key.rb +11 -0
  48. data/lib/pact/matchers/unix_diff_formatter.rb +157 -0
  49. data/lib/pact/matchers.rb +1 -0
  50. data/lib/pact/matching_rules/extract.rb +91 -0
  51. data/lib/pact/matching_rules/jsonpath.rb +58 -0
  52. data/lib/pact/matching_rules/merge.rb +125 -0
  53. data/lib/pact/matching_rules/v3/extract.rb +94 -0
  54. data/lib/pact/matching_rules/v3/merge.rb +141 -0
  55. data/lib/pact/matching_rules.rb +30 -0
  56. data/lib/pact/reification.rb +56 -0
  57. data/lib/pact/rspec.rb +51 -0
  58. data/lib/pact/shared/active_support_support.rb +65 -0
  59. data/lib/pact/shared/dsl.rb +76 -0
  60. data/lib/pact/shared/form_differ.rb +32 -0
  61. data/lib/pact/shared/jruby_support.rb +18 -0
  62. data/lib/pact/shared/json_differ.rb +10 -0
  63. data/lib/pact/shared/key_not_found.rb +15 -0
  64. data/lib/pact/shared/multipart_form_differ.rb +16 -0
  65. data/lib/pact/shared/null_expectation.rb +31 -0
  66. data/lib/pact/shared/request.rb +106 -0
  67. data/lib/pact/shared/text_differ.rb +11 -0
  68. data/lib/pact/something_like.rb +49 -0
  69. data/lib/pact/specification_version.rb +18 -0
  70. data/lib/pact/support/version.rb +5 -0
  71. data/lib/pact/support.rb +12 -0
  72. data/lib/pact/symbolize_keys.rb +13 -0
  73. data/lib/pact/term.rb +85 -0
  74. data/lib/tasks/pact.rake +29 -0
  75. metadata +327 -0
@@ -0,0 +1,65 @@
1
+ # The support you need when you use ActiveSupport
2
+
3
+ module Pact
4
+ module ActiveSupportSupport
5
+ extend self
6
+
7
+ def fix_all_the_things thing
8
+ if defined?(ActiveSupport)
9
+ if thing.is_a?(Regexp)
10
+ fix_regexp(thing)
11
+ elsif thing.is_a?(Array)
12
+ thing.collect{ | it | fix_all_the_things it }
13
+ elsif thing.is_a?(Hash)
14
+ thing.each_with_object({}) { | (k, v), new_hash | new_hash[k] = fix_all_the_things(v) }
15
+ elsif thing.is_a?(Pact::Term)
16
+ # matcher Regexp is fixed in its own as_json method
17
+ thing
18
+ elsif thing.class.name.start_with?("Pact")
19
+ warn_about_regexp(thing)
20
+ thing
21
+ else
22
+ thing
23
+ end
24
+ else
25
+ thing
26
+ end
27
+ end
28
+
29
+ # ActiveSupport JSON overwrites (i.e. TRAMPLES) the json methods of the Regexp class directly
30
+ # (beneath its destructive hooves of destruction).
31
+ # This does not seem to be able to be undone without affecting the JSON serialisation in the
32
+ # calling project, so the best way I've found to fix this issue is to reattach the
33
+ # original as_json to the Regexp instances in the ConsumerContract before we write them to the
34
+ # pact file. If anyone can find a better way, please submit a pull request ASAP!
35
+ def fix_regexp regexp
36
+ {:json_class => 'Regexp', "o" => regexp.options, "s" => regexp.source }
37
+ end
38
+
39
+ # Having Active Support JSON loaded somehow kills the formatting of pretty_generate for objects.
40
+ # Don't ask me why, but it still seems to work for hashes, so the hacky work around is to
41
+ # reparse the generated JSON into a hash and pretty_generate that... sigh...
42
+ # Oh ActiveSupport, why....
43
+ def fix_json_formatting json
44
+ if json =~ /\{".*?":"/
45
+ JSON.pretty_generate(JSON.parse(json, create_additions: false))
46
+ else
47
+ json
48
+ end
49
+ end
50
+
51
+ def remove_unicode json
52
+ json.gsub(/\\u([0-9A-Za-z]{4})/) {|s| [$1.to_i(16)].pack("U")}
53
+ end
54
+
55
+ def warn_about_regexp(thing)
56
+ thing.instance_variables.each do | iv_name |
57
+ iv = thing.instance_variable_get(iv_name)
58
+ if iv.is_a?(Regexp)
59
+ require 'pact/configuration'
60
+ Pact.configuration.error_stream.puts("WARN: Instance variable #{iv_name} for class #{thing.class.name} is a Regexp and isn't been serialized properly. Please raise an issue at https://github.com/pact-foundation/pact-support/issues/new.")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,76 @@
1
+ module Pact
2
+
3
+ class DslDelegator
4
+
5
+ def initialize delegation_target
6
+ @delegation_target = delegation_target
7
+ end
8
+
9
+ def instance_eval_with_previous_context_available(*args, &block)
10
+ with_previous_context_available(block.binding) do
11
+ bind_block_as_instance_method_on_self(&block).call(*args)
12
+ end
13
+ end
14
+
15
+ protected
16
+
17
+ def method_missing(method, *args, &block)
18
+ if delegation_target_responds_to? method
19
+ delegation_target.send(method, *args, &block)
20
+ else
21
+ previous_context.send(method, *args, &block)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_accessor :delegation_target, :previous_context
28
+
29
+ def bind_block_as_instance_method_on_self(&block)
30
+ create_instance_method_from_block(&block).bind(self)
31
+ end
32
+
33
+
34
+ def create_instance_method_from_block &block
35
+ meth = self.class.class_eval do
36
+ define_method :block_as_instance_method_, &block
37
+ meth = instance_method :block_as_instance_method_
38
+ remove_method :block_as_instance_method_
39
+ meth
40
+ end
41
+ end
42
+
43
+ def with_previous_context_available(binding, &block)
44
+ @previous_context = binding.eval('self')
45
+ result = block.call
46
+ @previous_context = nil
47
+ result
48
+ end
49
+
50
+ def delegation_target_responds_to?(method)
51
+ delegation_target.respond_to? method
52
+ end
53
+
54
+ end
55
+
56
+
57
+ # Ripped from http://blog.joecorcoran.co.uk/2013/09/04/simple-pattern-ruby-dsl
58
+ # and then fixed up by using http://www.skorks.com/2013/03/a-closure-is-not-always-a-closure-in-ruby/
59
+ # to access variables and methods defined in the calling scope.
60
+ module DSL
61
+ def build(*args, &block)
62
+ new_instance_of_delegation_target_class = self.new(*args)
63
+ dsl_delegator_class = self.const_get('DSL_DELEGATOR_CLASS')
64
+ dsl_delegator = dsl_delegator_class.new(new_instance_of_delegation_target_class)
65
+ dsl_delegator.instance_eval_with_previous_context_available(&block) if block
66
+ new_instance_of_delegation_target_class.finalize
67
+ new_instance_of_delegation_target_class
68
+ end
69
+
70
+ def dsl(&block)
71
+ dsl_delegator_class = Class.new(DslDelegator, &block)
72
+ self.const_set('DSL_DELEGATOR_CLASS', dsl_delegator_class)
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ require 'uri'
2
+
3
+ module Pact
4
+ class FormDiffer
5
+
6
+ def self.call expected, actual, options = {}
7
+ require 'pact/matchers' # avoid recursive loop between this file and pact/matchers
8
+ ::Pact::Matchers.diff to_hash(expected), to_hash(actual), options
9
+ end
10
+
11
+ def self.to_hash form_body
12
+ if form_body.is_a?(Hash)
13
+ ensure_values_are_arrays form_body
14
+ else
15
+ decode_www_form form_body
16
+ end
17
+ end
18
+
19
+ def self.ensure_values_are_arrays hash
20
+ hash.each_with_object({}) do | (key, value), h |
21
+ h[key.to_s] = [*value]
22
+ end
23
+ end
24
+
25
+ def self.decode_www_form string
26
+ URI.decode_www_form(string).each_with_object({}) do | (key, value), hash |
27
+ hash[key] ||= []
28
+ hash[key] << value
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ module Pact
2
+ module JRubySupport
3
+
4
+ # Under jruby, JSON.pretty_generate inserts a blank new line between the opening and closing
5
+ # brackets of an empty hash, like so:
6
+ # {
7
+ # "empty": {
8
+ #
9
+ # }
10
+ # }
11
+ # This screws up the UnixDiffFormatter, so we need to remove the blank lines.
12
+
13
+ def fix_blank_lines_in_empty_hashes json
14
+ json.gsub(/({\n)\n(\s*})/,'\1\2')
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ module Pact
2
+ class JsonDiffer
3
+
4
+ # Delegates to https://github.com/pact-foundation/pact-support/blob/master/lib/pact/matchers/matchers.rb#L25
5
+ def self.call expected, actual, options = {}
6
+ require 'pact/matchers' # avoid recursive loop between this file and pact/matchers
7
+ ::Pact::Matchers.diff expected, actual, options
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ require 'pact/matchers/difference_indicator'
2
+
3
+ module Pact
4
+ class KeyNotFound < Pact::DifferenceIndicator
5
+
6
+ def to_s
7
+ "<key not found>"
8
+ end
9
+
10
+ def empty?
11
+ true
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,16 @@
1
+ require 'uri'
2
+ require 'pact/shared/text_differ'
3
+
4
+ module Pact
5
+ class MultipartFormDiffer
6
+ def self.call expected, actual, options = {}
7
+ require 'pact/matchers' # avoid recursive loop between this file and pact/matchers
8
+ expected_boundary = expected.split.first
9
+ actual_boundary = actual.split.first
10
+ actual_with_hardcoded_boundary = actual.gsub(actual_boundary, expected_boundary)
11
+ TextDiffer.call(expected, actual_with_hardcoded_boundary, options)
12
+ rescue StandardError
13
+ TextDiffer.call(expected, actual, options)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ module Pact
2
+ class NullExpectation
3
+ def to_s
4
+ "<No expectation>"
5
+ end
6
+
7
+ def ==(other_object)
8
+ other_object.is_a? NullExpectation
9
+ end
10
+
11
+ def ===(other_object)
12
+ other_object.is_a? NullExpectation
13
+ end
14
+
15
+ def eql?(other_object)
16
+ self == other_object
17
+ end
18
+
19
+ def hash
20
+ 2934820948209428748274238642672
21
+ end
22
+
23
+ def empty?
24
+ true
25
+ end
26
+
27
+ def nil?
28
+ true
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,106 @@
1
+ require 'pact/symbolize_keys'
2
+ require 'pact/consumer_contract/headers'
3
+ require 'pact/consumer_contract/query'
4
+
5
+ module Pact
6
+ module Request
7
+ class Base
8
+ include Pact::SymbolizeKeys
9
+
10
+ attr_reader :method, :path, :headers, :body, :query, :options
11
+
12
+ def initialize(method, path, headers, body, query)
13
+ @method = method.to_s
14
+ @path = path
15
+ @headers = Hash === headers ? Headers.new(headers) : headers # Could be a NullExpectation - TODO make this more elegant
16
+ @body = body
17
+ set_query(query)
18
+ end
19
+
20
+ def to_hash
21
+ hash = {
22
+ method: method,
23
+ path: path,
24
+ }
25
+ hash[:query] = query if specified?(:query)
26
+ hash[:headers] = headers if specified?(:headers)
27
+ hash[:body] = body if specified?(:body)
28
+ hash
29
+ end
30
+
31
+ def method_and_path
32
+ "#{method.upcase} #{full_path}"
33
+ end
34
+
35
+ def full_path
36
+ display_path + display_query
37
+ end
38
+
39
+ def content_type
40
+ return nil unless specified?(:headers) && headers['Content-Type']
41
+ Pact::Reification.from_term(headers['Content-Type'])
42
+ end
43
+
44
+ def content_type? content_type
45
+ self.content_type == content_type
46
+ end
47
+
48
+ def modifies_resource?
49
+ http_method_modifies_resource? && body_specified?
50
+ end
51
+
52
+ def specified? key
53
+ !is_unspecified?(self.send(key))
54
+ end
55
+
56
+ protected
57
+
58
+ # Not including DELETE, as we don't care about the resources updated state.
59
+ def http_method_modifies_resource?
60
+ ['PUT','POST','PATCH'].include?(method.to_s.upcase)
61
+ end
62
+
63
+ def self.key_not_found
64
+ raise NotImplementedError
65
+ end
66
+
67
+ def body_specified?
68
+ specified?(:body)
69
+ end
70
+
71
+ def is_unspecified? value
72
+ value.is_a? self.class.key_not_found.class
73
+ end
74
+
75
+ def to_hash_without_body_or_query
76
+ hash = {
77
+ method: method.upcase,
78
+ path: path
79
+ }
80
+ hash[:headers] = headers if specified?(:headers)
81
+ hash
82
+ end
83
+
84
+ def display_path
85
+ reified_path = Pact::Reification.from_term(path)
86
+ reified_path.empty? ? "/" : reified_path
87
+ end
88
+
89
+ def display_query
90
+ (query.nil? || query.empty?) ? '' : "?#{Pact::Reification.from_term(query)}"
91
+ end
92
+
93
+ def set_query(query)
94
+ @query = if is_unspecified?(query)
95
+ query
96
+ else
97
+ if Pact::Query.is_a_query_object?(query)
98
+ query
99
+ else
100
+ Pact::Query.create(query)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,11 @@
1
+
2
+ module Pact
3
+ class TextDiffer
4
+
5
+ def self.call expected, actual, options = {}
6
+ require 'pact/matchers' # avoid recursive loop between this file and pact/matchers
7
+ ::Pact::Matchers.diff expected, actual, options
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,49 @@
1
+ require 'pact/symbolize_keys'
2
+ module Pact
3
+
4
+ # Specifies that the actual object should be considered a match if
5
+ # it includes the same keys, and the values of the keys are of the same class.
6
+
7
+ class SomethingLike
8
+ include SymbolizeKeys
9
+
10
+ attr_reader :contents
11
+
12
+ def initialize contents
13
+ @contents = contents
14
+ end
15
+
16
+ def to_hash
17
+ {
18
+ :json_class => self.class.name,
19
+ :contents => contents
20
+ }
21
+ end
22
+
23
+ def as_json opts = {}
24
+ to_hash
25
+ end
26
+
27
+ def to_json opts = {}
28
+ as_json.to_json opts
29
+ end
30
+
31
+ def self.json_create hash
32
+ new(symbolize_keys(hash)[:contents])
33
+ end
34
+
35
+ def eq other
36
+ self == other
37
+ end
38
+
39
+ def == other
40
+ other.is_a?(SomethingLike) && other.contents == self.contents
41
+ end
42
+
43
+ def generate
44
+ contents
45
+ end
46
+ end
47
+ end
48
+
49
+
@@ -0,0 +1,18 @@
1
+ module Pact
2
+ class SpecificationVersion < Gem::Version
3
+
4
+ def major
5
+ segments.first
6
+ end
7
+
8
+ def === other
9
+ major && major == other
10
+ end
11
+
12
+ def after? other
13
+ major && other < major
14
+ end
15
+ end
16
+
17
+ SpecificationVersion::NIL_VERSION = Pact::SpecificationVersion.new('0')
18
+ end
@@ -0,0 +1,5 @@
1
+ module Pact
2
+ module Support
3
+ VERSION = "1.17.1"
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ require 'pact/support/version'
2
+ require 'pact/consumer_contract'
3
+ require 'pact/matchers'
4
+ require 'pact/logging'
5
+ require 'pact/term'
6
+ require 'pact/helpers'
7
+ require 'pact/configuration'
8
+ require 'pact/reification'
9
+
10
+ module Pact
11
+ include Pact::Helpers
12
+ end
@@ -0,0 +1,13 @@
1
+ module Pact
2
+ module SymbolizeKeys
3
+
4
+ def self.included(base)
5
+ base.extend(self)
6
+ end
7
+
8
+ def symbolize_keys hash
9
+ return nil if hash.nil?
10
+ hash.inject({}) { |memo, (k,v)| memo[k.to_sym] = v; memo }
11
+ end
12
+ end
13
+ end
data/lib/pact/term.rb ADDED
@@ -0,0 +1,85 @@
1
+ require 'pact/shared/active_support_support'
2
+ require 'json/add/regexp'
3
+ require 'pact/errors'
4
+
5
+ module Pact
6
+ class Term
7
+
8
+ include Pact::ActiveSupportSupport
9
+
10
+ attr_reader :generate, :matcher
11
+
12
+ def self.json_create(obj)
13
+ new(generate: obj['data']['generate'], matcher: obj['data']['matcher'])
14
+ end
15
+
16
+ def self.unpack_regexps source
17
+ case source
18
+ when Pact::Term then source.matcher
19
+ when Array then unpack_regexps_from_array source
20
+ when Hash then unpack_regexps_from_hash source
21
+ else
22
+ source
23
+ end
24
+ end
25
+
26
+ def initialize(attributes = {})
27
+ @generate = attributes[:generate]
28
+ @matcher = attributes[:matcher]
29
+ raise Pact::Error.new("Please specify a matcher for the Term") unless @matcher != nil
30
+ raise Pact::Error.new("Please specify a value to generate for the Term") unless @generate != nil
31
+ raise Pact::Error.new("Value to generate \"#{@generate}\" does not match regular expression #{@matcher.inspect}") unless @generate =~ @matcher
32
+ end
33
+
34
+ def to_hash
35
+ { json_class: self.class.name, data: { generate: generate, matcher: fix_regexp(matcher) } }
36
+ end
37
+
38
+ def as_json(options = {})
39
+ to_hash
40
+ end
41
+
42
+
43
+ def to_json(options = {})
44
+ as_json.to_json(options)
45
+ end
46
+
47
+ def match(literal)
48
+ literal.respond_to?(:to_s) ? matcher.match(literal.to_s) : nil
49
+ end
50
+
51
+ def ==(other)
52
+ return false unless other.respond_to?(:generate) && other.respond_to?(:matcher)
53
+ generate == other.generate && matcher == other.matcher
54
+ end
55
+
56
+ def to_s
57
+ "Pact::Term matcher: #{matcher.inspect}" + (generate.nil? ? "" : " generate: \"#{generate}\"")
58
+ end
59
+
60
+ def diff_with_actual(actual)
61
+ match(actual) ? nil : {
62
+ expected: self,
63
+ actual: actual
64
+ }
65
+ end
66
+
67
+ def empty?
68
+ false
69
+ end
70
+
71
+ private
72
+
73
+ def self.unpack_regexps_from_array source
74
+ source.each_with_object([]) do | item, destination |
75
+ destination << unpack_regexps(item)
76
+ end
77
+ end
78
+
79
+ def self.unpack_regexps_from_hash source
80
+ source.keys.each_with_object({}) do | key, destination |
81
+ destination[key] = unpack_regexps source[key]
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,29 @@
1
+
2
+ namespace :pact do
3
+
4
+ desc "Verifies the pact files configured in the pact_helper.rb against this service provider."
5
+ task :verify do
6
+
7
+ require 'pact/tasks/task_helper'
8
+
9
+ include Pact::TaskHelper
10
+
11
+ handle_verification_failure do
12
+ execute_pact_verify
13
+ end
14
+ end
15
+
16
+ desc "Verifies the pact at the given URI against this service provider."
17
+ task 'verify:at', :pact_uri do | t, args |
18
+ require 'rainbow'
19
+ require 'pact/tasks/task_helper'
20
+
21
+ include Pact::TaskHelper
22
+
23
+ abort(Rainbow("Please provide a pact URI. eg. rake pact:verify:at[../my-consumer/spec/pacts/my_consumer-my_provider.json]").red) unless args[:pact_uri]
24
+ handle_verification_failure do
25
+ execute_pact_verify args[:pact_uri]
26
+ end
27
+ end
28
+
29
+ end