pact-support 0.0.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 (107) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +29 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +8 -0
  5. data/CHANGELOG.md +4 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +80 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +4 -0
  10. data/Rakefile +4 -0
  11. data/lib/pact/configuration.rb +164 -0
  12. data/lib/pact/consumer/request.rb +27 -0
  13. data/lib/pact/consumer_contract.rb +1 -0
  14. data/lib/pact/consumer_contract/consumer_contract.rb +114 -0
  15. data/lib/pact/consumer_contract/file_name.rb +19 -0
  16. data/lib/pact/consumer_contract/headers.rb +51 -0
  17. data/lib/pact/consumer_contract/interaction.rb +73 -0
  18. data/lib/pact/consumer_contract/pact_file.rb +24 -0
  19. data/lib/pact/consumer_contract/request.rb +73 -0
  20. data/lib/pact/consumer_contract/response.rb +51 -0
  21. data/lib/pact/consumer_contract/service_consumer.rb +28 -0
  22. data/lib/pact/consumer_contract/service_provider.rb +28 -0
  23. data/lib/pact/logging.rb +14 -0
  24. data/lib/pact/matchers.rb +1 -0
  25. data/lib/pact/matchers/actual_type.rb +16 -0
  26. data/lib/pact/matchers/base_difference.rb +37 -0
  27. data/lib/pact/matchers/differ.rb +153 -0
  28. data/lib/pact/matchers/difference.rb +13 -0
  29. data/lib/pact/matchers/difference_indicator.rb +26 -0
  30. data/lib/pact/matchers/embedded_diff_formatter.rb +62 -0
  31. data/lib/pact/matchers/expected_type.rb +35 -0
  32. data/lib/pact/matchers/index_not_found.rb +15 -0
  33. data/lib/pact/matchers/list_diff_formatter.rb +101 -0
  34. data/lib/pact/matchers/matchers.rb +139 -0
  35. data/lib/pact/matchers/no_diff_indicator.rb +18 -0
  36. data/lib/pact/matchers/regexp_difference.rb +13 -0
  37. data/lib/pact/matchers/type_difference.rb +16 -0
  38. data/lib/pact/matchers/unexpected_index.rb +11 -0
  39. data/lib/pact/matchers/unexpected_key.rb +11 -0
  40. data/lib/pact/matchers/unix_diff_formatter.rb +114 -0
  41. data/lib/pact/reification.rb +28 -0
  42. data/lib/pact/rspec.rb +53 -0
  43. data/lib/pact/shared/active_support_support.rb +51 -0
  44. data/lib/pact/shared/dsl.rb +76 -0
  45. data/lib/pact/shared/jruby_support.rb +18 -0
  46. data/lib/pact/shared/json_differ.rb +15 -0
  47. data/lib/pact/shared/key_not_found.rb +15 -0
  48. data/lib/pact/shared/null_expectation.rb +31 -0
  49. data/lib/pact/shared/request.rb +97 -0
  50. data/lib/pact/shared/text_differ.rb +14 -0
  51. data/lib/pact/something_like.rb +49 -0
  52. data/lib/pact/support.rb +9 -0
  53. data/lib/pact/support/version.rb +5 -0
  54. data/lib/pact/symbolize_keys.rb +12 -0
  55. data/lib/pact/term.rb +85 -0
  56. data/lib/tasks/pact.rake +29 -0
  57. data/pact-support.gemspec +35 -0
  58. data/spec/lib/pact/consumer/request_spec.rb +25 -0
  59. data/spec/lib/pact/consumer_contract/active_support_support_spec.rb +58 -0
  60. data/spec/lib/pact/consumer_contract/consumer_contract_spec.rb +141 -0
  61. data/spec/lib/pact/consumer_contract/headers_spec.rb +107 -0
  62. data/spec/lib/pact/consumer_contract/interaction_spec.rb +151 -0
  63. data/spec/lib/pact/consumer_contract/request_spec.rb +329 -0
  64. data/spec/lib/pact/consumer_contract/response_spec.rb +73 -0
  65. data/spec/lib/pact/matchers/differ_spec.rb +215 -0
  66. data/spec/lib/pact/matchers/difference_spec.rb +22 -0
  67. data/spec/lib/pact/matchers/embedded_diff_formatter_spec.rb +90 -0
  68. data/spec/lib/pact/matchers/index_not_found_spec.rb +21 -0
  69. data/spec/lib/pact/matchers/list_diff_formatter_spec.rb +120 -0
  70. data/spec/lib/pact/matchers/matchers_spec.rb +500 -0
  71. data/spec/lib/pact/matchers/regexp_difference_spec.rb +20 -0
  72. data/spec/lib/pact/matchers/type_difference_spec.rb +34 -0
  73. data/spec/lib/pact/matchers/unexpected_index_spec.rb +20 -0
  74. data/spec/lib/pact/matchers/unexpected_key_spec.rb +20 -0
  75. data/spec/lib/pact/matchers/unix_diff_formatter_spec.rb +216 -0
  76. data/spec/lib/pact/reification_spec.rb +67 -0
  77. data/spec/lib/pact/shared/dsl_spec.rb +86 -0
  78. data/spec/lib/pact/shared/json_differ_spec.rb +36 -0
  79. data/spec/lib/pact/shared/key_not_found_spec.rb +20 -0
  80. data/spec/lib/pact/shared/request_spec.rb +196 -0
  81. data/spec/lib/pact/shared/text_differ_spec.rb +54 -0
  82. data/spec/lib/pact/something_like_spec.rb +21 -0
  83. data/spec/lib/pact/term_spec.rb +89 -0
  84. data/spec/spec_helper.rb +19 -0
  85. data/spec/support/a_consumer-a_producer.json +32 -0
  86. data/spec/support/a_consumer-a_provider.json +32 -0
  87. data/spec/support/active_support_if_configured.rb +6 -0
  88. data/spec/support/case-insensitive-response-header-matching.json +21 -0
  89. data/spec/support/consumer_contract_template.json +24 -0
  90. data/spec/support/dsl_spec_support.rb +7 -0
  91. data/spec/support/factories.rb +82 -0
  92. data/spec/support/generated_index.md +4 -0
  93. data/spec/support/generated_markdown.md +55 -0
  94. data/spec/support/interaction_view_model.json +63 -0
  95. data/spec/support/interaction_view_model_with_terms.json +50 -0
  96. data/spec/support/markdown_pact.json +48 -0
  97. data/spec/support/missing_provider_states_output.txt +25 -0
  98. data/spec/support/options.json +21 -0
  99. data/spec/support/shared_examples_for_request.rb +94 -0
  100. data/spec/support/spec_support.rb +20 -0
  101. data/spec/support/stubbing.json +22 -0
  102. data/spec/support/term.json +48 -0
  103. data/spec/support/test_app_fail.json +61 -0
  104. data/spec/support/test_app_pass.json +38 -0
  105. data/spec/support/test_app_with_right_content_type_differ.json +23 -0
  106. data/tasks/spec.rake +6 -0
  107. metadata +401 -0
@@ -0,0 +1,19 @@
1
+ module Pact
2
+
3
+ module FileName
4
+
5
+ extend self
6
+
7
+ def file_name consumer_name, provider_name
8
+ "#{filenamify(consumer_name)}-#{filenamify(provider_name)}.json"
9
+ end
10
+
11
+ def file_path consumer_name, provider_name, pact_dir = Pact.configuration.pact_dir
12
+ File.join(pact_dir, file_name(consumer_name, provider_name))
13
+ end
14
+
15
+ def filenamify name
16
+ name.downcase.gsub(/\s/, '_')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ module Pact
2
+
3
+ class DuplicateHeaderError < StandardError; end
4
+ class InvalidHeaderNameTypeError < StandardError; end
5
+
6
+ class Headers < Hash
7
+
8
+ def initialize hash = {}
9
+ hash.each_pair do | key, value |
10
+ check_for_invalid key
11
+ self[find_matching_key(key)] = value
12
+ end
13
+ self.freeze
14
+ end
15
+
16
+ def [] key
17
+ super(find_matching_key(key))
18
+ end
19
+
20
+ def fetch *args, &block
21
+ args[0] = find_matching_key(args[0]) if args.first
22
+ super(*args, &block)
23
+ end
24
+
25
+ def key? key
26
+ super(find_matching_key(key))
27
+ end
28
+
29
+ alias_method :has_key?, :key?
30
+ alias_method :include?, :key?
31
+
32
+ private
33
+
34
+ def find_matching_key key
35
+ key = key.to_s
36
+ match = keys.find { |k| k.downcase == key.downcase }
37
+ match.nil? ? key : match
38
+ end
39
+
40
+ def check_for_invalid key
41
+ unless (String === key || Symbol === key)
42
+ raise InvalidHeaderNameTypeError.new "Header name (#{key}) must be a String or a Symbol."
43
+ end
44
+ if key? key
45
+ raise DuplicateHeaderError.new "Duplicate header found (#{find_matching_key(key)} and #{key}. Please use a comma separated single value when multiple headers with the same name are required."
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,73 @@
1
+ require 'pact/consumer_contract/request'
2
+ require 'pact/consumer_contract/response'
3
+ require 'pact/symbolize_keys'
4
+ require 'pact/shared/active_support_support'
5
+
6
+ module Pact
7
+ class Interaction
8
+ include ActiveSupportSupport
9
+ include SymbolizeKeys
10
+
11
+ attr_accessor :description, :request, :response, :provider_state
12
+
13
+ def initialize attributes = {}
14
+ @description = attributes[:description]
15
+ @request = attributes[:request]
16
+ @response = attributes[:response]
17
+ @provider_state = attributes[:provider_state] || attributes[:providerState]
18
+ end
19
+
20
+ def self.from_hash hash
21
+ request = Pact::Request::Expected.from_hash(hash['request'])
22
+ response = Pact::Response.from_hash(hash['response'])
23
+ new(symbolize_keys(hash).merge({request: request, response: response}))
24
+ end
25
+
26
+ def to_hash
27
+ hash = { :description => @description }
28
+ hash[:provider_state] = @provider_state if @provider_state #Easier to read when provider state at top
29
+ hash.merge(:request => @request.as_json, :response => @response)
30
+ end
31
+
32
+ def as_json options = {}
33
+ fix_all_the_things to_hash
34
+ end
35
+
36
+ def to_json(options = {})
37
+ as_json.to_json(options)
38
+ end
39
+
40
+ def matches_criteria? criteria
41
+ criteria.each do | key, value |
42
+ unless match_criterion self.send(key.to_s), value
43
+ return false
44
+ end
45
+ end
46
+ true
47
+ end
48
+
49
+ def match_criterion target, criterion
50
+ target == criterion || (criterion.is_a?(Regexp) && criterion.match(target))
51
+ end
52
+
53
+ def == other
54
+ other.is_a?(Interaction) && as_json == other.as_json
55
+ end
56
+
57
+ def eq? other
58
+ self == other
59
+ end
60
+
61
+ def description_with_provider_state_quoted
62
+ provider_state ? "\"#{description}\" given \"#{provider_state}\"" : "\"#{description}\""
63
+ end
64
+
65
+ def request_modifies_resource_without_checking_response_body?
66
+ request.modifies_resource? && response.body_allows_any_value?
67
+ end
68
+
69
+ def to_s
70
+ JSON.pretty_generate(self)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,24 @@
1
+ module Pact
2
+
3
+ module PactFile
4
+
5
+ extend self
6
+
7
+ def read uri, options = {}
8
+ pact = open(uri.to_s) { | file | file.read }
9
+ if options[:save_pactfile_to_tmp]
10
+ save_pactfile_to_tmp pact, ::File.basename(uri.to_s)
11
+ end
12
+ pact
13
+ rescue StandardError => e
14
+ $stderr.puts "Error reading file from #{uri}"
15
+ $stderr.puts "#{e.to_s} #{e.backtrace.join("\n")}"
16
+ raise e
17
+ end
18
+
19
+ def save_pactfile_to_tmp pact, name
20
+ ::FileUtils.mkdir_p Pact.configuration.tmp_dir
21
+ ::File.open(Pact.configuration.tmp_dir + "/#{name}", "w") { |file| file << pact}
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,73 @@
1
+ require 'pact/shared/request'
2
+ require 'pact/shared/null_expectation'
3
+
4
+ module Pact
5
+
6
+ module Request
7
+
8
+ class Expected < Pact::Request::Base
9
+
10
+ DEFAULT_OPTIONS = {:allow_unexpected_keys => false}.freeze
11
+ attr_accessor :options #Temporary hack
12
+
13
+ def self.from_hash(hash)
14
+ sym_hash = symbolize_keys hash
15
+ method = sym_hash.fetch(:method)
16
+ path = sym_hash.fetch(:path)
17
+ query = sym_hash.fetch(:query, key_not_found)
18
+ headers = sym_hash.fetch(:headers, key_not_found)
19
+ body = sym_hash.fetch(:body, key_not_found)
20
+ options = sym_hash.fetch(:options, {})
21
+ new(method, path, headers, body, query, options)
22
+ end
23
+
24
+ def initialize(method, path, headers, body, query, options = {})
25
+ super(method, path, headers, body, query)
26
+ @options = options
27
+ end
28
+
29
+ def matches?(actual_request)
30
+ difference(actual_request).empty?
31
+ end
32
+
33
+ def matches_route? actual_request
34
+ diff({:method => method, :path => path}, {:method => actual_request.method, :path => actual_request.path}).empty?
35
+ end
36
+
37
+ def difference(actual_request)
38
+ request_diff = diff(to_hash_without_body, actual_request.to_hash_without_body)
39
+ unless body.is_a? NullExpectation
40
+ request_diff.merge(body_difference(actual_request.body))
41
+ else
42
+ request_diff
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ def self.key_not_found
49
+ Pact::NullExpectation.new
50
+ end
51
+
52
+ private
53
+
54
+ # Options is a dirty hack to allow Condor to send extra keys in the request,
55
+ # as it's too much work to set up an exactly matching expectation.
56
+ # Need to implement a proper matching strategy and remove this.
57
+ # Do not rely on it!
58
+ def runtime_options
59
+ DEFAULT_OPTIONS.merge(symbolize_keys(options))
60
+ end
61
+
62
+ def body_difference(actual_body)
63
+ body_differ.call({:body => body}, {body: actual_body}, allow_unexpected_keys: runtime_options[:allow_unexpected_keys_in_body])
64
+ end
65
+
66
+ def body_differ
67
+ Pact.configuration.body_differ_for_content_type content_type
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,51 @@
1
+ require 'pact/consumer_contract/headers'
2
+ require 'pact/symbolize_keys'
3
+
4
+ module Pact
5
+
6
+ class Response < Hash
7
+
8
+ include SymbolizeKeys
9
+
10
+ def initialize attributes
11
+ merge!(attributes)
12
+ end
13
+
14
+ def status
15
+ self[:status]
16
+ end
17
+
18
+ def headers
19
+ self[:headers]
20
+ end
21
+
22
+ def body
23
+ self[:body]
24
+ end
25
+
26
+ def body_allows_any_value?
27
+ body_not_specified? || body_is_empty_hash?
28
+ end
29
+
30
+ def [] key
31
+ super key.to_sym
32
+ end
33
+
34
+ def self.from_hash hash
35
+ headers = Headers.new(hash[:headers] || hash['headers'] || {})
36
+ new(symbolize_keys(hash).merge(headers: headers))
37
+ end
38
+
39
+ private
40
+
41
+ def body_is_empty_hash?
42
+ body.is_a?(Hash) && body.empty?
43
+ end
44
+
45
+ def body_not_specified?
46
+ !self.key?(:body)
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,28 @@
1
+ require 'pact/symbolize_keys'
2
+
3
+ module Pact
4
+ class ServiceConsumer
5
+ include SymbolizeKeys
6
+
7
+ attr_accessor :name
8
+ def initialize options
9
+ @name = options[:name]
10
+ end
11
+
12
+ def to_s
13
+ name
14
+ end
15
+
16
+ def to_hash
17
+ {name: name}
18
+ end
19
+
20
+ def as_json options = {}
21
+ to_hash
22
+ end
23
+
24
+ def self.from_hash hash
25
+ new(symbolize_keys(hash))
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ require 'pact/symbolize_keys'
2
+
3
+ module Pact
4
+ class ServiceProvider
5
+ include SymbolizeKeys
6
+
7
+ attr_accessor :name
8
+ def initialize options
9
+ @name = options[:name] || '[provider name unknown - please update the pact gem in the consumer project to the latest version and regenerate the pacts]'
10
+ end
11
+
12
+ def to_s
13
+ name
14
+ end
15
+
16
+ def to_hash
17
+ {name: name}
18
+ end
19
+
20
+ def as_json options = {}
21
+ to_hash
22
+ end
23
+
24
+ def self.from_hash hash
25
+ new(symbolize_keys(hash))
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ require 'logger'
2
+ require_relative 'configuration'
3
+
4
+ module Pact
5
+ module Logging
6
+ def self.included(base)
7
+ base.extend(self)
8
+ end
9
+
10
+ def logger
11
+ Pact.configuration.logger
12
+ end
13
+ end
14
+ end
@@ -0,0 +1 @@
1
+ require_relative 'matchers/matchers'
@@ -0,0 +1,16 @@
1
+ require 'pact/matchers/expected_type'
2
+
3
+
4
+ module Pact
5
+ class ActualType < Pact::ExpectedType
6
+
7
+ def initialize value
8
+ @value = value
9
+ end
10
+
11
+ def to_s
12
+ type
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ require 'pact/matchers/expected_type'
2
+ require 'pact/matchers/actual_type'
3
+
4
+ module Pact
5
+ module Matchers
6
+ class BaseDifference
7
+
8
+ attr_reader :expected, :actual
9
+
10
+ def initialize expected, actual
11
+ @expected = expected
12
+ @actual = actual
13
+ end
14
+
15
+ def any?
16
+ true
17
+ end
18
+
19
+ def empty?
20
+ false
21
+ end
22
+
23
+ def to_json options = {}
24
+ as_json.to_json(options)
25
+ end
26
+
27
+ def to_s
28
+ as_json.to_s
29
+ end
30
+
31
+ def == other
32
+ other.class == self.class && other.expected == expected && other.actual == actual
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,153 @@
1
+ # Ripped from RSpec::Expectations::Differ in rspec/expectations/differ.rb in rspec-expectations 2.14.3
2
+ # Thank you kindly to the original author.
3
+ # Needed to be able to turn the colour off, but can't set RSpec.configuration.color to false
4
+ # once it has been set to true due to a "if bool" at the start of the color= method
5
+
6
+ require 'diff/lcs'
7
+ require 'diff/lcs/hunk'
8
+ require 'pp'
9
+
10
+ module Pact
11
+ module Matchers
12
+ class Differ
13
+
14
+ def initialize(color = false)
15
+ @color = color
16
+ end
17
+
18
+ # This is snagged from diff/lcs/ldiff.rb (which is a commandline tool)
19
+ def diff_as_string(input_data_new, input_data_old)
20
+ output = matching_encoding("", input_data_old)
21
+ data_old = input_data_old.split(matching_encoding("\n", input_data_old)).map! { |e| e.chomp }
22
+ data_new = input_data_new.split(matching_encoding("\n", input_data_new)).map! { |e| e.chomp }
23
+ diffs = Diff::LCS.diff(data_old, data_new)
24
+ return output if diffs.empty?
25
+ oldhunk = hunk = nil
26
+ file_length_difference = 0
27
+ diffs.each do |piece|
28
+ begin
29
+ hunk = Diff::LCS::Hunk.new(
30
+ data_old, data_new, piece, context_lines, file_length_difference
31
+ )
32
+ file_length_difference = hunk.file_length_difference
33
+ next unless oldhunk
34
+ # Hunks may overlap, which is why we need to be careful when our
35
+ # diff includes lines of context. Otherwise, we might print
36
+ # redundant lines.
37
+ if (context_lines > 0) and hunk.overlaps?(oldhunk)
38
+ if hunk.respond_to?(:merge)
39
+ # diff-lcs 1.2.x
40
+ hunk.merge(oldhunk)
41
+ else
42
+ # diff-lcs 1.1.3
43
+ hunk.unshift(oldhunk)
44
+ end
45
+ else
46
+ output << matching_encoding(oldhunk.diff(format).to_s, output)
47
+ end
48
+ ensure
49
+ oldhunk = hunk
50
+ output << matching_encoding("\n", output)
51
+ end
52
+ end
53
+ #Handle the last remaining hunk
54
+ output << matching_encoding(oldhunk.diff(format).to_s,output)
55
+ output << matching_encoding("\n",output)
56
+ color_diff output
57
+ rescue Encoding::CompatibilityError
58
+ if input_data_new.encoding != input_data_old.encoding
59
+ "Could not produce a diff because the encoding of the actual string (#{input_data_old.encoding}) "+
60
+ "differs from the encoding of the expected string (#{input_data_new.encoding})"
61
+ else
62
+ "Could not produce a diff because of the encoding of the string (#{input_data_old.encoding})"
63
+ end
64
+ end
65
+
66
+ def diff_as_object(actual, expected)
67
+ actual_as_string = object_to_string(actual)
68
+ expected_as_string = object_to_string(expected)
69
+ if diff = diff_as_string(actual_as_string, expected_as_string)
70
+ color_diff diff
71
+ end
72
+ end
73
+
74
+ def red(text)
75
+ return text unless @color
76
+ color(text, 31)
77
+ end
78
+
79
+ def green(text)
80
+ return text unless @color
81
+ color(text, 32)
82
+ end
83
+
84
+ protected
85
+
86
+ def format
87
+ :unified
88
+ end
89
+
90
+ def context_lines
91
+ 3
92
+ end
93
+
94
+ def color(text, color_code)
95
+ "\e[#{color_code}m#{text}\e[0m"
96
+ end
97
+
98
+
99
+ def blue(text)
100
+ color(text, 34)
101
+ end
102
+
103
+ def color_diff(diff)
104
+ return diff unless @color
105
+
106
+ diff.lines.map { |line|
107
+ case line[0].chr
108
+ when "+"
109
+ green line
110
+ when "-"
111
+ red line
112
+ when "@"
113
+ line[1].chr == "@" ? blue(line) : line
114
+ else
115
+ line
116
+ end
117
+ }.join
118
+ end
119
+
120
+ def object_to_string(object)
121
+ case object
122
+ when Hash
123
+ object.keys.sort_by { |k| k.to_s }.map do |key|
124
+ pp_key = PP.singleline_pp(key, "")
125
+ pp_value = PP.singleline_pp(object[key], "")
126
+
127
+ # on 1.9.3 PP seems to minimise to US-ASCII, ensure we're matching source encoding
128
+ #
129
+ # note, PP is used to ensure the ordering of the internal values of key/value e.g.
130
+ # <# a: b: c:> not <# c: a: b:>
131
+ matching_encoding("#{pp_key} => #{pp_value}", key.to_s)
132
+ end.join(",\n")
133
+ when String
134
+ object =~ /\n/ ? object : object.inspect
135
+ else
136
+ PP.pp(object,"")
137
+ end
138
+ end
139
+
140
+ if String.method_defined?(:encoding)
141
+ def matching_encoding(string, source)
142
+ string.encode(source.encoding)
143
+ end
144
+ else
145
+ def matching_encoding(string, source)
146
+ string
147
+ end
148
+ end
149
+ end
150
+
151
+ end
152
+ end
153
+