pact 0.1.35 → 0.1.37
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.
- data/Gemfile.lock +1 -1
- data/README.md +8 -0
- data/lib/pact/consumer/dsl.rb +1 -1
- data/lib/pact/consumer/interaction.rb +2 -1
- data/lib/pact/consumer/mock_service.rb +3 -1
- data/lib/pact/consumer_contract.rb +4 -3
- data/lib/pact/json_warning.rb +6 -2
- data/lib/pact/matchers/matchers.rb +18 -5
- data/lib/pact/request.rb +21 -0
- data/lib/pact/version.rb +1 -1
- data/spec/lib/pact/consumer/interaction_spec.rb +4 -4
- data/spec/lib/pact/consumer/mock_service_spec.rb +3 -3
- data/spec/lib/pact/consumer_contract_spec.rb +5 -4
- data/spec/lib/pact/matchers/matchers_spec.rb +13 -3
- data/spec/lib/pact/request_spec.rb +22 -0
- metadata +2 -2
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -11,6 +11,14 @@ This allows you to test both sides of an integration point using fast unit tests
|
|
11
11
|
|
12
12
|
This gem is inspired by the concept of "Consumer driven contracts". See http://martinfowler.com/articles/consumerDrivenContracts.html for more information.
|
13
13
|
|
14
|
+
## Features
|
15
|
+
* A services is mocked using an actual process running on a specified port, so javascript clients can be tested as easily as backend clients.
|
16
|
+
* "Provider states" (similar to fixtures) allow the same request to be made with a different expected response.
|
17
|
+
* Consumers specify only the fields they are interested in, allowing a provider to return more fields without breaking the pact. This allows a provider to have a different pact with a different consumer, and know which fields each cares about in a given response.
|
18
|
+
* Expected interactions are verified to have actually occurred.
|
19
|
+
* A rake verification task allows a pact at any URI to be checked against a given service provider codebase.
|
20
|
+
* Different versions of a consumer/provider pair can be easily tested against each other, allowing confidence when deploying new versions of each.
|
21
|
+
|
14
22
|
## Installation
|
15
23
|
|
16
24
|
Put it in your Gemfile. You know how.
|
data/lib/pact/consumer/dsl.rb
CHANGED
@@ -38,7 +38,8 @@ module Pact
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def as_json_for_mock_service
|
41
|
-
{:response => Reification.from_term(response), :request => @request.as_json_with_options, :description => description }
|
41
|
+
{:response => Reification.from_term(response), :request => @request.as_json_with_options, :description => description }.
|
42
|
+
tap{ | hash | hash[:producer_state] = @producer_state if @producer_state }
|
42
43
|
end
|
43
44
|
|
44
45
|
def to_json_for_mock_service
|
@@ -43,6 +43,7 @@ module Pact
|
|
43
43
|
@matched_interactions << interaction
|
44
44
|
end
|
45
45
|
|
46
|
+
# Request::Actual
|
46
47
|
def register_unexpected request
|
47
48
|
@unexpected_requests << request
|
48
49
|
end
|
@@ -58,7 +59,7 @@ module Pact
|
|
58
59
|
def interaction_diffs
|
59
60
|
{
|
60
61
|
:missing_interactions => missing_interactions,
|
61
|
-
:unexpected_requests => unexpected_requests
|
62
|
+
:unexpected_requests => unexpected_requests.collect(&:as_json)
|
62
63
|
}.inject({}) do | hash, pair |
|
63
64
|
hash[pair.first] = pair.last if pair.last.any?
|
64
65
|
hash
|
@@ -238,6 +239,7 @@ module Pact
|
|
238
239
|
end
|
239
240
|
|
240
241
|
def handle_unrecognised_request request, candidates
|
242
|
+
InteractionList.instance.register_unexpected request
|
241
243
|
@logger.error "No interaction found on #{@name} amongst expected requests \"#{candidates.map(&:description).join(', ')}\""
|
242
244
|
@logger.error 'Interaction diffs for that route:'
|
243
245
|
interaction_diff = candidates.map do |candidate|
|
@@ -72,9 +72,10 @@ module Pact
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
|
+
# Move this to interaction
|
75
76
|
def match_criteria? interaction, criteria
|
76
77
|
criteria.each do | key, value |
|
77
|
-
unless match_criterion interaction
|
78
|
+
unless match_criterion interaction.send(key.to_s), value
|
78
79
|
return false
|
79
80
|
end
|
80
81
|
end
|
@@ -100,12 +101,12 @@ module Pact
|
|
100
101
|
File.open(pactfile_path, 'w') do |f|
|
101
102
|
f.write JSON.pretty_generate(self)
|
102
103
|
end
|
103
|
-
end
|
104
|
+
end
|
104
105
|
|
105
106
|
private
|
106
107
|
|
107
108
|
def filenamify name
|
108
109
|
name.downcase.gsub(/\s/, '_')
|
109
|
-
end
|
110
|
+
end
|
110
111
|
end
|
111
112
|
end
|
data/lib/pact/json_warning.rb
CHANGED
@@ -4,6 +4,7 @@ require 'json/add/regexp'
|
|
4
4
|
module Pact
|
5
5
|
module JsonWarning
|
6
6
|
def check_for_active_support_json
|
7
|
+
@already_warned ||= false
|
7
8
|
# Active support clobbers the as_json methods defined in the json/add directory of the json gem.
|
8
9
|
# These methods are required to serialize and deserialize the Regexp and Symbol classes properly.
|
9
10
|
# You can potentially fix this by making sure the json gem is required AFTER the active_support/json gem
|
@@ -15,8 +16,11 @@ module Pact
|
|
15
16
|
# without breaking the calling code, which may depend on activesupport/json... then please fix this.
|
16
17
|
# Note: we can probably do this in Ruby 2.0 with refinements, but for now, we're all stuck on 1.9 :(
|
17
18
|
|
18
|
-
unless
|
19
|
-
|
19
|
+
unless @already_warned
|
20
|
+
unless Regexp.new('').as_json.is_a?(Hash)
|
21
|
+
Logger.new($stderr).warn("It appears you are using ActiveSupport json in your project. You are now in rubygems hell. Please see Pact::JsonWarning for more info.")
|
22
|
+
@already_warned = true
|
23
|
+
end
|
20
24
|
end
|
21
25
|
end
|
22
26
|
end
|
@@ -105,15 +105,28 @@ module Pact
|
|
105
105
|
end
|
106
106
|
|
107
107
|
def class_diff expected, actual
|
108
|
-
if expected
|
109
|
-
actual_display = actual.nil? ? nil : {:class => actual.class, :value => actual }
|
110
|
-
expected_display = expected.nil? ? nil : {:class => expected.class, eg: expected}
|
111
|
-
{:expected => expected_display, :actual => actual_display}
|
112
|
-
else
|
108
|
+
if classes_match? expected, actual
|
113
109
|
{}
|
110
|
+
else
|
111
|
+
{:expected => structure_diff_expected_display(expected), :actual => structure_diff_actual_display(actual)}
|
114
112
|
end
|
115
113
|
end
|
116
114
|
|
115
|
+
def structure_diff_actual_display actual
|
116
|
+
(actual.nil? || actual.is_a?(KeyNotFound) ) ? actual : {:class => actual.class, :value => actual }
|
117
|
+
end
|
118
|
+
|
119
|
+
def structure_diff_expected_display expected
|
120
|
+
(expected.nil?) ? expected : {:class => expected.class, eg: expected}
|
121
|
+
end
|
122
|
+
|
123
|
+
def classes_match? expected, actual
|
124
|
+
#There must be a more elegant way to do this
|
125
|
+
expected.class == actual.class ||
|
126
|
+
(expected.is_a?(TrueClass) && actual.is_a?(FalseClass)) ||
|
127
|
+
(expected.is_a?(FalseClass) && actual.is_a?(TrueClass))
|
128
|
+
end
|
129
|
+
|
117
130
|
def object_diff expected, actual, options
|
118
131
|
return class_diff(expected, actual) if options[:structure]
|
119
132
|
if expected != actual
|
data/lib/pact/request.rb
CHANGED
@@ -85,6 +85,23 @@ module Pact
|
|
85
85
|
as_json.reject{ |key, value| !keep_keys.include? key }
|
86
86
|
end
|
87
87
|
|
88
|
+
def short_description
|
89
|
+
"#{method} #{full_path}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def full_path
|
93
|
+
fp = ''
|
94
|
+
if path.empty?
|
95
|
+
fp << "/"
|
96
|
+
else
|
97
|
+
fp << path
|
98
|
+
end
|
99
|
+
if query && !query.empty?
|
100
|
+
fp << ("?" + query)
|
101
|
+
end
|
102
|
+
fp
|
103
|
+
end
|
104
|
+
|
88
105
|
end
|
89
106
|
|
90
107
|
class Expected < Base
|
@@ -130,6 +147,10 @@ module Pact
|
|
130
147
|
as_json.merge( options.empty? ? {} : { options: options} )
|
131
148
|
end
|
132
149
|
|
150
|
+
def generated_body
|
151
|
+
Pact::Reification.from_term(body)
|
152
|
+
end
|
153
|
+
|
133
154
|
# Don't want to put the default options in the pact json just yet, so calculating these at run time, rather than assigning
|
134
155
|
# the result to @options
|
135
156
|
def runtime_options
|
data/lib/pact/version.rb
CHANGED
@@ -46,7 +46,7 @@ module Pact
|
|
46
46
|
let(:request) { double(Pact::Request::Expected, :as_json_with_options => {:opts => 'blah'})}
|
47
47
|
let(:response) { double('response') }
|
48
48
|
let(:generated_response ) { double('generated_response', :to_json => 'generated_response') }
|
49
|
-
subject { Interaction.new(:description => 'description', :request => request, :response => response, :producer_state => '
|
49
|
+
subject { Interaction.new(:description => 'description', :request => request, :response => response, :producer_state => 'some state')}
|
50
50
|
let(:expected_hash) { {:response => generated_response, :request => as_json_with_options, :description => '' } }
|
51
51
|
|
52
52
|
before do
|
@@ -62,8 +62,8 @@ module Pact
|
|
62
62
|
expect(subject.as_json_for_mock_service[:request]).to eq as_json_with_options
|
63
63
|
end
|
64
64
|
|
65
|
-
it "
|
66
|
-
expect(subject.as_json_for_mock_service
|
65
|
+
it "includes the producer state" do
|
66
|
+
expect(subject.as_json_for_mock_service[:producer_state]).to eq 'some state'
|
67
67
|
end
|
68
68
|
|
69
69
|
it "includes the description" do
|
@@ -71,7 +71,7 @@ module Pact
|
|
71
71
|
end
|
72
72
|
|
73
73
|
it "doesn't have any other keys" do
|
74
|
-
expect(subject.as_json_for_mock_service.keys).to eq [:response, :request, :description]
|
74
|
+
expect(subject.as_json_for_mock_service.keys).to eq [:response, :request, :description, :producer_state]
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
@@ -10,7 +10,7 @@ module Pact::Consumer
|
|
10
10
|
|
11
11
|
shared_context "unexpected requests and missed interactions" do
|
12
12
|
let(:expected_call) { {request: 'blah'} }
|
13
|
-
let(:unexpected_call) {
|
13
|
+
let(:unexpected_call) { Pact::Request::Actual.from_hash(path: '/path', method: 'get') }
|
14
14
|
subject {
|
15
15
|
interactionList = InteractionList.instance
|
16
16
|
interactionList.add expected_call
|
@@ -21,7 +21,7 @@ module Pact::Consumer
|
|
21
21
|
|
22
22
|
shared_context "no unexpected requests or missed interactions exist" do
|
23
23
|
let(:expected_call) { {request: 'blah'} }
|
24
|
-
let(:unexpected_call) {
|
24
|
+
let(:unexpected_call) { Pact::Request::Actual.from_hash(path: '/path', method: 'get') }
|
25
25
|
subject {
|
26
26
|
interactionList = InteractionList.instance
|
27
27
|
interactionList.add expected_call
|
@@ -34,7 +34,7 @@ module Pact::Consumer
|
|
34
34
|
context "when unexpected requests and missed interactions exist" do
|
35
35
|
include_context "unexpected requests and missed interactions"
|
36
36
|
let(:expected) {
|
37
|
-
{:missing_interactions=>[{:request=>"blah"}], :unexpected_requests=>[{:
|
37
|
+
{:missing_interactions=>[{:request=>"blah"}], :unexpected_requests=>[{:method=>"get", :path=>"/path"}]}
|
38
38
|
}
|
39
39
|
it "returns the unexpected requests and missed interactions" do
|
40
40
|
expect(subject.interaction_diffs).to eq expected
|
@@ -68,8 +68,8 @@ module Pact
|
|
68
68
|
describe "find_interactions" do
|
69
69
|
let(:consumer) { double('ServiceConsumer', :name => 'Consumer')}
|
70
70
|
let(:producer) { double('ServiceProducer', :name => 'Producer')}
|
71
|
-
let(:interaction1) {
|
72
|
-
let(:interaction2) {
|
71
|
+
let(:interaction1) { Pact::Consumer::Interaction.new(:description => 'a request for food') }
|
72
|
+
let(:interaction2) { Pact::Consumer::Interaction.new(:description => 'a request for drink') }
|
73
73
|
subject { ConsumerContract.new(:interactions => [interaction1, interaction2], :consumer => consumer, :producer => producer) }
|
74
74
|
context "by description" do
|
75
75
|
context "when no interactions are found" do
|
@@ -87,8 +87,9 @@ module Pact
|
|
87
87
|
describe "find_interaction" do
|
88
88
|
let(:consumer) { double('ServiceConsumer', :name => 'Consumer')}
|
89
89
|
let(:producer) { double('ServiceProducer', :name => 'Producer')}
|
90
|
-
|
91
|
-
let(:
|
90
|
+
# Should be stubbing these
|
91
|
+
let(:interaction1) { Pact::Consumer::Interaction.new(:description => 'a request for food') }
|
92
|
+
let(:interaction2) { Pact::Consumer::Interaction.new(:description => 'a request for drink') }
|
92
93
|
subject { ConsumerContract.new(:interactions => [interaction1, interaction2], :consumer => consumer, :producer => producer) }
|
93
94
|
context "by description" do
|
94
95
|
context "when a match is found" do
|
@@ -27,13 +27,23 @@ describe Pact::Matchers do
|
|
27
27
|
|
28
28
|
describe 'structure_diff' do
|
29
29
|
let(:expected) {
|
30
|
-
{a: 'a string', b: 1, c: nil, d: [{e: 'thing'}], f: {g: 10}}
|
30
|
+
{a: 'a string', b: 1, c: nil, d: [{e: 'thing'}], f: {g: 10}, h: false}
|
31
31
|
}
|
32
32
|
|
33
33
|
context "when the classes match" do
|
34
|
-
let(:actual) { {a: 'another string', b: 2, c: nil, d: [{e: 'something'}], f: {g: 100}} }
|
34
|
+
let(:actual) { {a: 'another string', b: 2, c: nil, d: [{e: 'something'}], f: {g: 100}, h: true} }
|
35
|
+
let(:difference) { {} }
|
35
36
|
it "returns an empty hash" do
|
36
|
-
expect(structure_diff(expected, actual)).to
|
37
|
+
expect(structure_diff(expected, actual)).to eq difference
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "when a key is not found" do
|
42
|
+
let(:actual) { {a: 'blah'} }
|
43
|
+
let(:expected) { {b: 'blah'} }
|
44
|
+
let(:difference) { {:b=>{:expected=>{:class=>String, :eg=>"blah"}, :actual=>Pact::Matchers::KeyNotFound.new}} }
|
45
|
+
it "returns the difference" do
|
46
|
+
expect(structure_diff(expected, actual)).to eq difference
|
37
47
|
end
|
38
48
|
end
|
39
49
|
|
@@ -2,6 +2,28 @@ require 'spec_helper'
|
|
2
2
|
require 'pact/request'
|
3
3
|
|
4
4
|
shared_examples "a request" do
|
5
|
+
|
6
|
+
describe 'full_path' do
|
7
|
+
context "with empty path" do
|
8
|
+
subject { described_class.from_hash({:path => '', :method => 'get'}) }
|
9
|
+
it "returns the full path"do
|
10
|
+
expect(subject.full_path).to eq "/"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
context "with a path" do
|
14
|
+
subject { described_class.from_hash({:path => '/path', :method => 'get'}) }
|
15
|
+
it "returns the full path"do
|
16
|
+
expect(subject.full_path).to eq "/path"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
context "with a path and query" do
|
20
|
+
subject { described_class.from_hash({:path => '/path', :method => 'get', :query => "something"}) }
|
21
|
+
it "returns the full path"do
|
22
|
+
expect(subject.full_path).to eq "/path?something"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
5
27
|
describe "building from a hash" do
|
6
28
|
|
7
29
|
let(:raw_request) do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pact
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.37
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ authors:
|
|
13
13
|
autorequire:
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
|
-
date: 2013-
|
16
|
+
date: 2013-09-11 00:00:00.000000000 Z
|
17
17
|
dependencies:
|
18
18
|
- !ruby/object:Gem::Dependency
|
19
19
|
name: randexp
|