pact 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.travis.yml +1 -1
  2. data/CHANGELOG.md +14 -0
  3. data/Gemfile.lock +5 -5
  4. data/README.md +25 -29
  5. data/documentation/README.md +9 -7
  6. data/lib/pact/configuration.rb +83 -12
  7. data/lib/pact/consumer/mock_service/interaction_mismatch.rb +5 -1
  8. data/lib/pact/consumer/spec_hooks.rb +7 -4
  9. data/lib/pact/consumer/world.rb +12 -0
  10. data/lib/pact/consumer_contract/headers.rb +51 -0
  11. data/lib/pact/consumer_contract/request.rb +6 -1
  12. data/lib/pact/provider/configuration/service_provider_dsl.rb +6 -1
  13. data/lib/pact/provider/matchers/messages.rb +2 -2
  14. data/lib/pact/provider/rspec.rb +7 -1
  15. data/lib/pact/provider/rspec/backtrace_formatter.rb +30 -6
  16. data/lib/pact/provider/rspec/matchers.rb +9 -6
  17. data/lib/pact/shared/json_differ.rb +15 -0
  18. data/lib/pact/shared/request.rb +11 -2
  19. data/lib/pact/shared/text_differ.rb +14 -0
  20. data/lib/pact/version.rb +1 -1
  21. data/spec/lib/pact/configuration_spec.rb +127 -5
  22. data/spec/lib/pact/consumer/mock_service/interaction_mismatch_spec.rb +8 -5
  23. data/spec/lib/pact/consumer_contract/headers_spec.rb +107 -0
  24. data/spec/lib/pact/consumer_contract/request_spec.rb +9 -0
  25. data/spec/lib/pact/matchers/matchers_spec.rb +35 -0
  26. data/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb +16 -0
  27. data/spec/lib/pact/provider/matchers/messages_spec.rb +5 -4
  28. data/spec/lib/pact/provider/rspec_spec.rb +15 -11
  29. data/spec/lib/pact/shared/json_differ_spec.rb +36 -0
  30. data/spec/lib/pact/shared/request_spec.rb +25 -1
  31. data/spec/lib/pact/shared/text_differ_spec.rb +54 -0
  32. data/spec/support/pact_helper.rb +2 -0
  33. data/spec/support/test_app_with_right_content_type_differ.json +23 -0
  34. data/tasks/pact-test.rake +6 -0
  35. metadata +72 -30
  36. checksums.yaml +0 -7
  37. data/documentation/Testing with pact.png +0 -0
  38. data/documentation/best-practices.md +0 -33
  39. data/documentation/development-workflow.md +0 -22
  40. data/documentation/faq.md +0 -81
  41. data/documentation/provider-states.md +0 -178
  42. data/documentation/raq.md +0 -39
  43. data/documentation/terminology.md +0 -25
  44. data/documentation/troubleshooting.md +0 -4
  45. data/documentation/verifying-pacts.md +0 -106
@@ -13,7 +13,12 @@ module Pact
13
13
 
14
14
  attr_accessor :name, :app_block
15
15
 
16
- CONFIG_RU_APP = lambda { Rack::Builder.parse_file(Pact.configuration.config_ru_path).first }
16
+ CONFIG_RU_APP = lambda {
17
+ unless File.exist? Pact.configuration.config_ru_path
18
+ raise "Could not find config.ru file at #{Pact.configuration.config_ru_path} Please configure the service provider app or create a config.ru file in the root directory of the project. See https://github.com/realestate-com-au/pact/blob/master/documentation/verifying-pacts.md for more information."
19
+ end
20
+ Rack::Builder.parse_file(Pact.configuration.config_ru_path).first
21
+ }
17
22
 
18
23
  def initialize name
19
24
  @name = name
@@ -5,9 +5,9 @@ module Pact
5
5
  module Matchers
6
6
  module Messages
7
7
 
8
- def match_term_failure_message diff, actual, color_enabled
8
+ def match_term_failure_message diff, actual, diff_formatter, color_enabled
9
9
  message = "Actual: #{(String === actual ? actual : actual.to_json)}\n\n"
10
- formatted_diff = Pact.configuration.diff_formatter.call(diff)
10
+ formatted_diff = diff_formatter.call(diff)
11
11
  message + colorize_if_enabled(formatted_diff, color_enabled)
12
12
  end
13
13
 
@@ -106,6 +106,12 @@ module Pact
106
106
  let(:response) { interaction_context.last_response }
107
107
  let(:response_status) { response.status }
108
108
  let(:response_body) { parse_body_from_response(response) }
109
+ let(:differ) { Pact.configuration.body_differ_for_content_type diff_content_type }
110
+ let(:diff_formatter) { Pact.configuration.diff_formatter_for_content_type diff_content_type }
111
+ let(:expected_content_type) { Pact::Headers.new(expected_response['headers'] || {})['Content-Type'] }
112
+ let(:actual_content_type) { response.headers['Content-Type']}
113
+ let(:diff_content_type) { String === expected_content_type ? expected_content_type : actual_content_type } # expected_content_type may be a Regexp
114
+ let(:options) { { with: differ, diff_formatter: diff_formatter } }
109
115
 
110
116
  if expected_response['status']
111
117
  it "has status code #{expected_response['status']}" do
@@ -126,7 +132,7 @@ module Pact
126
132
 
127
133
  if expected_response['body']
128
134
  it "has a matching body" do
129
- expect(response_body).to match_term expected_response_body
135
+ expect(response_body).to match_term expected_response_body, options
130
136
  end
131
137
  end
132
138
  end
@@ -1,19 +1,43 @@
1
1
  module RSpec
2
2
  module Core
3
3
 
4
+ # RSpec 3 has a hardwired @system_exclusion_patterns which removes everything matching /bin\//
5
+ # This causes *all* the backtrace lines to be cleaned, as rake pact:verify now shells out
6
+ # to the executable `pact verify ...`
7
+ # which then causes *all* the lines to be included as the BacktraceFormatter will
8
+ # include all lines of the backtrace if all lines were filtered out.
9
+ # This monkey patch only shows lines including bin/pact and removes the
10
+ # "show all lines if no lines would otherwise be shown" logic.
11
+
4
12
  class BacktraceFormatter
5
13
 
6
- # RSpec 3 has a hardwired @system_exclusion_patterns which removes everything matching /bin\//
7
- # This causes *all* the backtrace lines to be cleaned, as rake pact:verify now shells out
8
- # to the executable `pact verify ...`
9
- # which then causes *all* the lines to be included
14
+
15
+ def format_backtrace(backtrace, options = {})
16
+ return backtrace if options[:full_backtrace]
17
+ backtrace.map { |l| backtrace_line(l) }.compact
18
+ end
19
+
20
+ def backtrace_line(line)
21
+ relative_path(line) unless exclude?(line)
22
+ rescue SecurityError
23
+ nil
24
+ end
10
25
 
11
26
  def exclude?(line)
12
27
  return false if @full_backtrace
13
- matches_an_exclusion_pattern?(line) &&
14
- @inclusion_patterns.none? { |p| p =~ line }
28
+ relative_line = relative_path(line)
29
+ return true unless /bin\/pact/ =~ relative_line
15
30
  end
16
31
 
32
+ # Copied from Metadata so a refactor can't break this overridden class
33
+ def relative_path(line)
34
+ line = line.sub(File.expand_path("."), ".")
35
+ line = line.sub(/\A([^:]+:\d+)$/, '\\1')
36
+ return nil if line == '-e:1'
37
+ line
38
+ rescue SecurityError
39
+ nil
40
+ end
17
41
  end
18
42
  end
19
43
  end
@@ -2,6 +2,8 @@ require 'rspec'
2
2
  require 'pact/matchers'
3
3
  require 'pact/provider/matchers/messages'
4
4
  require 'pact/rspec'
5
+ require 'pact/shared/json_differ'
6
+
5
7
 
6
8
  module Pact
7
9
  module RSpec
@@ -20,27 +22,28 @@ module Pact
20
22
 
21
23
  class MatchTerm
22
24
 
23
- include Pact::Matchers
24
25
  include Pact::Matchers::Messages
25
26
  include RSpec2Delegator
26
27
 
27
- def initialize expected
28
+ def initialize expected, differ, diff_formatter
28
29
  @expected = expected
30
+ @differ = differ
31
+ @diff_formatter = diff_formatter
29
32
  end
30
33
 
31
34
  def matches? actual
32
35
  @actual = actual
33
- (@difference = diff(@expected, @actual)).empty?
36
+ (@difference = @differ.call(@expected, @actual)).empty?
34
37
  end
35
38
 
36
39
  def failure_message
37
- match_term_failure_message @difference, @actual, Pact::RSpec.color_enabled?
40
+ match_term_failure_message @difference, @actual, @diff_formatter, Pact::RSpec.color_enabled?
38
41
  end
39
42
 
40
43
  end
41
44
 
42
- def match_term expected
43
- MatchTerm.new(expected)
45
+ def match_term expected, options
46
+ MatchTerm.new(expected, options.fetch(:with), options.fetch(:diff_formatter))
44
47
  end
45
48
 
46
49
  class MatchHeader
@@ -0,0 +1,15 @@
1
+ require 'pact/matchers/matchers'
2
+
3
+ module Pact
4
+ class JsonDiffer
5
+
6
+ extend Matchers
7
+
8
+
9
+ def self.call expected, actual, options = {}
10
+ diff expected, actual, options
11
+ end
12
+
13
+
14
+ end
15
+ end
@@ -1,5 +1,6 @@
1
1
  require 'pact/matchers'
2
2
  require 'pact/symbolize_keys'
3
+ require 'pact/consumer_contract/headers'
3
4
 
4
5
  module Pact
5
6
 
@@ -15,7 +16,7 @@ module Pact
15
16
  def initialize(method, path, headers, body, query)
16
17
  @method = method.to_s
17
18
  @path = path.chomp('/')
18
- @headers = headers
19
+ @headers = Hash === headers ? Headers.new(headers) : headers # Could be a NullExpectation - TODO make this more elegant
19
20
  @body = body
20
21
  @query = query
21
22
  end
@@ -48,6 +49,11 @@ module Pact
48
49
  display_path + display_query
49
50
  end
50
51
 
52
+ def content_type
53
+ return nil if headers.is_a? self.class.key_not_found.class
54
+ headers['Content-Type']
55
+ end
56
+
51
57
  protected
52
58
 
53
59
  def self.key_not_found
@@ -56,7 +62,9 @@ module Pact
56
62
 
57
63
  def to_hash_without_body
58
64
  keep_keys = [:method, :path, :headers, :query]
59
- as_json.reject{ |key, value| !keep_keys.include? key }
65
+ as_json.reject{ |key, value| !keep_keys.include? key }.tap do | hash |
66
+ hash[:method] = method.upcase
67
+ end
60
68
  end
61
69
 
62
70
  def display_path
@@ -66,6 +74,7 @@ module Pact
66
74
  def display_query
67
75
  (query.nil? || query.empty?) ? '' : "?#{Pact::Reification.from_term(query)}"
68
76
  end
77
+
69
78
  end
70
79
  end
71
80
  end
@@ -0,0 +1,14 @@
1
+ require 'pact/matchers/matchers'
2
+ require 'pact/matchers/difference'
3
+
4
+ module Pact
5
+ class TextDiffer
6
+
7
+ extend Matchers
8
+
9
+ def self.call expected, actual, options = {}
10
+ diff expected, actual, options
11
+ end
12
+
13
+ end
14
+ end
data/lib/pact/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pact
2
- VERSION = "1.3.0"
2
+ VERSION = "1.3.1"
3
3
  end
@@ -83,12 +83,12 @@ describe Pact do
83
83
 
84
84
  end
85
85
 
86
- describe "#diff_formatter" do
86
+ describe "#diff_formatter_for_content_type" do
87
87
 
88
88
  let(:subject) { Pact::Configuration.new }
89
89
 
90
90
  it "returns the Pact::Matchers::UnixDiffFormatter by default" do
91
- expect(subject.diff_formatter).to eq(Pact::Matchers::UnixDiffFormatter)
91
+ expect(subject.diff_formatter_for_content_type 'anything').to eq(Pact::Matchers::UnixDiffFormatter)
92
92
  end
93
93
 
94
94
  Pact::Configuration::DIFF_FORMATTERS.each_pair do | key, diff_formatter |
@@ -100,7 +100,7 @@ describe Pact do
100
100
  end
101
101
 
102
102
  it "sets the diff_formatter to #{diff_formatter}" do
103
- expect(subject.diff_formatter).to be diff_formatter
103
+ expect(subject.diff_formatter_for_content_type nil).to be diff_formatter
104
104
  end
105
105
  end
106
106
 
@@ -115,18 +115,140 @@ describe Pact do
115
115
  end
116
116
 
117
117
  it "sets the diff_formatter to the object" do
118
- expect(subject.diff_formatter).to be diff_formatter
118
+ expect(subject.diff_formatter_for_content_type nil).to be diff_formatter
119
119
  end
120
120
  end
121
121
 
122
122
  context "when set to an object that does not respond to call and isn't a known default option" do
123
123
  it "raises an error" do
124
- expect { subject.diff_formatter = Object.new }.to raise_error "Pact.configuration.diff_formatter needs to respond to call, or be in the preconfigured list: [:embedded, :unix, :list]"
124
+ expect { subject.diff_formatter = Object.new }.to raise_error "Pact diff_formatter needs to respond to call, or be in the preconfigured list: [:embedded, :unix, :list]"
125
+ expect { subject.diff_formatter = Object.new }.to raise_error "Pact diff_formatter needs to respond to call, or be in the preconfigured list: [:embedded, :unix, :list]"
125
126
  end
126
127
  end
127
128
 
128
129
  end
129
130
 
131
+ describe "diff_formatter_for_content_type" do
132
+ let(:diff_formatter) { lambda { |expected, actual| }}
133
+ context "with the default configuration" do
134
+ context "when the content type is nil" do
135
+ it "returns the UnixDiffFormatter" do
136
+ expect(Pact.configuration.diff_formatter_for_content_type nil).to eq Pact::Matchers::UnixDiffFormatter
137
+ end
138
+ end
139
+ context "when the content type is application/json" do
140
+ it "returns the UnixDiffFormatter" do
141
+ expect(Pact.configuration.diff_formatter_for_content_type nil).to eq Pact::Matchers::UnixDiffFormatter
142
+ end
143
+ end
144
+ context "when the content type is text/plain" do
145
+ it "returns the UnixDiffFormatter" do
146
+ expect(Pact.configuration.diff_formatter_for_content_type nil).to eq Pact::Matchers::UnixDiffFormatter
147
+ end
148
+ end
149
+ end
150
+ context "with a custom diff_formatter registered for nil content type" do
151
+ context "when the content_type is nil" do
152
+ it "returns the custom diff_formatter" do
153
+ Pact.configuration.register_diff_formatter nil, diff_formatter
154
+ expect(Pact.configuration.diff_formatter_for_content_type nil).to eq diff_formatter
155
+ end
156
+ end
157
+ end
158
+ context "with a custom diff_formatter registered for json content type" do
159
+ context "when the content_type is application/json" do
160
+ it "returns the custom diff_formatter" do
161
+ Pact.configuration.register_diff_formatter /json/, diff_formatter
162
+ expect(Pact.configuration.diff_formatter_for_content_type 'application/json').to eq diff_formatter
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ describe "register_body_differ" do
169
+
170
+ let(:differ) { lambda{ |expected, actual| } }
171
+
172
+ context "with a string for a content type" do
173
+ it "configures the differ for the given content type" do
174
+ Pact.configure do | config |
175
+ config.register_body_differ 'application/xml', differ
176
+ end
177
+
178
+ expect(Pact.configuration.body_differ_for_content_type 'application/xml').to be differ
179
+ end
180
+ end
181
+
182
+ context "with a regexp for a content type" do
183
+ it "returns a matching differ" do
184
+ Pact.configuration.register_body_differ /application\/.*xml/, differ
185
+ expect(Pact.configuration.body_differ_for_content_type 'application/hal+xml').to be differ
186
+ end
187
+ end
188
+
189
+ context "when a non string or regexp is used to register a differ" do
190
+ it "raises an error" do
191
+ expect { Pact.configuration.register_body_differ 1, differ }.to raise_error /Invalid/
192
+ end
193
+ end
194
+
195
+ context "when something that does not respond to call is sumbitted as a differ" do
196
+ it "raises an error" do
197
+ expect { Pact.configuration.register_body_differ 'thing', Object.new }.to raise_error /responds to call/
198
+ end
199
+ end
200
+
201
+ context "when a nil content type is registered for responses without a content type header" do
202
+ it "returns that differ if the differ for a nil content type is requested" do
203
+ Pact.configuration.register_body_differ nil, differ
204
+ expect(Pact.configuration.body_differ_for_content_type(nil)).to be differ
205
+ end
206
+ end
207
+
208
+ end
209
+
210
+ describe "body_differ_for_content_type" do
211
+
212
+ let(:differ) { lambda { |expected, actual| }}
213
+
214
+ context "when 2 potentially matching content types have a differ registered" do
215
+ let(:differ_1) { lambda{ |expected, actual| } }
216
+ let(:differ_2) { lambda{ |expected, actual| } }
217
+
218
+ it "returns the differ that was configured first" do
219
+ Pact.configuration.register_body_differ /application\/.*xml/, differ_2
220
+ Pact.configuration.register_body_differ /application\/hal\+xml/, differ_1
221
+ expect(Pact.configuration.body_differ_for_content_type 'application/hal+xml').to be differ_2
222
+ end
223
+ end
224
+
225
+ context "when a nil content type is given" do
226
+ it "returns the text differ" do
227
+ expect(Pact.configuration.body_differ_for_content_type nil).to be Pact::TextDiffer
228
+ end
229
+ end
230
+
231
+ context "when no matching content type is found" do
232
+ it "returns the text differ" do
233
+ expect(Pact.configuration.body_differ_for_content_type 'blah').to be Pact::TextDiffer
234
+ end
235
+ end
236
+
237
+ context "when the nil content type has a custom differ configured" do
238
+ it "returns the custom differ" do
239
+ Pact.configuration.register_body_differ nil, differ
240
+ expect(Pact.configuration.body_differ_for_content_type(nil)).to be differ
241
+ end
242
+ end
243
+
244
+ context "when a custom differ is registered for a content type that has a default differ" do
245
+ it "returns the custom differ" do
246
+ Pact.configuration.register_body_differ /application\/json/, differ
247
+ expect(Pact.configuration.body_differ_for_content_type 'application/json').to be differ
248
+ end
249
+ end
250
+ end
251
+
130
252
  describe "pactfile_write_mode" do
131
253
  context "when @pactfile_write_mode is :overwrite" do
132
254
  it 'returns :overwrite' do
@@ -4,9 +4,10 @@ require 'pact/consumer/mock_service/interaction_mismatch'
4
4
  module Pact
5
5
  module Consumer
6
6
  describe InteractionMismatch do
7
+ let(:content_type) { 'some/content' }
7
8
  let(:actual_request) { instance_double('Pact::Consumer::Request::Actual', :method_and_path => 'GET /path') }
8
- let(:expected_request_1) { instance_double('Pact::Request::Expected') }
9
- let(:expected_request_2) { instance_double('Pact::Request::Expected') }
9
+ let(:expected_request_1) { instance_double('Pact::Request::Expected', :content_type => content_type) }
10
+ let(:expected_request_2) { instance_double('Pact::Request::Expected', :content_type => content_type) }
10
11
  let(:candidate_1) { instance_double('Pact::Interaction', request: expected_request_1, description_with_provider_state_quoted: "desc 1") }
11
12
  let(:candidate_2) { instance_double('Pact::Interaction', request: expected_request_2, description_with_provider_state_quoted: "desc 2") }
12
13
  let(:candidate_interactions) { [candidate_1, candidate_2] }
@@ -48,13 +49,15 @@ module Pact
48
49
  describe "to_s" do
49
50
  let(:expected_message) { "Diff with interaction: desc 1\ndiff 1\nDiff with interaction: desc 2\ndiff 2" }
50
51
 
52
+ let(:diff_formatter) { double("diff_formatter")}
51
53
  before do
52
- allow(Pact.configuration.diff_formatter).to receive(:call).and_return("diff 1", "diff 2")
54
+ allow(Pact.configuration).to receive(:diff_formatter_for_content_type).with(content_type).and_return(diff_formatter)
55
+ allow(diff_formatter).to receive(:call).and_return("diff 1", "diff 2")
53
56
  end
54
57
 
55
58
  it "creates diff output using the configured diff_formatter" do
56
- expect(Pact.configuration.diff_formatter).to receive(:call).with(diff_1, colour: false)
57
- expect(Pact.configuration.diff_formatter).to receive(:call).with(diff_2, colour: false)
59
+ expect(diff_formatter).to receive(:call).with(diff_1, colour: false)
60
+ expect(diff_formatter).to receive(:call).with(diff_2, colour: false)
58
61
  subject.to_s
59
62
  end
60
63
 
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+ require 'pact/consumer_contract/headers'
3
+
4
+ module Pact
5
+ describe Headers do
6
+
7
+ describe "initialize" do
8
+
9
+ context "with duplicate headers" do
10
+
11
+ subject { Headers.new('Content-Type' => 'application/hippo', 'CONTENT-TYPE' => 'application/giraffe') }
12
+
13
+ it "raises an error" do
14
+ expect { subject }.to raise_error DuplicateHeaderError, /Content\-Type.*CONTENT\-TYPE/
15
+ end
16
+
17
+ end
18
+
19
+ context "with a symbol as a header name" do
20
+
21
+ subject { Headers.new(:'content-type' => 'application/hippo') }
22
+
23
+ it "converts the header name to a String" do
24
+ expect( subject.to_hash ).to eq 'content-type' => 'application/hippo'
25
+ end
26
+
27
+ end
28
+
29
+ context "with a nil header name" do
30
+
31
+ subject { Headers.new(nil => 'application/hippo') }
32
+
33
+ it "raises an error" do
34
+ expect{ subject }.to raise_error InvalidHeaderNameTypeError
35
+ end
36
+
37
+ end
38
+
39
+ context "with a boolean header name" do
40
+
41
+ subject { Headers.new(false => 'application/hippo') }
42
+
43
+ it "raises an error" do
44
+ expect{ subject }.to raise_error InvalidHeaderNameTypeError
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ describe "[]" do
52
+
53
+ subject { Headers.new 'Content-Type' => 'application/hippo' }
54
+
55
+ it "is case insensitive as HTTP headers are case insensitive" do
56
+ expect(subject['Content-Type']).to eq('application/hippo')
57
+ expect(subject['CONTENT-TYPE']).to eq('application/hippo')
58
+ expect(subject['content-type']).to eq('application/hippo')
59
+ end
60
+
61
+ end
62
+
63
+ describe "fetch" do
64
+
65
+ subject { Headers.new 'Content-Type' => 'application/hippo' }
66
+
67
+ it "is case insensitive as HTTP headers are case insensitive" do
68
+ expect(subject.fetch('Content-Type')).to eq('application/hippo')
69
+ expect(subject.fetch('CONTENT-TYPE')).to eq('application/hippo')
70
+ expect(subject.fetch('content-type')).to eq('application/hippo')
71
+ expect(subject.fetch('Content-Length','1')).to eq('1')
72
+ expect { subject.fetch('Content-Length')}.to raise_error KeyError
73
+ end
74
+
75
+ end
76
+
77
+ describe "key?" do
78
+
79
+ subject { Headers.new 'Content-Type' => 'application/hippo' }
80
+
81
+ it "is case insensitive as HTTP headers are case insensitive" do
82
+ expect(subject.key?('CONTENT-TYPE')).to be true
83
+ expect(subject.key?('CONTENT-LENGTH')).to be false
84
+ end
85
+ end
86
+
87
+ describe "has_key?" do
88
+
89
+ subject { Headers.new 'Content-Type' => 'application/hippo' }
90
+
91
+ it "is case insensitive as HTTP headers are case insensitive" do
92
+ expect(subject.has_key?('CONTENT-TYPE')).to be true
93
+ expect(subject.has_key?('CONTENT-LENGTH')).to be false
94
+ end
95
+ end
96
+
97
+ describe "[]=" do
98
+
99
+ subject { Headers.new }
100
+
101
+ it "does not allow modification" do
102
+ expect{ subject['Content-Type'] = 'application/hippo' }.to raise_error /frozen/
103
+ end
104
+
105
+ end
106
+ end
107
+ end