pacto 0.2.5 → 0.3.0.pre
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 +3 -0
- data/.rspec +0 -2
- data/.rubocop-todo.yml +51 -0
- data/.rubocop.yml +1 -0
- data/.travis.yml +4 -2
- data/Guardfile +28 -14
- data/README.md +81 -51
- data/Rakefile +24 -11
- data/features/generation/generation.feature +25 -0
- data/features/journeys/validation.feature +74 -0
- data/features/support/env.rb +16 -0
- data/lib/pacto.rb +63 -34
- data/lib/pacto/contract.rb +25 -11
- data/lib/pacto/contract_factory.rb +13 -44
- data/lib/pacto/core/callback.rb +11 -0
- data/lib/pacto/core/configuration.rb +34 -0
- data/lib/pacto/core/contract_repository.rb +44 -0
- data/lib/pacto/erb_processor.rb +18 -0
- data/lib/pacto/exceptions/invalid_contract.rb +10 -1
- data/lib/pacto/extensions.rb +2 -2
- data/lib/pacto/generator.rb +75 -0
- data/lib/pacto/hash_merge_processor.rb +14 -0
- data/lib/pacto/hooks/erb_hook.rb +17 -0
- data/lib/pacto/logger.rb +42 -0
- data/lib/pacto/meta_schema.rb +17 -0
- data/lib/pacto/rake_task.rb +75 -12
- data/lib/pacto/request.rb +3 -4
- data/lib/pacto/response.rb +27 -19
- data/lib/pacto/server.rb +2 -0
- data/lib/pacto/server/dummy.rb +45 -0
- data/lib/pacto/server/playback_servlet.rb +21 -0
- data/lib/pacto/stubs/built_in.rb +57 -0
- data/lib/pacto/version.rb +1 -1
- data/pacto.gemspec +8 -2
- data/resources/contract_schema.json +216 -0
- data/spec/coveralls_helper.rb +10 -0
- data/spec/integration/data/strict_contract.json +33 -0
- data/spec/integration/data/templating_contract.json +25 -0
- data/spec/integration/e2e_spec.rb +40 -7
- data/spec/integration/templating_spec.rb +55 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/unit/data/simple_contract.json +22 -0
- data/spec/unit/hooks/erb_hook_spec.rb +51 -0
- data/spec/unit/pacto/configuration_spec.rb +51 -0
- data/spec/unit/pacto/contract_factory_spec.rb +4 -35
- data/spec/unit/pacto/contract_spec.rb +59 -31
- data/spec/unit/pacto/core/configuration_spec.rb +28 -0
- data/spec/unit/pacto/core/contract_repository_spec.rb +133 -0
- data/spec/unit/pacto/erb_processor_spec.rb +23 -0
- data/spec/unit/pacto/extensions_spec.rb +11 -11
- data/spec/unit/pacto/generator_spec.rb +142 -0
- data/spec/unit/pacto/hash_merge_processor_spec.rb +20 -0
- data/spec/unit/pacto/logger_spec.rb +44 -0
- data/spec/unit/pacto/meta_schema_spec.rb +70 -0
- data/spec/unit/pacto/pacto_spec.rb +32 -58
- data/spec/unit/pacto/request_spec.rb +83 -34
- data/spec/unit/pacto/response_adapter_spec.rb +9 -11
- data/spec/unit/pacto/response_spec.rb +68 -68
- data/spec/unit/pacto/server/playback_servlet_spec.rb +24 -0
- data/spec/unit/pacto/stubs/built_in_spec.rb +168 -0
- metadata +291 -147
- data/.rspec_integration +0 -4
- data/.rspec_unit +0 -4
- data/lib/pacto/file_pre_processor.rb +0 -12
- data/lib/pacto/instantiated_contract.rb +0 -62
- data/spec/integration/spec_helper.rb +0 -1
- data/spec/integration/utils/dummy_server.rb +0 -34
- data/spec/unit/pacto/file_pre_processor_spec.rb +0 -13
- data/spec/unit/pacto/instantiated_contract_spec.rb +0 -224
- data/spec/unit/spec_helper.rb +0 -5
@@ -0,0 +1,23 @@
|
|
1
|
+
module Pacto
|
2
|
+
describe ERBProcessor do
|
3
|
+
subject(:processor) { ERBProcessor.new }
|
4
|
+
|
5
|
+
describe '#process' do
|
6
|
+
let(:erb) { '2 + 2 = <%= 2 + 2 %>' }
|
7
|
+
let(:result) { '2 + 2 = 4' }
|
8
|
+
|
9
|
+
it 'returns the result of ERB' do
|
10
|
+
expect(processor.process(erb)).to eq result
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'logs the erb processed' do
|
14
|
+
Logger.instance.should_receive(:debug).with("Processed contract: \"#{result}\"")
|
15
|
+
processor.process erb
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'does not mess with pure JSONs' do
|
19
|
+
processor.process('{"property": ["one", "two, null"]}')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -3,30 +3,30 @@ module Pacto
|
|
3
3
|
describe HashSubsetOf do
|
4
4
|
describe '#subset_of?' do
|
5
5
|
context 'when the other hash is the same' do
|
6
|
-
it '
|
7
|
-
{:a => 'a'}.
|
6
|
+
it 'returns true' do
|
7
|
+
expect({:a => 'a'}).to be_subset_of({:a => 'a'})
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
11
|
context 'when the other hash is a subset' do
|
12
|
-
it '
|
13
|
-
{:a => 'a'}.
|
12
|
+
it 'returns true' do
|
13
|
+
expect({:a => 'a'}).to be_subset_of({:a => 'a', :b => 'b'})
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
17
|
context 'when the other hash is not a subset' do
|
18
|
-
it '
|
19
|
-
{:a => 'a'}.subset_of?({:a => 'b'}).
|
18
|
+
it 'returns false' do
|
19
|
+
expect({:a => 'a'}.subset_of?({:a => 'b'})).to be_false
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
24
|
describe '#normalize_keys' do
|
25
|
-
it '
|
26
|
-
{:A => 'a'}.normalize_keys.
|
27
|
-
{:a => 'a'}.normalize_keys.
|
28
|
-
{'A' => 'a'}.normalize_keys.
|
29
|
-
{'a' => 'a'}.normalize_keys.
|
25
|
+
it 'turns keys into downcased strings' do
|
26
|
+
expect({:A => 'a'}.normalize_keys).to eq({'a' => 'a'})
|
27
|
+
expect({:a => 'a'}.normalize_keys).to eq({'a' => 'a'})
|
28
|
+
expect({'A' => 'a'}.normalize_keys).to eq({'a' => 'a'})
|
29
|
+
expect({'a' => 'a'}.normalize_keys).to eq({'a' => 'a'})
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Pacto
|
2
|
+
describe Generator do
|
3
|
+
let(:record_host) {
|
4
|
+
'http://example.com'
|
5
|
+
}
|
6
|
+
let(:request) do
|
7
|
+
Pacto::Request.new(record_host, {
|
8
|
+
'method' => 'GET',
|
9
|
+
'path' => '/abcd',
|
10
|
+
'headers' => {
|
11
|
+
'Content-Length' => [1234],
|
12
|
+
'Via' => ['Some Proxy'],
|
13
|
+
'User-Agent' => ['rspec']
|
14
|
+
},
|
15
|
+
'params' => []
|
16
|
+
})
|
17
|
+
end
|
18
|
+
let(:response_adapter) do
|
19
|
+
Pacto::ResponseAdapter.new(
|
20
|
+
OpenStruct.new({
|
21
|
+
'status' => 200,
|
22
|
+
'headers' => {
|
23
|
+
'Date' => [Time.now],
|
24
|
+
'Server' => ['Fake Server'],
|
25
|
+
'Content-Type' => ['application/json']
|
26
|
+
},
|
27
|
+
'body' => double('dummy body')
|
28
|
+
})
|
29
|
+
)
|
30
|
+
end
|
31
|
+
let(:response_body_schema) { '{"message": "dummy generated schema"}' }
|
32
|
+
let(:version) { 'draft3' }
|
33
|
+
let(:schema_generator) { double('schema_generator') }
|
34
|
+
let(:validator) { double('validator') }
|
35
|
+
let(:request_file) { 'request.json' }
|
36
|
+
let(:generator) { described_class.new version, schema_generator, validator }
|
37
|
+
|
38
|
+
def pretty obj
|
39
|
+
MultiJson.encode(obj, :pretty => true).gsub(/^$\n/, '')
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#generate' do
|
43
|
+
let(:request_contract) {
|
44
|
+
double({
|
45
|
+
:request => request,
|
46
|
+
})
|
47
|
+
}
|
48
|
+
let(:generated_contract) { double('generated contract') }
|
49
|
+
before do
|
50
|
+
Pacto.should_receive(:build_from_file).with(request_file, record_host).and_return request_contract
|
51
|
+
request.should_receive(:execute).and_return response_adapter
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'parses the request' do
|
55
|
+
generator.should_receive(:save).with(request_file, request, anything)
|
56
|
+
generator.generate request_file, record_host
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'fetches a response' do
|
60
|
+
generator.should_receive(:save).with(request_file, anything, response_adapter)
|
61
|
+
generator.generate request_file, record_host
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'saves the result' do
|
65
|
+
generator.should_receive(:save).with(request_file, request, response_adapter).and_return generated_contract
|
66
|
+
expect(generator.generate request_file, record_host).to eq(generated_contract)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#save' do
|
71
|
+
context 'invalid schema' do
|
72
|
+
it 'raises an error if schema generation fails' do
|
73
|
+
JSON::SchemaGenerator.should_receive(:generate).and_raise ArgumentError.new('Could not generate schema')
|
74
|
+
expect { generator.save request_file, request, response_adapter }.to raise_error
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'raises an error if the generated contract is invalid' do
|
78
|
+
JSON::SchemaGenerator.should_receive(:generate).and_return response_body_schema
|
79
|
+
validator.should_receive(:validate).and_raise InvalidContract.new('dummy error')
|
80
|
+
expect { generator.save request_file, request, response_adapter }.to raise_error
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'valid schema' do
|
85
|
+
let(:raw_contract) {
|
86
|
+
JSON::SchemaGenerator.should_receive(:generate).with(request_file, response_adapter.body, 'draft3').and_return response_body_schema
|
87
|
+
validator.should_receive(:validate).and_return true
|
88
|
+
generator.save request_file, request, response_adapter
|
89
|
+
}
|
90
|
+
subject(:generated_contract) { JSON.parse raw_contract }
|
91
|
+
|
92
|
+
it 'sets the body to the generated json-schema' do
|
93
|
+
expect(subject['response']['body']).to eq(JSON.parse response_body_schema)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'sets the request attributes' do
|
97
|
+
generated_request = subject['request']
|
98
|
+
expect(generated_request['params']).to eq(request.params)
|
99
|
+
expect(generated_request['path']).to eq(request.path)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'keeps important request headers' do
|
103
|
+
saved_headers = subject['request']['headers']
|
104
|
+
expect(saved_headers.keys).to include 'User-Agent'
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'filters informational request headers' do
|
108
|
+
saved_headers = subject['request']['headers']
|
109
|
+
expect(saved_headers).not_to include 'Date'
|
110
|
+
expect(saved_headers).not_to include 'Server'
|
111
|
+
expect(saved_headers).not_to include 'Content-Length'
|
112
|
+
expect(saved_headers).not_to include 'Connection'
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'normalizes the request method' do
|
116
|
+
generated_request = subject['request']
|
117
|
+
expect(generated_request['method']).to eq(request.method.downcase.to_s)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'sets the response attributes' do
|
121
|
+
generated_response = subject['response']
|
122
|
+
expect(generated_response['status']).to eq(response_adapter.status)
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'keeps important response headers' do
|
126
|
+
saved_headers = subject['response']['headers']
|
127
|
+
expect(saved_headers.keys).to include 'Content-Type'
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'filters informational response headers' do
|
131
|
+
saved_headers = subject['response']['headers']
|
132
|
+
expect(saved_headers).not_to include 'Content-Length'
|
133
|
+
expect(saved_headers).not_to include 'Via'
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'generates pretty JSON' do
|
137
|
+
expect(raw_contract).to eq(pretty(subject))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Pacto
|
2
|
+
describe HashMergeProcessor do
|
3
|
+
describe '#process' do
|
4
|
+
let(:response_body_string) { 'a simple string' }
|
5
|
+
let(:response_body_hash) {
|
6
|
+
{'a' => 'simple hash'}
|
7
|
+
}
|
8
|
+
|
9
|
+
it 'does not change contract if values is nil' do
|
10
|
+
expect(subject.process(response_body_string, nil)).to eq response_body_string
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'merges response body with values' do
|
14
|
+
merged_body = {'a' => 'simple hash', 'b' => :key}
|
15
|
+
expect(subject.process(response_body_hash, {:b => :key})).to eq merged_body.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Pacto
|
2
|
+
describe Logger do
|
3
|
+
before do
|
4
|
+
logger.log logger_lib
|
5
|
+
end
|
6
|
+
|
7
|
+
subject(:logger) { described_class.instance }
|
8
|
+
let(:logger_lib) { ::Logger.new(StringIO.new) }
|
9
|
+
|
10
|
+
it 'delegates debug to the logger lib' do
|
11
|
+
logger_lib.should_receive(:debug)
|
12
|
+
logger.debug
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'delegates info to the logger lib' do
|
16
|
+
logger_lib.should_receive(:info)
|
17
|
+
logger.info
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'delegates warn to the logger lib' do
|
21
|
+
logger_lib.should_receive(:warn)
|
22
|
+
logger.warn
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'delegates error to the logger lib' do
|
26
|
+
logger_lib.should_receive(:error)
|
27
|
+
logger.error
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'delegates fatal to the logger lib' do
|
31
|
+
logger_lib.should_receive(:error)
|
32
|
+
logger.error
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'has the default log level as error' do
|
36
|
+
expect(logger.level).to eq :error
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'provides access to the log level' do
|
40
|
+
logger.level = :info
|
41
|
+
expect(logger.level).to eq :info
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Pacto
|
2
|
+
describe MetaSchema do
|
3
|
+
let(:valid_contract) do
|
4
|
+
<<-EOF
|
5
|
+
{
|
6
|
+
"request": {
|
7
|
+
"method": "GET",
|
8
|
+
"path": "/hello_world",
|
9
|
+
"headers": {
|
10
|
+
"Accept": "application/json"
|
11
|
+
},
|
12
|
+
"params": {}
|
13
|
+
},
|
14
|
+
|
15
|
+
"response": {
|
16
|
+
"status": 200,
|
17
|
+
"headers": {
|
18
|
+
"Content-Type": "application/json"
|
19
|
+
},
|
20
|
+
"body": {
|
21
|
+
"description": "A simple response",
|
22
|
+
"type": "object",
|
23
|
+
"properties": {
|
24
|
+
"message": {
|
25
|
+
"type": "string"
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
EOF
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:invalid_contract) do
|
35
|
+
<<-EOF
|
36
|
+
{
|
37
|
+
"request": {
|
38
|
+
"method": "GET",
|
39
|
+
"path": "/hello_world",
|
40
|
+
"headers": {
|
41
|
+
"Accept": "application/json"
|
42
|
+
},
|
43
|
+
"params": {}
|
44
|
+
}
|
45
|
+
|
46
|
+
}
|
47
|
+
EOF
|
48
|
+
end
|
49
|
+
|
50
|
+
subject(:schema) { MetaSchema.new }
|
51
|
+
|
52
|
+
describe 'when validating a contract against the master schema' do
|
53
|
+
context 'with a valid contract structure' do
|
54
|
+
it 'does not raise any exceptions' do
|
55
|
+
expect {
|
56
|
+
schema.validate(valid_contract)
|
57
|
+
}.to_not raise_error(Exception)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'with an invalid contract structure' do
|
62
|
+
it 'raises InvalidContract exception' do
|
63
|
+
expect {
|
64
|
+
schema.validate(invalid_contract)
|
65
|
+
}.to raise_error(InvalidContract)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -1,23 +1,41 @@
|
|
1
1
|
describe Pacto do
|
2
|
-
let(:contract_name) { 'contract' }
|
3
|
-
let(:contract) { double('contract') }
|
4
2
|
|
5
|
-
|
6
|
-
|
3
|
+
around(:each) do |example|
|
4
|
+
$stdout = StringIO.new
|
5
|
+
example.run
|
6
|
+
$stdout = STDOUT
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
def output
|
10
|
+
$stdout.string.strip
|
11
|
+
end
|
12
|
+
|
13
|
+
def mock_validation(errors)
|
14
|
+
expect(JSON::Validator).to receive(:fully_validate).with(any_args).and_return errors
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '.validate_contract' do
|
18
|
+
context 'valid' do
|
19
|
+
it 'displays a success message and return true' do
|
20
|
+
mock_validation []
|
21
|
+
success = Pacto.validate_contract 'my_contract.json'
|
22
|
+
expect(output).to eq 'All contracts successfully meta-validated'
|
23
|
+
expect(success).to be_true
|
14
24
|
end
|
15
25
|
end
|
16
26
|
|
17
|
-
context '
|
18
|
-
it '
|
19
|
-
|
20
|
-
|
27
|
+
context 'invalid' do
|
28
|
+
it 'displays one error messages and return false' do
|
29
|
+
mock_validation ['Error 1']
|
30
|
+
success = Pacto.validate_contract 'my_contract.json'
|
31
|
+
expect(output).to match /error/
|
32
|
+
expect(success).to be_false
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'displays several error messages and return false' do
|
36
|
+
mock_validation ['Error 1', 'Error 2']
|
37
|
+
success = Pacto.validate_contract 'my_contract.json'
|
38
|
+
expect(success).to be_false
|
21
39
|
end
|
22
40
|
end
|
23
41
|
end
|
@@ -35,52 +53,8 @@ describe Pacto do
|
|
35
53
|
|
36
54
|
it 'returns whatever the factory returns' do
|
37
55
|
Pacto::ContractFactory.stub(:build_from_file => instantiated_contract)
|
38
|
-
described_class.build_from_file(path, host, file_pre_processor).
|
56
|
+
expect(described_class.build_from_file(path, host, file_pre_processor)).to eq instantiated_contract
|
39
57
|
end
|
40
58
|
end
|
41
59
|
|
42
|
-
describe '.use' do
|
43
|
-
before do
|
44
|
-
described_class.register(contract_name, contract)
|
45
|
-
end
|
46
|
-
|
47
|
-
context 'by default' do
|
48
|
-
let(:instantiated_contract) { double('instantiated contract', :response_body => response_body)}
|
49
|
-
let(:response_body) { double('response_body') }
|
50
|
-
|
51
|
-
before do
|
52
|
-
described_class.registered[contract_name].stub(:instantiate => instantiated_contract)
|
53
|
-
instantiated_contract.stub(:stub!)
|
54
|
-
end
|
55
|
-
|
56
|
-
it 'should instantiate a contract with default values' do
|
57
|
-
described_class.registered[contract_name].should_receive(:instantiate).with(nil).and_return(instantiated_contract)
|
58
|
-
described_class.use(contract_name)
|
59
|
-
end
|
60
|
-
|
61
|
-
it 'should return the instantiated contract' do
|
62
|
-
described_class.use(contract_name).should == instantiated_contract
|
63
|
-
end
|
64
|
-
|
65
|
-
it 'should stub further requests with the instantiated contract' do
|
66
|
-
instantiated_contract.should_receive(:stub!)
|
67
|
-
described_class.use(contract_name)
|
68
|
-
end
|
69
|
-
|
70
|
-
end
|
71
|
-
|
72
|
-
context 'when contract has not been registered' do
|
73
|
-
it 'should raise an argument error' do
|
74
|
-
expect { described_class.use('unregistered') }.to raise_error ArgumentError
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
describe '.unregister_all!' do
|
80
|
-
it 'should unregister all previously registered contracts' do
|
81
|
-
described_class.register(contract_name, contract)
|
82
|
-
described_class.unregister_all!
|
83
|
-
described_class.registered.should be_empty
|
84
|
-
end
|
85
|
-
end
|
86
60
|
end
|