contracts_api_test 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|