pacto 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.
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +129 -0
- data/Rakefile +7 -0
- data/TODO.md +19 -0
- data/lib/pacto.rb +48 -0
- data/lib/pacto/contract.rb +22 -0
- data/lib/pacto/extensions.rb +18 -0
- data/lib/pacto/file_pre_processor.rb +12 -0
- data/lib/pacto/instantiated_contract.rb +62 -0
- data/lib/pacto/rake_task.rb +75 -0
- data/lib/pacto/request.rb +58 -0
- data/lib/pacto/response.rb +27 -0
- data/lib/pacto/response_adapter.rb +24 -0
- data/lib/pacto/version.rb +3 -0
- data/pacto.gemspec +36 -0
- data/spec/data/contract.json +25 -0
- data/spec/pacto/contract_spec.rb +50 -0
- data/spec/pacto/extensions_spec.rb +34 -0
- data/spec/pacto/file_pre_processor_spec.rb +13 -0
- data/spec/pacto/instantiated_contract_spec.rb +224 -0
- data/spec/pacto/pacto_spec.rb +87 -0
- data/spec/pacto/request_spec.rb +73 -0
- data/spec/pacto/response_adapter_spec.rb +27 -0
- data/spec/pacto/response_spec.rb +114 -0
- data/spec/spec_helper.rb +4 -0
- metadata +295 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
module Pacto
|
2
|
+
class Request
|
3
|
+
def initialize(host, definition)
|
4
|
+
@host = host
|
5
|
+
@definition = definition
|
6
|
+
end
|
7
|
+
|
8
|
+
def host
|
9
|
+
@host
|
10
|
+
end
|
11
|
+
|
12
|
+
def method
|
13
|
+
@definition['method'].to_s.downcase.to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
def path
|
17
|
+
@definition['path']
|
18
|
+
end
|
19
|
+
|
20
|
+
def headers
|
21
|
+
@definition['headers']
|
22
|
+
end
|
23
|
+
|
24
|
+
def params
|
25
|
+
@definition['params']
|
26
|
+
end
|
27
|
+
|
28
|
+
def absolute_uri
|
29
|
+
@host + path
|
30
|
+
end
|
31
|
+
|
32
|
+
def full_uri
|
33
|
+
return absolute_uri if params.empty?
|
34
|
+
|
35
|
+
uri = Addressable::URI.new
|
36
|
+
uri.query_values = params
|
37
|
+
|
38
|
+
absolute_uri + '?' + uri.query
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute
|
42
|
+
response = HTTParty.send(method, @host + path, {
|
43
|
+
httparty_params_key => normalized_params,
|
44
|
+
:headers => headers
|
45
|
+
})
|
46
|
+
ResponseAdapter.new(response)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def httparty_params_key
|
51
|
+
method == :get ? :query : :body
|
52
|
+
end
|
53
|
+
|
54
|
+
def normalized_params
|
55
|
+
method == :get ? params : params.to_json
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pacto
|
2
|
+
class Response
|
3
|
+
def initialize(definition)
|
4
|
+
@definition = definition
|
5
|
+
end
|
6
|
+
|
7
|
+
def instantiate
|
8
|
+
OpenStruct.new({
|
9
|
+
'status' => @definition['status'],
|
10
|
+
'headers' => @definition['headers'],
|
11
|
+
'body' => JSON::Generator.generate(@definition['body'])
|
12
|
+
})
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate(response)
|
16
|
+
@errors = []
|
17
|
+
if @definition['status'] != response.status
|
18
|
+
@errors << "Invalid status: expected #{@definition['status']} but got #{response.status}"
|
19
|
+
end
|
20
|
+
unless @definition['headers'].normalize_keys.subset_of?(response.headers.normalize_keys)
|
21
|
+
@errors << "Invalid headers: expected #{@definition['headers'].inspect} to be a subset of #{response.headers.inspect}"
|
22
|
+
end
|
23
|
+
@errors << JSON::Validator.fully_validate(@definition['body'], response.body)
|
24
|
+
@errors.flatten
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Pacto
|
2
|
+
class ResponseAdapter
|
3
|
+
def initialize(response)
|
4
|
+
@response = response
|
5
|
+
end
|
6
|
+
|
7
|
+
def status
|
8
|
+
@response.code
|
9
|
+
end
|
10
|
+
|
11
|
+
def body
|
12
|
+
@response.body
|
13
|
+
end
|
14
|
+
|
15
|
+
def headers
|
16
|
+
# Normalize headers values according to RFC2616
|
17
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
|
18
|
+
normalized_headers = @response.headers.map do |(key, value)|
|
19
|
+
[key, value.join(',')]
|
20
|
+
end
|
21
|
+
Hash[normalized_headers]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/pacto.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pacto/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "pacto"
|
8
|
+
gem.version = Pacto::VERSION
|
9
|
+
gem.authors = ["ThoughtWorks & Abril"]
|
10
|
+
gem.email = ["abril_vejasp_dev@thoughtworks.com"]
|
11
|
+
gem.description = %q{Pacto is a Ruby implementation of the [Consumer-Driven Contracts](http://martinfowler.com/articles/consumerDrivenContracts.html) pattern for evolving services}
|
12
|
+
gem.summary = %q{Consumer-Driven Contracts implementation}
|
13
|
+
gem.homepage = 'https://github.com/thoughtworks/pacto'
|
14
|
+
gem.license = 'MIT'
|
15
|
+
|
16
|
+
|
17
|
+
gem.files = `git ls-files`.split($/)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.require_paths = ["lib"]
|
21
|
+
|
22
|
+
gem.add_dependency "webmock"
|
23
|
+
gem.add_dependency "json"
|
24
|
+
gem.add_dependency "json-schema", "1.0.4"
|
25
|
+
gem.add_dependency "json-generator"
|
26
|
+
gem.add_dependency "hash-deep-merge"
|
27
|
+
gem.add_dependency "httparty"
|
28
|
+
gem.add_dependency "addressable"
|
29
|
+
gem.add_dependency "coveralls"
|
30
|
+
|
31
|
+
gem.add_development_dependency "rake"
|
32
|
+
gem.add_development_dependency "rspec"
|
33
|
+
gem.add_development_dependency "guard-rspec"
|
34
|
+
gem.add_development_dependency "rb-fsevent" if RUBY_PLATFORM =~ /darwin/i
|
35
|
+
gem.add_development_dependency "terminal-notifier-guard" if RUBY_PLATFORM =~ /darwin/i
|
36
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
{
|
2
|
+
"request": {
|
3
|
+
"method": "GET",
|
4
|
+
"path": "/hello_world",
|
5
|
+
"headers": {
|
6
|
+
"Accept": "application/json"
|
7
|
+
}
|
8
|
+
},
|
9
|
+
|
10
|
+
"response": {
|
11
|
+
"status": 200,
|
12
|
+
"headers": {
|
13
|
+
"Content-Type": "application/json"
|
14
|
+
},
|
15
|
+
"body": {
|
16
|
+
"description": "A simple response",
|
17
|
+
"type": "object",
|
18
|
+
"properties": {
|
19
|
+
"message": {
|
20
|
+
"type": "string"
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Pacto
|
2
|
+
describe Contract do
|
3
|
+
let(:request) { double('request') }
|
4
|
+
let(:response) { double('response') }
|
5
|
+
|
6
|
+
let(:contract) { described_class.new(request, response) }
|
7
|
+
|
8
|
+
describe '#instantiate' do
|
9
|
+
let(:instantiated_response) { double('instantiated response') }
|
10
|
+
let(:instantiated_contract) { double('instantiated contract') }
|
11
|
+
|
12
|
+
context 'by default' do
|
13
|
+
it 'should instantiate a contract with default attributes' do
|
14
|
+
response.should_receive(:instantiate).and_return(instantiated_response)
|
15
|
+
InstantiatedContract.should_receive(:new).
|
16
|
+
with(request, instantiated_response).
|
17
|
+
and_return(instantiated_contract)
|
18
|
+
instantiated_contract.should_not_receive(:replace!)
|
19
|
+
|
20
|
+
contract.instantiate.should == instantiated_contract
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with extra attributes' do
|
25
|
+
let(:attributes) { {:foo => 'bar'} }
|
26
|
+
|
27
|
+
it 'should instantiate a contract and overwrite default attributes' do
|
28
|
+
response.should_receive(:instantiate).and_return(instantiated_response)
|
29
|
+
InstantiatedContract.should_receive(:new).
|
30
|
+
with(request, instantiated_response).
|
31
|
+
and_return(instantiated_contract)
|
32
|
+
instantiated_contract.should_receive(:replace!).with(attributes)
|
33
|
+
|
34
|
+
contract.instantiate(attributes).should == instantiated_contract
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#validate' do
|
40
|
+
let(:fake_response) { double('fake response') }
|
41
|
+
let(:validation_result) { double('validation result') }
|
42
|
+
|
43
|
+
it 'should execute the request and match it against the expected response' do
|
44
|
+
request.should_receive(:execute).and_return(fake_response)
|
45
|
+
response.should_receive(:validate).with(fake_response).and_return(validation_result)
|
46
|
+
contract.validate.should == validation_result
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Pacto
|
2
|
+
module Extensions
|
3
|
+
describe HashSubsetOf do
|
4
|
+
describe '#subset_of?' do
|
5
|
+
context 'when the other hash is the same' do
|
6
|
+
it 'should return true' do
|
7
|
+
{:a => 'a'}.should be_subset_of({:a => 'a'})
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'when the other hash is a subset' do
|
12
|
+
it 'should return true' do
|
13
|
+
{:a => 'a'}.should be_subset_of({:a => 'a', :b => 'b'})
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'when the other hash is not a subset' do
|
18
|
+
it 'should return false' do
|
19
|
+
{:a => 'a'}.subset_of?({:a => 'b'}).should be_false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#normalize_keys' do
|
25
|
+
it 'should turn keys into downcased strings' do
|
26
|
+
{:A => 'a'}.normalize_keys.should == {'a' => 'a'}
|
27
|
+
{:a => 'a'}.normalize_keys.should == {'a' => 'a'}
|
28
|
+
{'A' => 'a'}.normalize_keys.should == {'a' => 'a'}
|
29
|
+
{'a' => 'a'}.normalize_keys.should == {'a' => 'a'}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Pacto
|
2
|
+
describe FilePreProcessor do
|
3
|
+
describe "#process" do
|
4
|
+
it "should return the result of ERB" do
|
5
|
+
subject.process("2 + 2 = <%= 2 + 2 %>").should == "2 + 2 = 4"
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should not mess with pure JSONs" do
|
9
|
+
subject.process('{"property": ["one", "two, null"]}')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
module Pacto
|
2
|
+
describe InstantiatedContract do
|
3
|
+
describe '#replace!' do
|
4
|
+
let(:body) { double('body') }
|
5
|
+
let(:response) { double(:body => body) }
|
6
|
+
let(:values) { double('values') }
|
7
|
+
|
8
|
+
context 'when response body is a hash' do
|
9
|
+
let(:normalized_values) { double('normalized values') }
|
10
|
+
let(:normalized_body) { double('normalized body') }
|
11
|
+
let(:merged_body) { double('merged body') }
|
12
|
+
|
13
|
+
it 'should normalize keys and deep merge response body with given values' do
|
14
|
+
values.should_receive(:normalize_keys).and_return(normalized_values)
|
15
|
+
response.body.should_receive(:normalize_keys).and_return(normalized_body)
|
16
|
+
normalized_body.should_receive(:deep_merge).with(normalized_values).and_return(merged_body)
|
17
|
+
|
18
|
+
instantiated_contract = described_class.new(nil, response)
|
19
|
+
instantiated_contract.replace!(values)
|
20
|
+
|
21
|
+
instantiated_contract.response_body.should == merged_body
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when response body is a string' do
|
26
|
+
let(:body) { 'foo' }
|
27
|
+
|
28
|
+
it 'should replace response body with given values' do
|
29
|
+
instantiated_contract = described_class.new(nil, response)
|
30
|
+
instantiated_contract.replace!(values)
|
31
|
+
instantiated_contract.response_body.should == values
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when response body is nil' do
|
36
|
+
let(:body) { nil }
|
37
|
+
|
38
|
+
it 'should replace response body with given values' do
|
39
|
+
instantiated_contract = described_class.new(nil, response)
|
40
|
+
instantiated_contract.replace!(values)
|
41
|
+
instantiated_contract.response_body.should == values
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#response_body' do
|
47
|
+
let(:response) { double(:body => double('body')) }
|
48
|
+
|
49
|
+
it "should return response body" do
|
50
|
+
described_class.new(nil, response).response_body.should == response.body
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#request_path' do
|
55
|
+
let(:request) { double('request', :absolute_uri => "http://dummy_link/hello_world") }
|
56
|
+
let(:response) { double('response', :body => double('body')) }
|
57
|
+
|
58
|
+
it "should return the request absolute uri" do
|
59
|
+
described_class.new(request, response).request_path.should == "http://dummy_link/hello_world"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#request_uri' do
|
64
|
+
let(:request) { double('request', :full_uri => "http://dummy_link/hello_world?param=value#fragment") }
|
65
|
+
let(:response) { double('response', :body => double('body')) }
|
66
|
+
|
67
|
+
it "should return request full uri" do
|
68
|
+
described_class.new(request, response).request_uri.should == "http://dummy_link/hello_world?param=value#fragment"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#stub!' do
|
73
|
+
let(:request) do
|
74
|
+
double({
|
75
|
+
:host => 'http://localhost',
|
76
|
+
:method => method,
|
77
|
+
:path => '/hello_world',
|
78
|
+
:headers => {'Accept' => 'application/json'},
|
79
|
+
:params => {'foo' => 'bar'}
|
80
|
+
})
|
81
|
+
end
|
82
|
+
|
83
|
+
let(:method) { :get }
|
84
|
+
|
85
|
+
let(:response) do
|
86
|
+
double({
|
87
|
+
:status => 200,
|
88
|
+
:headers => {},
|
89
|
+
:body => body
|
90
|
+
})
|
91
|
+
end
|
92
|
+
|
93
|
+
let(:body) do
|
94
|
+
{'message' => 'foo'}
|
95
|
+
end
|
96
|
+
|
97
|
+
let(:stubbed_request) { double('stubbed request') }
|
98
|
+
|
99
|
+
before do
|
100
|
+
WebMock.should_receive(:stub_request).
|
101
|
+
with(request.method, "#{request.host}#{request.path}").
|
102
|
+
and_return(stubbed_request)
|
103
|
+
|
104
|
+
stubbed_request.stub(:to_return).with({
|
105
|
+
:status => response.status,
|
106
|
+
:headers => response.headers,
|
107
|
+
:body => response.body.to_json
|
108
|
+
})
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'when the response body is an object' do
|
112
|
+
let(:body) do
|
113
|
+
{'message' => 'foo'}
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should stub the response body with a json representation' do
|
117
|
+
stubbed_request.should_receive(:to_return).with({
|
118
|
+
:status => response.status,
|
119
|
+
:headers => response.headers,
|
120
|
+
:body => response.body.to_json
|
121
|
+
})
|
122
|
+
|
123
|
+
stubbed_request.stub(:with).and_return(stubbed_request)
|
124
|
+
|
125
|
+
described_class.new(request, response).stub!
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'when the response body is an array' do
|
130
|
+
let(:body) do
|
131
|
+
[1, 2, 3]
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'should stub the response body with a json representation' do
|
135
|
+
stubbed_request.should_receive(:to_return).with({
|
136
|
+
:status => response.status,
|
137
|
+
:headers => response.headers,
|
138
|
+
:body => response.body.to_json
|
139
|
+
})
|
140
|
+
|
141
|
+
stubbed_request.stub(:with).and_return(stubbed_request)
|
142
|
+
|
143
|
+
described_class.new(request, response).stub!
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context 'when the response body is not an object or an array' do
|
148
|
+
let(:body) { nil }
|
149
|
+
|
150
|
+
it 'should stub the response body with the original body' do
|
151
|
+
stubbed_request.should_receive(:to_return).with({
|
152
|
+
:status => response.status,
|
153
|
+
:headers => response.headers,
|
154
|
+
:body => response.body
|
155
|
+
})
|
156
|
+
|
157
|
+
stubbed_request.stub(:with).and_return(stubbed_request)
|
158
|
+
|
159
|
+
described_class.new(request, response).stub!
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'a GET request' do
|
164
|
+
let(:method) { :get }
|
165
|
+
|
166
|
+
it 'should use WebMock to stub the request' do
|
167
|
+
stubbed_request.should_receive(:with).
|
168
|
+
with({:headers => request.headers, :query => request.params}).
|
169
|
+
and_return(stubbed_request)
|
170
|
+
described_class.new(request, response).stub!
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context 'a POST request' do
|
175
|
+
let(:method) { :post }
|
176
|
+
|
177
|
+
it 'should use WebMock to stub the request' do
|
178
|
+
stubbed_request.should_receive(:with).
|
179
|
+
with({:headers => request.headers, :body => request.params}).
|
180
|
+
and_return(stubbed_request)
|
181
|
+
described_class.new(request, response).stub!
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
context 'a request with no headers' do
|
186
|
+
let(:request) do
|
187
|
+
double({
|
188
|
+
:host => 'http://localhost',
|
189
|
+
:method => :get,
|
190
|
+
:path => '/hello_world',
|
191
|
+
:headers => {},
|
192
|
+
:params => {'foo' => 'bar'}
|
193
|
+
})
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'should use WebMock to stub the request' do
|
197
|
+
stubbed_request.should_receive(:with).
|
198
|
+
with({:query => request.params}).
|
199
|
+
and_return(stubbed_request)
|
200
|
+
described_class.new(request, response).stub!
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
context 'a request with no params' do
|
205
|
+
let(:request) do
|
206
|
+
double({
|
207
|
+
:host => 'http://localhost',
|
208
|
+
:method => :get,
|
209
|
+
:path => '/hello_world',
|
210
|
+
:headers => {},
|
211
|
+
:params => {}
|
212
|
+
})
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'should use WebMock to stub the request' do
|
216
|
+
stubbed_request.should_receive(:with).
|
217
|
+
with({}).
|
218
|
+
and_return(stubbed_request)
|
219
|
+
described_class.new(request, response).stub!
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|