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.
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +117 -0
- data/Rakefile +7 -0
- data/TODO.md +19 -0
- data/contracts.gemspec +29 -0
- data/lib/contracts.rb +46 -0
- data/lib/contracts/contract.rb +18 -0
- data/lib/contracts/extensions.rb +18 -0
- data/lib/contracts/instantiated_contract.rb +63 -0
- data/lib/contracts/rake_task.rb +75 -0
- data/lib/contracts/request.rb +62 -0
- data/lib/contracts/response.rb +27 -0
- data/lib/contracts/response_adapter.rb +24 -0
- data/lib/contracts/version.rb +3 -0
- data/lib/json-generator.rb +1 -0
- data/lib/json/generator.rb +18 -0
- data/lib/json/generator/array_attribute.rb +11 -0
- data/lib/json/generator/attribute_factory.rb +18 -0
- data/lib/json/generator/basic_attribute.rb +17 -0
- data/lib/json/generator/boolean_attribute.rb +7 -0
- data/lib/json/generator/dereferencer.rb +22 -0
- data/lib/json/generator/empty_attribute.rb +7 -0
- data/lib/json/generator/integer_attribute.rb +7 -0
- data/lib/json/generator/object_attribute.rb +18 -0
- data/lib/json/generator/string_attribute.rb +7 -0
- data/spec/contracts/contract_spec.rb +50 -0
- data/spec/contracts/contracts_spec.rb +77 -0
- data/spec/contracts/extensions_spec.rb +34 -0
- data/spec/contracts/instantiated_contract_spec.rb +224 -0
- data/spec/contracts/request_spec.rb +73 -0
- data/spec/contracts/response_adapter_spec.rb +27 -0
- data/spec/contracts/response_spec.rb +114 -0
- data/spec/data/contract.json +25 -0
- data/spec/json/generator/array_attribute_spec.rb +42 -0
- data/spec/json/generator/attribute_factory_spec.rb +72 -0
- data/spec/json/generator/basic_attribute_spec.rb +41 -0
- data/spec/json/generator/boolean_attribute_spec.rb +17 -0
- data/spec/json/generator/dereferencer_spec.rb +72 -0
- data/spec/json/generator/empty_attribute_spec.rb +17 -0
- data/spec/json/generator/integer_attribute_spec.rb +17 -0
- data/spec/json/generator/object_attribute_spec.rb +100 -0
- data/spec/json/generator/string_attribute_spec.rb +17 -0
- data/spec/json/generator_spec.rb +20 -0
- data/spec/spec_helper.rb +1 -0
- metadata +259 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
module Contracts
|
2
|
+
describe Request do
|
3
|
+
let(:host) { 'http://localhost' }
|
4
|
+
let(:method) { 'GET' }
|
5
|
+
let(:path) { '/hello_world' }
|
6
|
+
let(:headers) { {'accept' => 'application/json'} }
|
7
|
+
let(:params) { {'foo' => 'bar'} }
|
8
|
+
|
9
|
+
let(:request) do
|
10
|
+
described_class.new(host, {
|
11
|
+
'method' => method,
|
12
|
+
'path' => path,
|
13
|
+
'headers' => headers,
|
14
|
+
'params' => params
|
15
|
+
})
|
16
|
+
end
|
17
|
+
subject { request }
|
18
|
+
|
19
|
+
its(:method) { should == :get }
|
20
|
+
its(:path) { should == path }
|
21
|
+
its(:headers) { should == headers }
|
22
|
+
its(:params) { should == params }
|
23
|
+
|
24
|
+
describe '#execute' do
|
25
|
+
let(:connection) { double('connection') }
|
26
|
+
let(:response) { double('response') }
|
27
|
+
let(:adapted_response) { double('adapted response') }
|
28
|
+
|
29
|
+
context 'for a GET request' do
|
30
|
+
it 'should make a GET request and return the response' do
|
31
|
+
HTTParty.should_receive(:get).
|
32
|
+
with(host + path, {:query => params, :headers => headers}).
|
33
|
+
and_return(response)
|
34
|
+
ResponseAdapter.should_receive(:new).with(response).and_return(adapted_response)
|
35
|
+
request.execute.should == adapted_response
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'for a POST request' do
|
40
|
+
let(:method) { 'POST' }
|
41
|
+
|
42
|
+
it 'should make a POST request and return the response' do
|
43
|
+
HTTParty.should_receive(:post).
|
44
|
+
with(host + path, {:body => params.to_json, :headers => headers}).
|
45
|
+
and_return(response)
|
46
|
+
ResponseAdapter.should_receive(:new).with(response).and_return(adapted_response)
|
47
|
+
request.execute.should == adapted_response
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#absolute_uri" do
|
53
|
+
it "should be equal to the host followed by the path" do
|
54
|
+
request.absolute_uri.should == "http://localhost/hello_world"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#full_uri" do
|
59
|
+
context "when the query (params) exists" do
|
60
|
+
it "should be equal to the host followed by the path and the query" do
|
61
|
+
request.full_uri.should == "http://localhost/hello_world?foo=bar"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "when the query (params) does not exists" do
|
66
|
+
let(:params) { {} }
|
67
|
+
it "should be equal to the host followed by the path" do
|
68
|
+
request.full_uri.should == "http://localhost/hello_world"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Contracts
|
2
|
+
describe ResponseAdapter do
|
3
|
+
let(:response) do
|
4
|
+
double({
|
5
|
+
:code => 200,
|
6
|
+
:headers => {'foo' => ['bar', 'baz'], 'hello' => ['world']},
|
7
|
+
:body => double('body')
|
8
|
+
})
|
9
|
+
end
|
10
|
+
|
11
|
+
before do
|
12
|
+
@response_adapter = described_class.new(response)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should have a status' do
|
16
|
+
@response_adapter.status.should == response.code
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should have a body' do
|
20
|
+
@response_adapter.body.should == response.body
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should normalize headers values according to RFC2616' do
|
24
|
+
@response_adapter.headers.should == {'foo' => 'bar,baz', 'hello' => 'world'}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Contracts
|
2
|
+
describe Response do
|
3
|
+
let(:definition) do
|
4
|
+
{
|
5
|
+
'status' => 200,
|
6
|
+
'headers' => {'Content-Type' => 'application/json'},
|
7
|
+
'body' => double('definition body')
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#instantiate' do
|
12
|
+
let(:generated_body) { double('generated body') }
|
13
|
+
|
14
|
+
it 'should instantiate a response with a body that matches the given definition' do
|
15
|
+
JSON::Generator.should_receive(:generate).
|
16
|
+
with(definition['body']).
|
17
|
+
and_return(generated_body)
|
18
|
+
|
19
|
+
response = described_class.new(definition).instantiate
|
20
|
+
response.status.should == definition['status']
|
21
|
+
response.headers.should == definition['headers']
|
22
|
+
response.body.should == generated_body
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#validate' do
|
27
|
+
let(:status) { 200 }
|
28
|
+
let(:headers) { {'Content-Type' => 'application/json', 'Age' => '60'} }
|
29
|
+
let(:fake_response) do
|
30
|
+
double({
|
31
|
+
:status => status,
|
32
|
+
:headers => headers,
|
33
|
+
:body => 'body'
|
34
|
+
})
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'when status, headers and body match' do
|
38
|
+
it 'should not return any errors' do
|
39
|
+
JSON::Validator.should_receive(:fully_validate).
|
40
|
+
with(definition['body'], fake_response.body).
|
41
|
+
and_return([])
|
42
|
+
|
43
|
+
response = described_class.new(definition)
|
44
|
+
response.validate(fake_response).should == []
|
45
|
+
end
|
46
|
+
end
|
47
|
+
context 'when status, headers and body match' do
|
48
|
+
it 'should not return any errors' do
|
49
|
+
JSON::Validator.should_receive(:fully_validate).
|
50
|
+
with(definition['body'], fake_response.body).
|
51
|
+
and_return([])
|
52
|
+
|
53
|
+
response = described_class.new(definition)
|
54
|
+
response.validate(fake_response).should == []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'when status does not match' do
|
59
|
+
let(:status) { 500 }
|
60
|
+
|
61
|
+
it 'should return a status error' do
|
62
|
+
JSON::Validator.stub!(:fully_validate).and_return([])
|
63
|
+
|
64
|
+
response = described_class.new(definition)
|
65
|
+
response.validate(fake_response).should == ["Invalid status: expected #{definition['status']} but got #{status}"]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'when headers do not match' do
|
70
|
+
let(:headers) { {'Content-Type' => 'text/html'} }
|
71
|
+
|
72
|
+
it 'should return a header error' do
|
73
|
+
JSON::Validator.stub!(:fully_validate).and_return([])
|
74
|
+
|
75
|
+
response = described_class.new(definition)
|
76
|
+
response.validate(fake_response).should == ["Invalid headers: expected #{definition['headers'].inspect} to be a subset of #{headers.inspect}"]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'when headers are a subset of expected headers' do
|
81
|
+
let(:headers) { {'Content-Type' => 'application/json'} }
|
82
|
+
|
83
|
+
it 'should not return any errors' do
|
84
|
+
JSON::Validator.stub!(:fully_validate).and_return([])
|
85
|
+
|
86
|
+
response = described_class.new(definition)
|
87
|
+
response.validate(fake_response).should == []
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'when headers values match but keys have different case' do
|
92
|
+
let(:headers) { {'content-type' => 'application/json'} }
|
93
|
+
|
94
|
+
it 'should not return any errors' do
|
95
|
+
JSON::Validator.stub!(:fully_validate).and_return([])
|
96
|
+
|
97
|
+
response = described_class.new(definition)
|
98
|
+
response.validate(fake_response).should == []
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'when body does not match' do
|
103
|
+
let(:errors) { [double('error1'), double('error2')] }
|
104
|
+
|
105
|
+
it 'should return a list of errors' do
|
106
|
+
JSON::Validator.stub!(:fully_validate).and_return(errors)
|
107
|
+
|
108
|
+
response = described_class.new(definition)
|
109
|
+
response.validate(fake_response).should == errors
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
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,42 @@
|
|
1
|
+
module JSON
|
2
|
+
module Generator
|
3
|
+
describe ArrayAttribute do
|
4
|
+
it 'should be a BasicAttribute' do
|
5
|
+
described_class.new(nil).should be_a_kind_of(BasicAttribute)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '#generate' do
|
9
|
+
context 'with minItems' do
|
10
|
+
it 'should generate an array with two objects of the expected type' do
|
11
|
+
stubbed_item = stub('item', :generate => 'foo')
|
12
|
+
AttributeFactory.should_receive(:create).twice.
|
13
|
+
with('type' => 'dummy_type').
|
14
|
+
and_return(stubbed_item)
|
15
|
+
|
16
|
+
properties = {
|
17
|
+
'type' => 'array',
|
18
|
+
'minItems' => 2,
|
19
|
+
'items' => {
|
20
|
+
'type' => 'dummy_type'
|
21
|
+
}
|
22
|
+
}
|
23
|
+
described_class.new(properties).generate.should ==
|
24
|
+
[stubbed_item.generate, stubbed_item.generate]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'without minItems' do
|
29
|
+
it 'should generate an empty array' do
|
30
|
+
properties = {
|
31
|
+
'type' => 'array',
|
32
|
+
'items' => {
|
33
|
+
'type' => 'dummy_type'
|
34
|
+
}
|
35
|
+
}
|
36
|
+
described_class.new(properties).generate.should == []
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module JSON
|
2
|
+
module Generator
|
3
|
+
describe AttributeFactory do
|
4
|
+
describe '.create' do
|
5
|
+
let(:attribute) { stub('attribute') }
|
6
|
+
|
7
|
+
context 'when type is a string' do
|
8
|
+
let(:properties) { {'type' => 'string'} }
|
9
|
+
|
10
|
+
it 'should create a StringAttribute' do
|
11
|
+
StringAttribute.should_receive(:new).with(properties).and_return(attribute)
|
12
|
+
described_class.create(properties).should == attribute
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'when type is an object' do
|
17
|
+
let(:properties) { {'type' => 'object'} }
|
18
|
+
|
19
|
+
it 'should create an ObjectAttribute' do
|
20
|
+
ObjectAttribute.should_receive(:new).with(properties).and_return(attribute)
|
21
|
+
described_class.create(properties).should == attribute
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when type is an array' do
|
26
|
+
let(:properties) { {'type' => 'array'} }
|
27
|
+
|
28
|
+
it 'should create an ArrayAttribute' do
|
29
|
+
ArrayAttribute.should_receive(:new).with(properties).and_return(attribute)
|
30
|
+
described_class.create(properties).should == attribute
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'when type is an integer' do
|
35
|
+
let(:properties) { {'type' => 'integer'} }
|
36
|
+
|
37
|
+
it 'should create an IntegerAttribute' do
|
38
|
+
IntegerAttribute.should_receive(:new).with(properties).and_return(attribute)
|
39
|
+
described_class.create(properties).should == attribute
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when type is a boolean' do
|
44
|
+
let(:properties) { {'type' => 'boolean'} }
|
45
|
+
|
46
|
+
it 'should create a BooleanAttribute' do
|
47
|
+
BooleanAttribute.should_receive(:new).with(properties).and_return(attribute)
|
48
|
+
described_class.create(properties).should == attribute
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'when we have an array of types' do
|
53
|
+
let(:properties) { {'type' => ['string', 'boolean']} }
|
54
|
+
|
55
|
+
it 'should create a the default attribute for the first type' do
|
56
|
+
StringAttribute.should_receive(:new).with(properties).and_return(attribute)
|
57
|
+
described_class.create(properties).should == attribute
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'when there is no type' do
|
62
|
+
let(:properties) { {} }
|
63
|
+
|
64
|
+
it 'should create an EmptyAttribute' do
|
65
|
+
EmptyAttribute.should_receive(:new).with(properties).and_return(attribute)
|
66
|
+
described_class.create(properties).should == attribute
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module JSON
|
2
|
+
module Generator
|
3
|
+
describe BasicAttribute do
|
4
|
+
describe '#generate' do
|
5
|
+
context 'with default value' do
|
6
|
+
let(:properties) { {'default' => stub('default')} }
|
7
|
+
|
8
|
+
it 'should return the default value' do
|
9
|
+
described_class.new(properties).generate.should == properties['default']
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#required?' do
|
15
|
+
context 'when required property is true' do
|
16
|
+
let(:properties) { {'required' => true} }
|
17
|
+
|
18
|
+
it 'should be required' do
|
19
|
+
described_class.new(properties).should be_required
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when required property is false' do
|
24
|
+
let(:properties) { {'required' => false} }
|
25
|
+
|
26
|
+
it 'should not be required' do
|
27
|
+
described_class.new(properties).should_not be_required
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when required property is not defined' do
|
32
|
+
let(:properties) { {} }
|
33
|
+
|
34
|
+
it 'should not be required' do
|
35
|
+
described_class.new(properties).should_not be_required
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module JSON
|
2
|
+
module Generator
|
3
|
+
describe BooleanAttribute do
|
4
|
+
it 'should be a BasicAttribute' do
|
5
|
+
described_class.new(nil).should be_kind_of(BasicAttribute)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '#generate' do
|
9
|
+
context 'without a default value' do
|
10
|
+
it 'should return the default value' do
|
11
|
+
described_class.new({'type' => 'boolean'}).generate.should == false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module JSON
|
2
|
+
module Generator
|
3
|
+
describe Dereferencer do
|
4
|
+
describe '.dereference' do
|
5
|
+
let(:schema) do
|
6
|
+
{
|
7
|
+
'type' => 'object',
|
8
|
+
'properties' => {
|
9
|
+
'referencer' => referencer
|
10
|
+
},
|
11
|
+
'definitions' => definitions
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:definitions) { {'referenced' => referenced} }
|
16
|
+
|
17
|
+
let(:referencer) do
|
18
|
+
{
|
19
|
+
'type' => 'object',
|
20
|
+
'$ref' => '#/definitions/referenced'
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:referenced) do
|
25
|
+
{
|
26
|
+
'properties' => {
|
27
|
+
'message' => { 'type' => 'string' }
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should replace references with concrete definitions' do
|
33
|
+
described_class.dereference(schema).should == {
|
34
|
+
'type' => 'object',
|
35
|
+
'properties' => {
|
36
|
+
'referencer' => {
|
37
|
+
'type' => 'object',
|
38
|
+
'properties' => {
|
39
|
+
'message' => { 'type' => 'string' }
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'when the schema does not have any reference', 'but has definitions' do
|
47
|
+
let(:schema) { {'type' => 'object', 'properties' => {}, 'definitions' => {}} }
|
48
|
+
|
49
|
+
it 'should return the original schema without the definitions' do
|
50
|
+
described_class.dereference(schema).should == {'type' => 'object', 'properties' => {}}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'when the root element does not have properties' do
|
55
|
+
let(:schema) { {'type' => 'string'} }
|
56
|
+
|
57
|
+
it 'should return the original schema' do
|
58
|
+
described_class.dereference(schema).should == schema
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when a referenced definition does not exist' do
|
63
|
+
let(:definitions) { {} }
|
64
|
+
|
65
|
+
it 'should raise a name error' do
|
66
|
+
expect { described_class.dereference(schema) }.to raise_error NameError
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|