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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Pacto
2
+ VERSION = "0.0.1"
3
+ 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