pact 0.1.28
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/.gitignore +28 -0
- data/.rspec +2 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +83 -0
- data/LICENSE.txt +22 -0
- data/README.md +238 -0
- data/Rakefile +33 -0
- data/bin/pact +4 -0
- data/lib/pact/app.rb +32 -0
- data/lib/pact/configuration.rb +54 -0
- data/lib/pact/consumer/app_manager.rb +177 -0
- data/lib/pact/consumer/configuration_dsl.rb +71 -0
- data/lib/pact/consumer/consumer_contract_builder.rb +79 -0
- data/lib/pact/consumer/consumer_contract_builders.rb +10 -0
- data/lib/pact/consumer/dsl.rb +98 -0
- data/lib/pact/consumer/interaction.rb +70 -0
- data/lib/pact/consumer/mock_service.rb +340 -0
- data/lib/pact/consumer/rspec.rb +43 -0
- data/lib/pact/consumer/run_condor.rb +4 -0
- data/lib/pact/consumer/run_mock_contract_service.rb +13 -0
- data/lib/pact/consumer/service_consumer.rb +22 -0
- data/lib/pact/consumer/service_producer.rb +23 -0
- data/lib/pact/consumer.rb +7 -0
- data/lib/pact/consumer_contract.rb +110 -0
- data/lib/pact/json_warning.rb +23 -0
- data/lib/pact/logging.rb +14 -0
- data/lib/pact/matchers/matchers.rb +85 -0
- data/lib/pact/matchers.rb +1 -0
- data/lib/pact/producer/configuration_dsl.rb +62 -0
- data/lib/pact/producer/matchers.rb +22 -0
- data/lib/pact/producer/pact_spec_runner.rb +57 -0
- data/lib/pact/producer/producer_state.rb +81 -0
- data/lib/pact/producer/rspec.rb +127 -0
- data/lib/pact/producer/test_methods.rb +89 -0
- data/lib/pact/producer.rb +1 -0
- data/lib/pact/rake_task.rb +64 -0
- data/lib/pact/reification.rb +26 -0
- data/lib/pact/request.rb +109 -0
- data/lib/pact/term.rb +40 -0
- data/lib/pact/verification_task.rb +57 -0
- data/lib/pact/version.rb +3 -0
- data/lib/pact.rb +5 -0
- data/lib/tasks/pact.rake +6 -0
- data/pact.gemspec +36 -0
- data/scratchpad.txt +36 -0
- data/spec/features/consumption_spec.rb +146 -0
- data/spec/features/producer_states/zebras.rb +28 -0
- data/spec/features/production_spec.rb +160 -0
- data/spec/integration/pact/configuration_spec.rb +65 -0
- data/spec/lib/pact/configuration_spec.rb +35 -0
- data/spec/lib/pact/consumer/app_manager_spec.rb +41 -0
- data/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +87 -0
- data/spec/lib/pact/consumer/dsl_spec.rb +52 -0
- data/spec/lib/pact/consumer/interaction_spec.rb +108 -0
- data/spec/lib/pact/consumer/mock_service_spec.rb +147 -0
- data/spec/lib/pact/consumer/service_consumer_spec.rb +11 -0
- data/spec/lib/pact/consumer_contract_spec.rb +125 -0
- data/spec/lib/pact/matchers/matchers_spec.rb +354 -0
- data/spec/lib/pact/producer/configuration_dsl_spec.rb +101 -0
- data/spec/lib/pact/producer/producer_state_spec.rb +103 -0
- data/spec/lib/pact/producer/rspec_spec.rb +48 -0
- data/spec/lib/pact/reification_spec.rb +43 -0
- data/spec/lib/pact/request_spec.rb +316 -0
- data/spec/lib/pact/term_spec.rb +36 -0
- data/spec/lib/pact/verification_task_spec.rb +64 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/support/a_consumer-a_producer.json +34 -0
- data/spec/support/pact_rake_support.rb +41 -0
- data/spec/support/test_app_fail.json +22 -0
- data/spec/support/test_app_pass.json +21 -0
- data/tasks/pact-test.rake +19 -0
- metadata +381 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module Consumer
|
5
|
+
describe InteractionBuilder do
|
6
|
+
|
7
|
+
subject {
|
8
|
+
interaction_builder = InteractionBuilder.new('Test request', nil).with(request)
|
9
|
+
interaction_builder.on_interaction_fully_defined do | interaction |
|
10
|
+
producer.callback interaction
|
11
|
+
end
|
12
|
+
interaction_builder
|
13
|
+
}
|
14
|
+
|
15
|
+
let(:pact_path) { File.expand_path('../../../../pacts/mock', __FILE__) }
|
16
|
+
|
17
|
+
let(:request) do
|
18
|
+
{
|
19
|
+
method: 'post',
|
20
|
+
path: '/foo',
|
21
|
+
body: Term.new(generate: 'waffle', matcher: /ffl/),
|
22
|
+
headers: { 'Content-Type' => 'application/json' },
|
23
|
+
query: '',
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
let(:response) do
|
28
|
+
{ baz: /qux/, wiffle: Term.new(generate: 'wiffle', matcher: /iff/) }
|
29
|
+
end
|
30
|
+
|
31
|
+
let(:producer) do
|
32
|
+
double(callback: nil)
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "setting up responses" do
|
36
|
+
|
37
|
+
it "invokes the callback" do
|
38
|
+
producer.should_receive(:callback).with(subject.interaction)
|
39
|
+
subject.will_respond_with response
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "to JSON" do
|
45
|
+
|
46
|
+
let(:parsed_result) do
|
47
|
+
JSON.load(JSON.dump(subject.interaction))
|
48
|
+
end
|
49
|
+
|
50
|
+
before do
|
51
|
+
subject.will_respond_with response
|
52
|
+
end
|
53
|
+
|
54
|
+
it "contains the request" do
|
55
|
+
expect(parsed_result['request']).to eq({
|
56
|
+
'method' => 'post',
|
57
|
+
'path' => '/foo',
|
58
|
+
'headers' => {
|
59
|
+
'Content-Type' => 'application/json'
|
60
|
+
},
|
61
|
+
'body' => Term.new(generate: 'waffle', matcher: /ffl/),
|
62
|
+
'query' => ''
|
63
|
+
})
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "response" do
|
67
|
+
|
68
|
+
it "serialises regexes" do
|
69
|
+
expect(parsed_result['response']['baz']).to eql /qux/
|
70
|
+
end
|
71
|
+
|
72
|
+
it "serialises terms" do
|
73
|
+
term = Term.new(generate:'wiffle', matcher: /iff/)
|
74
|
+
parsed_term = parsed_result['response']['wiffle']
|
75
|
+
expect(term.matcher).to eql parsed_term.matcher
|
76
|
+
expect(term.generate).to eql parsed_term.generate
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
context "with a producer_state" do
|
82
|
+
context "described with a string" do
|
83
|
+
subject {
|
84
|
+
interaction_builder = InteractionBuilder.new('Test request', "there are no alligators").with(request)
|
85
|
+
interaction_builder.on_interaction_fully_defined {}
|
86
|
+
interaction_builder
|
87
|
+
}
|
88
|
+
|
89
|
+
it "includes the state name as a string" do
|
90
|
+
expect(parsed_result['producer_state']).to eql("there are no alligators")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
context "described with a symbol" do
|
94
|
+
subject {
|
95
|
+
interaction_builder = InteractionBuilder.new('Test request', :there_are_no_alligators).with(request)
|
96
|
+
interaction_builder.on_interaction_fully_defined {}
|
97
|
+
interaction_builder
|
98
|
+
}
|
99
|
+
|
100
|
+
it "includes the state name as a symbol" do
|
101
|
+
expect(parsed_result['producer_state']).to eql("there_are_no_alligators")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'pact/consumer/mock_service'
|
3
|
+
|
4
|
+
module Pact::Consumer
|
5
|
+
|
6
|
+
describe InteractionList do
|
7
|
+
before do
|
8
|
+
InteractionList.instance.clear
|
9
|
+
end
|
10
|
+
|
11
|
+
shared_context "unexpected requests and missed interactions" do
|
12
|
+
let(:expected_call) { {request: 'blah'} }
|
13
|
+
let(:unexpected_call) { {request: 'meh'} }
|
14
|
+
subject {
|
15
|
+
interactionList = InteractionList.instance
|
16
|
+
interactionList.add expected_call
|
17
|
+
interactionList.register_unexpected unexpected_call
|
18
|
+
interactionList
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
shared_context "no unexpected requests or missed interactions exist" do
|
23
|
+
let(:expected_call) { {request: 'blah'} }
|
24
|
+
let(:unexpected_call) { {request: 'meh'} }
|
25
|
+
subject {
|
26
|
+
interactionList = InteractionList.instance
|
27
|
+
interactionList.add expected_call
|
28
|
+
interactionList.register_matched expected_call
|
29
|
+
interactionList
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "interaction_diffs" do
|
34
|
+
context "when unexpected requests and missed interactions exist" do
|
35
|
+
include_context "unexpected requests and missed interactions"
|
36
|
+
let(:expected) {
|
37
|
+
{:missing_interactions=>[{:request=>"blah"}], :unexpected_requests=>[{:request=>"meh"}]}
|
38
|
+
}
|
39
|
+
it "returns the unexpected requests and missed interactions" do
|
40
|
+
expect(subject.interaction_diffs).to eq expected
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when no unexpected requests or missed interactions exist" do
|
45
|
+
include_context "no unexpected requests or missed interactions exist"
|
46
|
+
let(:expected) {
|
47
|
+
{}
|
48
|
+
}
|
49
|
+
it "returns an empty hash" do
|
50
|
+
expect(subject.interaction_diffs).to eq expected
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "all_matched?" do
|
56
|
+
context "when unexpected requests or missed interactions exist" do
|
57
|
+
include_context "unexpected requests and missed interactions"
|
58
|
+
it "returns false" do
|
59
|
+
expect(subject.all_matched?).to be_false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
context "when unexpected requests or missed interactions do not exist" do
|
63
|
+
include_context "no unexpected requests or missed interactions exist"
|
64
|
+
it "returns false" do
|
65
|
+
expect(subject.all_matched?).to be_true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe RequestExtractor do
|
72
|
+
class TestSubject
|
73
|
+
include RequestExtractor
|
74
|
+
end
|
75
|
+
|
76
|
+
let(:rack_env) {
|
77
|
+
{
|
78
|
+
"CONTENT_LENGTH" => "16",
|
79
|
+
"CONTENT_TYPE" => content_type,
|
80
|
+
"GATEWAY_INTERFACE" => "CGI/1.1",
|
81
|
+
"PATH_INFO" => "/donuts",
|
82
|
+
"QUERY_STRING" => "",
|
83
|
+
"REMOTE_ADDR" => "127.0.0.1",
|
84
|
+
"REMOTE_HOST" => "localhost",
|
85
|
+
"REQUEST_METHOD" => "POST",
|
86
|
+
"REQUEST_URI" => "http://localhost:4321/donuts",
|
87
|
+
"SCRIPT_NAME" => "",
|
88
|
+
"SERVER_NAME" => "localhost",
|
89
|
+
"SERVER_PORT" => "4321",
|
90
|
+
"SERVER_PROTOCOL" => "HTTP/1.1",
|
91
|
+
"SERVER_SOFTWARE" => "WEBrick/1.3.1 (Ruby/1.9.3/2013-02-22)",
|
92
|
+
"HTTP_ACCEPT" => "text/plain",
|
93
|
+
"HTTP_USER_AGENT" => "Ruby",
|
94
|
+
"HTTP_HOST" => "localhost:4321",
|
95
|
+
"rack.version" => [1, 2 ],
|
96
|
+
"rack.input" => StringIO.new(body),
|
97
|
+
"rack.errors" => nil,
|
98
|
+
"rack.multithread" => true,
|
99
|
+
"rack.multiprocess" => false,
|
100
|
+
"rack.run_once" => false,
|
101
|
+
"rack.url_scheme" => "http",
|
102
|
+
"HTTP_VERSION" => "HTTP/1.1",
|
103
|
+
"REQUEST_PATH" => "/donuts"
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
subject { TestSubject.new }
|
108
|
+
|
109
|
+
let(:expected_request) {
|
110
|
+
{
|
111
|
+
"query" => "",
|
112
|
+
"method" => "post",
|
113
|
+
"body" => expected_body,
|
114
|
+
"path" => "/donuts",
|
115
|
+
"headers" => {
|
116
|
+
"Content-Type" => content_type,
|
117
|
+
"Accept" => "text/plain",
|
118
|
+
"User-Agent" => "Ruby",
|
119
|
+
"Host" => "localhost:4321",
|
120
|
+
"Version" => "HTTP/1.1"
|
121
|
+
}
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
context "with a text body" do
|
126
|
+
let(:content_type) { "application/x-www-form-urlencoded" }
|
127
|
+
let(:body) { 'this is the body' }
|
128
|
+
let(:expected_body) { body }
|
129
|
+
|
130
|
+
it "extracts the body" do
|
131
|
+
expect(subject.request_from(rack_env)).to eq expected_request
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context "with a json body" do
|
136
|
+
let(:content_type) { "application/json" }
|
137
|
+
let(:body) { '{"a" : "body" }' }
|
138
|
+
let(:expected_body) { {"a" => "body"} }
|
139
|
+
|
140
|
+
it "extracts the body" do
|
141
|
+
expect(subject.request_from(rack_env)).to eq expected_request
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'pact/consumer_contract'
|
3
|
+
|
4
|
+
module Pact
|
5
|
+
describe ConsumerContract do
|
6
|
+
describe "as_json" do
|
7
|
+
|
8
|
+
class MockInteraction
|
9
|
+
def as_json(options ={})
|
10
|
+
{:mock => "interaction"}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
before do
|
15
|
+
@backup_version = Pact::VERSION
|
16
|
+
Pact::VERSION = "1.0"
|
17
|
+
DateTime.stub(:now).and_return(DateTime.strptime("2013-08-15T13:27:13+10:00"))
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:service_consumer) { double('ServiceConsumer', :as_json => {:a => 'consumer'}) }
|
21
|
+
let(:service_producer) { double('ServiceProducer', :as_json => {:a => 'producer'}) }
|
22
|
+
let(:pact) { ConsumerContract.new({:interactions => [MockInteraction.new], :consumer => service_consumer, :producer => service_producer }) }
|
23
|
+
let(:expected_as_json) { {:producer=>{:a=>"producer"}, :consumer=>{:a=>"consumer"}, :interactions=>[{:mock=>"interaction"}], :metadata=>{:pact_gem=>{:version=>"1.0"}}} }
|
24
|
+
|
25
|
+
it "should return a hash representation of the Pact" do
|
26
|
+
pact.as_json.should eq expected_as_json
|
27
|
+
end
|
28
|
+
|
29
|
+
after do
|
30
|
+
Pact::VERSION = @backup_version
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe ".from_json" do
|
35
|
+
let(:loaded_pact) { ConsumerContract.from_json(string) }
|
36
|
+
context "when the top level object is a ConsumerContract" do
|
37
|
+
let(:string) { '{"interactions":[{"request": {"path":"/path", "method" : "get"}}], "consumer": {"name" : "Bob"} }' }
|
38
|
+
|
39
|
+
it "should create a Pact" do
|
40
|
+
loaded_pact.should be_instance_of ConsumerContract
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should have interactions" do
|
44
|
+
loaded_pact.interactions.should be_instance_of Array
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should have a consumer" do
|
48
|
+
loaded_pact.consumer.should be_instance_of Pact::Consumer::ServiceConsumer
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should have a producer" do
|
52
|
+
loaded_pact.producer.should be_instance_of Pact::Consumer::ServiceProducer
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "find_interactions" do
|
58
|
+
let(:consumer) { double('ServiceConsumer', :name => 'Consumer')}
|
59
|
+
let(:producer) { double('ServiceProducer', :name => 'Producer')}
|
60
|
+
let(:interaction1) { {'description' => 'a request for food'} }
|
61
|
+
let(:interaction2) { {'description' => 'a request for drink'} }
|
62
|
+
subject { ConsumerContract.new(:interactions => [interaction1, interaction2], :consumer => consumer, :producer => producer) }
|
63
|
+
context "by description" do
|
64
|
+
context "when no interactions are found" do
|
65
|
+
it "returns an empty array" do
|
66
|
+
expect(subject.find_interactions(:description => /blah/)).to eql []
|
67
|
+
end
|
68
|
+
end
|
69
|
+
context "when interactions are found" do
|
70
|
+
it "returns an array of the matching interactions" do
|
71
|
+
expect(subject.find_interactions(:description => /request/)).to eql [interaction1, interaction2]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
describe "find_interaction" do
|
77
|
+
let(:consumer) { double('ServiceConsumer', :name => 'Consumer')}
|
78
|
+
let(:producer) { double('ServiceProducer', :name => 'Producer')}
|
79
|
+
let(:interaction1) { {'description' => 'a request for food'} }
|
80
|
+
let(:interaction2) { {'description' => 'a request for drink'} }
|
81
|
+
subject { ConsumerContract.new(:interactions => [interaction1, interaction2], :consumer => consumer, :producer => producer) }
|
82
|
+
context "by description" do
|
83
|
+
context "when a match is found" do
|
84
|
+
it "returns the interaction" do
|
85
|
+
expect(subject.find_interaction :description => /request.*food/).to eql interaction1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
context "when more than one match is found" do
|
89
|
+
it "raises an error" do
|
90
|
+
expect{ subject.find_interaction(:description => /request/) }.to raise_error "Found more than 1 interaction matching {:description=>/request/} in pact file between Consumer and Producer."
|
91
|
+
end
|
92
|
+
end
|
93
|
+
context "when a match is not found" do
|
94
|
+
it "raises an error" do
|
95
|
+
expect{ subject.find_interaction(:description => /blah/) }.to raise_error "Could not find interaction matching {:description=>/blah/} in pact file between Consumer and Producer."
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
describe "update_pactfile" do
|
101
|
+
let(:pacts_dir) { Pathname.new("./tmp/pactfiles") }
|
102
|
+
let(:expected_pact_path) { pacts_dir + "test_consumer-test_service.json" }
|
103
|
+
let(:expected_pact_string) { 'the_json' }
|
104
|
+
let(:consumer) { Pact::Consumer::ServiceConsumer.new(:name => 'test_consumer')}
|
105
|
+
let(:producer) { Pact::Consumer::ServiceProducer.new(:name => 'test_service')}
|
106
|
+
let(:interactions) { [double("interaction", as_json: "something")]}
|
107
|
+
subject { ConsumerContract.new(:consumer => consumer, :producer => producer, :interactions => interactions) }
|
108
|
+
before do
|
109
|
+
Pact.configuration.stub(:pact_dir).and_return(Pathname.new("./tmp/pactfiles"))
|
110
|
+
FileUtils.rm_rf pacts_dir
|
111
|
+
FileUtils.mkdir_p pacts_dir
|
112
|
+
JSON.should_receive(:pretty_generate).with(instance_of(Pact::ConsumerContract)).and_return(expected_pact_string)
|
113
|
+
subject.update_pactfile
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should write to a file specified by the consumer and producer name" do
|
117
|
+
File.exist?(expected_pact_path).should be_true
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should write the interactions to the file" do
|
121
|
+
File.read(expected_pact_path).should eql expected_pact_string
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|