contracts_api_test 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.
Files changed (49) hide show
  1. data/.gitignore +20 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/Guardfile +8 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +117 -0
  7. data/Rakefile +7 -0
  8. data/TODO.md +19 -0
  9. data/contracts.gemspec +29 -0
  10. data/lib/contracts.rb +46 -0
  11. data/lib/contracts/contract.rb +18 -0
  12. data/lib/contracts/extensions.rb +18 -0
  13. data/lib/contracts/instantiated_contract.rb +63 -0
  14. data/lib/contracts/rake_task.rb +75 -0
  15. data/lib/contracts/request.rb +62 -0
  16. data/lib/contracts/response.rb +27 -0
  17. data/lib/contracts/response_adapter.rb +24 -0
  18. data/lib/contracts/version.rb +3 -0
  19. data/lib/json-generator.rb +1 -0
  20. data/lib/json/generator.rb +18 -0
  21. data/lib/json/generator/array_attribute.rb +11 -0
  22. data/lib/json/generator/attribute_factory.rb +18 -0
  23. data/lib/json/generator/basic_attribute.rb +17 -0
  24. data/lib/json/generator/boolean_attribute.rb +7 -0
  25. data/lib/json/generator/dereferencer.rb +22 -0
  26. data/lib/json/generator/empty_attribute.rb +7 -0
  27. data/lib/json/generator/integer_attribute.rb +7 -0
  28. data/lib/json/generator/object_attribute.rb +18 -0
  29. data/lib/json/generator/string_attribute.rb +7 -0
  30. data/spec/contracts/contract_spec.rb +50 -0
  31. data/spec/contracts/contracts_spec.rb +77 -0
  32. data/spec/contracts/extensions_spec.rb +34 -0
  33. data/spec/contracts/instantiated_contract_spec.rb +224 -0
  34. data/spec/contracts/request_spec.rb +73 -0
  35. data/spec/contracts/response_adapter_spec.rb +27 -0
  36. data/spec/contracts/response_spec.rb +114 -0
  37. data/spec/data/contract.json +25 -0
  38. data/spec/json/generator/array_attribute_spec.rb +42 -0
  39. data/spec/json/generator/attribute_factory_spec.rb +72 -0
  40. data/spec/json/generator/basic_attribute_spec.rb +41 -0
  41. data/spec/json/generator/boolean_attribute_spec.rb +17 -0
  42. data/spec/json/generator/dereferencer_spec.rb +72 -0
  43. data/spec/json/generator/empty_attribute_spec.rb +17 -0
  44. data/spec/json/generator/integer_attribute_spec.rb +17 -0
  45. data/spec/json/generator/object_attribute_spec.rb +100 -0
  46. data/spec/json/generator/string_attribute_spec.rb +17 -0
  47. data/spec/json/generator_spec.rb +20 -0
  48. data/spec/spec_helper.rb +1 -0
  49. metadata +259 -0
@@ -0,0 +1,62 @@
1
+ module Contracts
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 path=(value)
21
+ @definition['path'] = value
22
+ end
23
+
24
+ def headers
25
+ @definition['headers']
26
+ end
27
+
28
+ def params
29
+ @definition['params']
30
+ end
31
+
32
+ def absolute_uri
33
+ @host + path
34
+ end
35
+
36
+ def full_uri
37
+ return absolute_uri if params.empty?
38
+
39
+ uri = Addressable::URI.new
40
+ uri.query_values = params
41
+
42
+ absolute_uri + '?' + uri.query
43
+ end
44
+
45
+ def execute
46
+ response = HTTParty.send(method, @host + path, {
47
+ httparty_params_key => normalized_params,
48
+ :headers => headers
49
+ })
50
+ ResponseAdapter.new(response)
51
+ end
52
+
53
+ private
54
+ def httparty_params_key
55
+ method == :get ? :query : :body
56
+ end
57
+
58
+ def normalized_params
59
+ method == :get ? params : params.to_json
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,27 @@
1
+ module Contracts
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 Contracts
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 Contracts
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1 @@
1
+ require 'json/generator'
@@ -0,0 +1,18 @@
1
+ require "json/generator/basic_attribute"
2
+ require "json/generator/empty_attribute"
3
+ require "json/generator/string_attribute"
4
+ require "json/generator/integer_attribute"
5
+ require "json/generator/array_attribute"
6
+ require "json/generator/object_attribute"
7
+ require "json/generator/boolean_attribute"
8
+ require "json/generator/attribute_factory"
9
+ require "json/generator/dereferencer"
10
+
11
+ module JSON
12
+ module Generator
13
+ def self.generate(schema)
14
+ dereferenced_schema = Dereferencer.dereference(schema)
15
+ AttributeFactory.create(dereferenced_schema).generate
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module JSON
2
+ module Generator
3
+ class ArrayAttribute < BasicAttribute
4
+ def generate
5
+ (@attributes['minItems'] || 0).times.map do |index|
6
+ AttributeFactory.create(@attributes['items']).generate
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ module JSON
2
+ module Generator
3
+ class AttributeFactory
4
+ CLASSES = {
5
+ 'string' => StringAttribute,
6
+ 'object' => ObjectAttribute,
7
+ 'integer' => IntegerAttribute,
8
+ 'array' => ArrayAttribute,
9
+ 'boolean' => BooleanAttribute,
10
+ nil => EmptyAttribute
11
+ }
12
+
13
+ def self.create(properties)
14
+ CLASSES[Array(properties['type']).first].new(properties)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ module JSON
2
+ module Generator
3
+ class BasicAttribute
4
+ def initialize(attributes)
5
+ @attributes = attributes
6
+ end
7
+
8
+ def generate
9
+ @attributes['default'] || self.class::DEFAULT_VALUE
10
+ end
11
+
12
+ def required?
13
+ @attributes['required']
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ module JSON
2
+ module Generator
3
+ class BooleanAttribute < BasicAttribute
4
+ DEFAULT_VALUE = false
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ module JSON
2
+ module Generator
3
+ class Dereferencer
4
+ def self.dereference(schema)
5
+ return schema unless schema.has_key?('properties')
6
+
7
+ definitions = schema.delete('definitions')
8
+ schema['properties'].each do |name, property|
9
+ next unless property.has_key?('$ref')
10
+
11
+ ref_name = property['$ref'].split('/').last
12
+ raise NameError, "definition for #{ref_name} not found" unless definitions.has_key?(ref_name)
13
+
14
+ property.merge!(definitions[ref_name])
15
+ property.delete('$ref')
16
+ end
17
+
18
+ schema
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module JSON
2
+ module Generator
3
+ class EmptyAttribute < BasicAttribute
4
+ DEFAULT_VALUE = nil
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module JSON
2
+ module Generator
3
+ class IntegerAttribute < BasicAttribute
4
+ DEFAULT_VALUE = 0
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ module JSON
2
+ module Generator
3
+ class ObjectAttribute < BasicAttribute
4
+ def generate
5
+ return nil unless required?
6
+ return {} unless @attributes.has_key?('properties')
7
+
8
+ @attributes['properties'].inject({}) do |json, (property_name, property_attributes)|
9
+ attribute = AttributeFactory.create(property_attributes)
10
+ if attribute.required?
11
+ json[property_name] = attribute.generate
12
+ end
13
+ json
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module JSON
2
+ module Generator
3
+ class StringAttribute < BasicAttribute
4
+ DEFAULT_VALUE = "bar"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ module Contracts
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,77 @@
1
+ describe Contracts do
2
+ let(:host) { 'http://localhost' }
3
+ let(:contract_name) { 'contract' }
4
+ let(:contract_path) { File.join('spec', 'data', "#{contract_name}.json") }
5
+ let(:contract) { double('contract') }
6
+
7
+ after do
8
+ described_class.unregister_all!
9
+ end
10
+
11
+ describe '.register' do
12
+ context 'by default' do
13
+ it 'should register a contract under a given name' do
14
+ described_class.register(contract_name, contract)
15
+ described_class.registered[contract_name].should == contract
16
+ end
17
+ end
18
+
19
+ context 'when a contract has already been registered with the same name' do
20
+ it 'should raise an argument error' do
21
+ described_class.register(contract_name, contract)
22
+ expect { described_class.register(contract_name, contract) }.to raise_error(ArgumentError)
23
+ end
24
+ end
25
+ end
26
+
27
+ describe '.build_from_file' do
28
+ it 'should build a contract given a file path and a host' do
29
+ described_class.build_from_file(contract_path, host).should be_a_kind_of(Contracts::Contract)
30
+ end
31
+ end
32
+
33
+ describe '.use' do
34
+ before do
35
+ described_class.register(contract_name, contract)
36
+ end
37
+
38
+ context 'by default' do
39
+ let(:instantiated_contract) { double('instantiated contract', :response_body => response_body)}
40
+ let(:response_body) { double('response_body') }
41
+
42
+ before do
43
+ described_class.registered[contract_name].stub(:instantiate => instantiated_contract)
44
+ instantiated_contract.stub(:stub!)
45
+ end
46
+
47
+ it 'should instantiate a contract with default values' do
48
+ described_class.registered[contract_name].should_receive(:instantiate).with(nil).and_return(instantiated_contract)
49
+ described_class.use(contract_name)
50
+ end
51
+
52
+ it 'should return the instantiated contract' do
53
+ described_class.use(contract_name).should == instantiated_contract
54
+ end
55
+
56
+ it 'should stub further requests with the instantiated contract' do
57
+ instantiated_contract.should_receive(:stub!)
58
+ described_class.use(contract_name)
59
+ end
60
+
61
+ end
62
+
63
+ context 'when contract has not been registered' do
64
+ it 'should raise an argument error' do
65
+ expect { described_class.use('unregistered') }.to raise_error ArgumentError
66
+ end
67
+ end
68
+ end
69
+
70
+ describe '.unregister_all!' do
71
+ it 'should unregister all previously registered contracts' do
72
+ described_class.register(contract_name, contract)
73
+ described_class.unregister_all!
74
+ described_class.registered.should be_empty
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,34 @@
1
+ module Contracts
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,224 @@
1
+ module Contracts
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