pacto 0.3.0.pre → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rubocop-todo.yml +0 -27
- data/.rubocop.yml +9 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -5
- data/CONTRIBUTING.md +112 -0
- data/Gemfile +5 -0
- data/Guardfile +18 -13
- data/README.md +157 -101
- data/Rakefile +3 -3
- data/features/configuration/strict_matchers.feature +97 -0
- data/features/evolve/README.md +11 -0
- data/features/evolve/existing_services.feature +82 -0
- data/features/generate/README.md +5 -0
- data/features/generate/generation.feature +28 -0
- data/features/steps/pacto_steps.rb +75 -0
- data/features/stub/README.md +2 -0
- data/features/stub/templates.feature +46 -0
- data/features/support/env.rb +11 -5
- data/features/validate/README.md +1 -0
- data/features/validate/body_only.feature +85 -0
- data/features/{journeys/validation.feature → validate/meta_validation.feature} +41 -24
- data/features/validate/validation.feature +36 -0
- data/lib/pacto.rb +61 -33
- data/lib/pacto/contract.rb +18 -15
- data/lib/pacto/contract_factory.rb +14 -11
- data/lib/pacto/contract_files.rb +17 -0
- data/lib/pacto/contract_list.rb +17 -0
- data/lib/pacto/contract_validator.rb +29 -0
- data/lib/pacto/core/configuration.rb +19 -17
- data/lib/pacto/core/contract_registry.rb +43 -0
- data/lib/pacto/core/{callback.rb → hook.rb} +3 -3
- data/lib/pacto/core/modes.rb +33 -0
- data/lib/pacto/core/validation_registry.rb +45 -0
- data/lib/pacto/erb_processor.rb +0 -1
- data/lib/pacto/extensions.rb +18 -4
- data/lib/pacto/generator.rb +34 -49
- data/lib/pacto/generator/filters.rb +41 -0
- data/lib/pacto/hooks/erb_hook.rb +4 -3
- data/lib/pacto/logger.rb +4 -2
- data/lib/pacto/meta_schema.rb +4 -2
- data/lib/pacto/rake_task.rb +28 -25
- data/lib/pacto/request_clause.rb +43 -0
- data/lib/pacto/request_pattern.rb +8 -0
- data/lib/pacto/response_clause.rb +15 -0
- data/lib/pacto/rspec.rb +102 -0
- data/lib/pacto/stubs/uri_pattern.rb +23 -0
- data/lib/pacto/stubs/webmock_adapter.rb +69 -0
- data/lib/pacto/stubs/webmock_helper.rb +71 -0
- data/lib/pacto/ui.rb +7 -0
- data/lib/pacto/uri.rb +9 -0
- data/lib/pacto/validation.rb +57 -0
- data/lib/pacto/validators/body_validator.rb +41 -0
- data/lib/pacto/validators/request_body_validator.rb +23 -0
- data/lib/pacto/validators/response_body_validator.rb +23 -0
- data/lib/pacto/validators/response_header_validator.rb +49 -0
- data/lib/pacto/validators/response_status_validator.rb +24 -0
- data/lib/pacto/version.rb +1 -1
- data/pacto.gemspec +33 -29
- data/resources/contract_schema.json +8 -176
- data/resources/draft-03.json +174 -0
- data/spec/integration/data/strict_contract.json +2 -2
- data/spec/integration/e2e_spec.rb +22 -31
- data/spec/integration/rspec_spec.rb +94 -0
- data/spec/integration/templating_spec.rb +9 -12
- data/{lib → spec}/pacto/server.rb +0 -0
- data/{lib → spec}/pacto/server/dummy.rb +11 -8
- data/{lib → spec}/pacto/server/playback_servlet.rb +1 -1
- data/spec/spec_helper.rb +2 -0
- data/spec/unit/hooks/erb_hook_spec.rb +15 -15
- data/spec/unit/pacto/configuration_spec.rb +2 -10
- data/spec/unit/pacto/contract_factory_spec.rb +16 -13
- data/spec/unit/pacto/contract_files_spec.rb +42 -0
- data/spec/unit/pacto/contract_list_spec.rb +35 -0
- data/spec/unit/pacto/contract_spec.rb +43 -44
- data/spec/unit/pacto/contract_validator_spec.rb +85 -0
- data/spec/unit/pacto/core/configuration_spec.rb +4 -11
- data/spec/unit/pacto/core/contract_registry_spec.rb +119 -0
- data/spec/unit/pacto/core/modes_spec.rb +18 -0
- data/spec/unit/pacto/core/validation_registry_spec.rb +76 -0
- data/spec/unit/pacto/core/validation_spec.rb +60 -0
- data/spec/unit/pacto/extensions_spec.rb +14 -23
- data/spec/unit/pacto/generator/filters_spec.rb +99 -0
- data/spec/unit/pacto/generator_spec.rb +34 -73
- data/spec/unit/pacto/meta_schema_spec.rb +46 -6
- data/spec/unit/pacto/pacto_spec.rb +17 -15
- data/spec/unit/pacto/{request_spec.rb → request_clause_spec.rb} +32 -44
- data/spec/unit/pacto/request_pattern_spec.rb +22 -0
- data/spec/unit/pacto/response_clause_spec.rb +54 -0
- data/spec/unit/pacto/stubs/uri_pattern_spec.rb +28 -0
- data/spec/unit/pacto/stubs/webmock_adapter_spec.rb +205 -0
- data/spec/unit/pacto/stubs/webmock_helper_spec.rb +20 -0
- data/spec/unit/pacto/uri_spec.rb +20 -0
- data/spec/unit/pacto/validators/body_validator_spec.rb +105 -0
- data/spec/unit/pacto/validators/response_header_validator_spec.rb +94 -0
- data/spec/unit/pacto/validators/response_status_validator_spec.rb +20 -0
- metadata +230 -146
- data/features/generation/generation.feature +0 -25
- data/lib/pacto/core/contract_repository.rb +0 -44
- data/lib/pacto/hash_merge_processor.rb +0 -14
- data/lib/pacto/request.rb +0 -57
- data/lib/pacto/response.rb +0 -63
- data/lib/pacto/response_adapter.rb +0 -24
- data/lib/pacto/stubs/built_in.rb +0 -57
- data/spec/unit/pacto/core/contract_repository_spec.rb +0 -133
- data/spec/unit/pacto/hash_merge_processor_spec.rb +0 -20
- data/spec/unit/pacto/response_adapter_spec.rb +0 -25
- data/spec/unit/pacto/response_spec.rb +0 -201
- data/spec/unit/pacto/stubs/built_in_spec.rb +0 -168
@@ -1,25 +0,0 @@
|
|
1
|
-
@needs_server
|
2
|
-
Feature: Contract Generation
|
3
|
-
Scenario: Generating a contract from a partial contract
|
4
|
-
Given a directory named "contracts"
|
5
|
-
Given a file named "requests/my_contract.json" with:
|
6
|
-
"""
|
7
|
-
{
|
8
|
-
"request": {
|
9
|
-
"method": "GET",
|
10
|
-
"path": "/hello",
|
11
|
-
"headers": {
|
12
|
-
"Accept": "application/json"
|
13
|
-
},
|
14
|
-
"params": {}
|
15
|
-
},
|
16
|
-
"response": {
|
17
|
-
"status": 200,
|
18
|
-
"body": {
|
19
|
-
"required": true
|
20
|
-
}
|
21
|
-
}
|
22
|
-
}
|
23
|
-
"""
|
24
|
-
When I successfully run `bundle exec rake --trace pacto:generate['tmp/aruba/requests','tmp/aruba/contracts','http://localhost:8000']`
|
25
|
-
Then the output should contain "Successfully generated all contracts"
|
@@ -1,44 +0,0 @@
|
|
1
|
-
module Pacto
|
2
|
-
class << self
|
3
|
-
|
4
|
-
def register_contract(contract = nil, *tags)
|
5
|
-
tags << :default if tags.empty?
|
6
|
-
start_count = registered.count
|
7
|
-
tags.uniq.each do |tag|
|
8
|
-
registered[tag] << contract
|
9
|
-
end
|
10
|
-
registered.count - start_count
|
11
|
-
end
|
12
|
-
|
13
|
-
def use(tag, values = {})
|
14
|
-
merged_contracts = registered[:default] + registered[tag]
|
15
|
-
|
16
|
-
raise ArgumentError, "contract \"#{tag}\" not found" if merged_contracts.empty?
|
17
|
-
|
18
|
-
merged_contracts.each do |contract|
|
19
|
-
contract.stub_contract! values
|
20
|
-
end
|
21
|
-
merged_contracts.count
|
22
|
-
end
|
23
|
-
|
24
|
-
def registered
|
25
|
-
@registered ||= Hash.new { |hash, key| hash[key] = Set.new }
|
26
|
-
end
|
27
|
-
|
28
|
-
def unregister_all!
|
29
|
-
registered.clear
|
30
|
-
end
|
31
|
-
|
32
|
-
def contract_for(request_signature)
|
33
|
-
matches = Set.new
|
34
|
-
registered.values.each do |contract_set|
|
35
|
-
contract_set.each do |contract|
|
36
|
-
if contract.matches? request_signature
|
37
|
-
matches.add contract
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
matches
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
module Pacto
|
2
|
-
class HashMergeProcessor
|
3
|
-
def process(response_body, values = {})
|
4
|
-
unless values.nil? || values.empty?
|
5
|
-
if response_body.respond_to?(:normalize_keys)
|
6
|
-
response_body = response_body.normalize_keys.deep_merge(values.normalize_keys)
|
7
|
-
else
|
8
|
-
response_body = values
|
9
|
-
end
|
10
|
-
end
|
11
|
-
response_body.to_s
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
data/lib/pacto/request.rb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
module Pacto
|
2
|
-
class Request
|
3
|
-
attr_reader :host
|
4
|
-
|
5
|
-
def initialize(host, definition)
|
6
|
-
@host = host
|
7
|
-
@definition = definition
|
8
|
-
end
|
9
|
-
|
10
|
-
def method
|
11
|
-
@definition['method'].to_s.downcase.to_sym
|
12
|
-
end
|
13
|
-
|
14
|
-
def path
|
15
|
-
@definition['path']
|
16
|
-
end
|
17
|
-
|
18
|
-
def headers
|
19
|
-
@definition['headers']
|
20
|
-
end
|
21
|
-
|
22
|
-
def params
|
23
|
-
@definition['params']
|
24
|
-
end
|
25
|
-
|
26
|
-
def absolute_uri
|
27
|
-
@host + path
|
28
|
-
end
|
29
|
-
|
30
|
-
def full_uri
|
31
|
-
return absolute_uri if params.empty?
|
32
|
-
|
33
|
-
uri = Addressable::URI.new
|
34
|
-
uri.query_values = params
|
35
|
-
|
36
|
-
absolute_uri + '?' + uri.query
|
37
|
-
end
|
38
|
-
|
39
|
-
def execute
|
40
|
-
response = HTTParty.send(method, @host + path, {
|
41
|
-
httparty_params_key => normalized_params,
|
42
|
-
:headers => headers
|
43
|
-
})
|
44
|
-
ResponseAdapter.new(response)
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
def httparty_params_key
|
50
|
-
method == :get ? :query : :body
|
51
|
-
end
|
52
|
-
|
53
|
-
def normalized_params
|
54
|
-
method == :get ? params : params.to_json
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
data/lib/pacto/response.rb
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
module Pacto
|
2
|
-
class Response
|
3
|
-
attr_reader :status, :headers, :schema
|
4
|
-
|
5
|
-
def initialize(definition)
|
6
|
-
@definition = definition
|
7
|
-
@status = @definition['status']
|
8
|
-
@headers = @definition['headers']
|
9
|
-
@schema = @definition['body']
|
10
|
-
end
|
11
|
-
|
12
|
-
def instantiate
|
13
|
-
OpenStruct.new({
|
14
|
-
'status' => @status,
|
15
|
-
'headers' => @headers,
|
16
|
-
'body' => JSON::Generator.generate(@schema)
|
17
|
-
})
|
18
|
-
end
|
19
|
-
|
20
|
-
def validate(response, opt = {})
|
21
|
-
|
22
|
-
unless opt[:body_only]
|
23
|
-
if @definition['status'] != response.status
|
24
|
-
return ["Invalid status: expected #{@definition['status']} but got #{response.status}"]
|
25
|
-
end
|
26
|
-
|
27
|
-
unless @definition['headers'].normalize_keys.subset_of?(response.headers.normalize_keys)
|
28
|
-
return ["Invalid headers: expected #{@definition['headers'].inspect} to be a subset of #{response.headers.inspect}"]
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
if @definition['body']
|
33
|
-
if @definition['body']['type'] && @definition['body']['type'] == 'string'
|
34
|
-
validate_as_pure_string response.body
|
35
|
-
else
|
36
|
-
response.respond_to?(:body) ? validate_as_json(response.body) : validate_as_json(response)
|
37
|
-
end
|
38
|
-
else
|
39
|
-
[]
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def validate_as_pure_string response_body
|
46
|
-
errors = []
|
47
|
-
if @definition['body']['required'] && response_body.nil?
|
48
|
-
errors << 'The response does not contain a body'
|
49
|
-
end
|
50
|
-
|
51
|
-
pattern = @definition['body']['pattern']
|
52
|
-
if pattern && !(response_body =~ Regexp.new(pattern))
|
53
|
-
errors << "The response does not match the pattern #{pattern}"
|
54
|
-
end
|
55
|
-
|
56
|
-
errors
|
57
|
-
end
|
58
|
-
|
59
|
-
def validate_as_json response_body
|
60
|
-
JSON::Validator.fully_validate(@definition['body'], response_body, :version => :draft3)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
@@ -1,24 +0,0 @@
|
|
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
|
data/lib/pacto/stubs/built_in.rb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
module Pacto
|
2
|
-
module Stubs
|
3
|
-
class BuiltIn
|
4
|
-
|
5
|
-
def initialize
|
6
|
-
register_callbacks
|
7
|
-
end
|
8
|
-
|
9
|
-
def stub_request! request, response
|
10
|
-
stub = WebMock.stub_request(request.method, "#{request.host}#{request.path}")
|
11
|
-
stub = stub.with(request_details(request)) if Pacto.configuration.strict_matchers
|
12
|
-
stub.to_return({
|
13
|
-
:status => response.status,
|
14
|
-
:headers => response.headers,
|
15
|
-
:body => format_body(response.body)
|
16
|
-
})
|
17
|
-
end
|
18
|
-
|
19
|
-
def reset!
|
20
|
-
WebMock.reset!
|
21
|
-
WebMock.reset_callbacks
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def register_callbacks
|
27
|
-
WebMock.after_request do |request_signature, response|
|
28
|
-
contracts = Pacto.contract_for request_signature
|
29
|
-
Pacto.configuration.callback.process contracts, request_signature, response
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def format_body(body)
|
34
|
-
if body.is_a?(Hash) || body.is_a?(Array)
|
35
|
-
body.to_json
|
36
|
-
else
|
37
|
-
body
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def request_details request
|
42
|
-
details = {}
|
43
|
-
unless request.params.empty?
|
44
|
-
details[webmock_params_key(request)] = request.params
|
45
|
-
end
|
46
|
-
unless request.headers.empty?
|
47
|
-
details[:headers] = request.headers
|
48
|
-
end
|
49
|
-
details
|
50
|
-
end
|
51
|
-
|
52
|
-
def webmock_params_key request
|
53
|
-
request.method == :get ? :query : :body
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
@@ -1,133 +0,0 @@
|
|
1
|
-
describe Pacto do
|
2
|
-
let(:tag) { 'contract_tag' }
|
3
|
-
let(:another_tag) { 'another_tag' }
|
4
|
-
let(:contract) { double('contract') }
|
5
|
-
let(:another_contract) { double('another_contract') }
|
6
|
-
|
7
|
-
after do
|
8
|
-
described_class.unregister_all!
|
9
|
-
end
|
10
|
-
|
11
|
-
describe '.register' do
|
12
|
-
context 'no tag' do
|
13
|
-
it 'registers the contract with the default tag' do
|
14
|
-
described_class.register_contract contract
|
15
|
-
expect(described_class.registered[:default]).to include(contract)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
context 'one tag' do
|
20
|
-
it 'registers a contract under a given tag' do
|
21
|
-
described_class.register_contract(contract, tag)
|
22
|
-
expect(described_class.registered[tag]).to include(contract)
|
23
|
-
end
|
24
|
-
|
25
|
-
it 'does not duplicate a contract when it has already been registered with the same tag' do
|
26
|
-
described_class.register_contract(contract, tag)
|
27
|
-
described_class.register_contract(contract, tag)
|
28
|
-
expect(described_class.registered[tag]).to include(contract)
|
29
|
-
expect(described_class.registered[tag]).to have(1).items
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
context 'multiple tags' do
|
34
|
-
it 'registers a contract using different tags' do
|
35
|
-
described_class.register_contract(contract, tag, another_tag)
|
36
|
-
expect(described_class.registered[tag]).to include(contract)
|
37
|
-
expect(described_class.registered[another_tag]).to include(contract)
|
38
|
-
end
|
39
|
-
|
40
|
-
it 'registers a tag with different contracts ' do
|
41
|
-
described_class.register_contract(contract, tag)
|
42
|
-
described_class.register_contract(another_contract, tag)
|
43
|
-
expect(described_class.registered[tag]).to include(contract, another_contract)
|
44
|
-
end
|
45
|
-
|
46
|
-
end
|
47
|
-
|
48
|
-
context 'with a block' do
|
49
|
-
it 'has a compact syntax for registering multiple contracts' do
|
50
|
-
described_class.configure do |c|
|
51
|
-
c.register_contract 'new_api/create_item_v2', :item, :new
|
52
|
-
c.register_contract 'authentication', :default
|
53
|
-
c.register_contract 'list_items_legacy', :legacy
|
54
|
-
c.register_contract 'get_item_legacy', :legacy
|
55
|
-
end
|
56
|
-
expect(described_class.registered[:new]).to include('new_api/create_item_v2')
|
57
|
-
expect(described_class.registered[:default]).to include('authentication')
|
58
|
-
expect(described_class.registered[:legacy]).to include('list_items_legacy', 'get_item_legacy')
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
describe '.use' do
|
64
|
-
before do
|
65
|
-
described_class.register_contract(contract, tag)
|
66
|
-
described_class.register_contract(another_contract, :default)
|
67
|
-
end
|
68
|
-
|
69
|
-
context 'when a contract has been registered' do
|
70
|
-
let(:response_body) { double('response_body') }
|
71
|
-
|
72
|
-
it 'stubs a contract with default values' do
|
73
|
-
contract.should_receive(:stub_contract!)
|
74
|
-
another_contract.should_receive(:stub_contract!)
|
75
|
-
expect(described_class.use(tag)).to eq 2
|
76
|
-
end
|
77
|
-
|
78
|
-
it 'stubs default contract if unused tag' do
|
79
|
-
another_contract.should_receive(:stub_contract!)
|
80
|
-
expect(described_class.use(another_tag)).to eq 1
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
context 'when contract has not been registered' do
|
85
|
-
it 'raises an argument error' do
|
86
|
-
described_class.unregister_all!
|
87
|
-
expect { described_class.use('unregistered') }.to raise_error ArgumentError
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
describe '.unregister_all!' do
|
93
|
-
it 'unregisters all previously registered contracts' do
|
94
|
-
described_class.register_contract(contract, tag)
|
95
|
-
described_class.unregister_all!
|
96
|
-
expect(described_class.registered).to be_empty
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
describe '.contract_for' do
|
101
|
-
let(:request_signature) { double('request signature') }
|
102
|
-
|
103
|
-
context 'when no contracts are found for a request' do
|
104
|
-
it 'returns an empty list' do
|
105
|
-
expect(described_class.contract_for request_signature).to be_empty
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
context 'when contracts are found for a request' do
|
110
|
-
let(:contracts_that_match) { create_contracts 2, true }
|
111
|
-
let(:contracts_that_dont_match) { create_contracts 3, false }
|
112
|
-
let(:all_contracts) { contracts_that_match + contracts_that_dont_match }
|
113
|
-
|
114
|
-
it 'returns the matching contracts' do
|
115
|
-
register_and_use all_contracts
|
116
|
-
expect(described_class.contract_for request_signature).to eq(contracts_that_match)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def create_contracts(total, matches)
|
122
|
-
total.times.map do
|
123
|
-
double('contract',
|
124
|
-
:stub_contract! => double('request matcher'),
|
125
|
-
:matches? => matches)
|
126
|
-
end.to_set
|
127
|
-
end
|
128
|
-
|
129
|
-
def register_and_use contracts
|
130
|
-
contracts.each { |contract| described_class.register_contract contract }
|
131
|
-
Pacto.use :default
|
132
|
-
end
|
133
|
-
end
|
@@ -1,20 +0,0 @@
|
|
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
|