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
@@ -0,0 +1,43 @@
|
|
1
|
+
module Pacto
|
2
|
+
class RequestClause
|
3
|
+
attr_reader :host, :method, :schema
|
4
|
+
attr_accessor :body
|
5
|
+
|
6
|
+
def initialize(host, definition)
|
7
|
+
@host = host
|
8
|
+
@definition = definition
|
9
|
+
@method = definition['method'].to_s.downcase.to_sym
|
10
|
+
@schema = definition['body'] || {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def uri
|
14
|
+
@uri ||= Pacto::URI.for(host, path, params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def body
|
18
|
+
JSON::Generator.generate(@definition['body']) if @definition['body']
|
19
|
+
end
|
20
|
+
|
21
|
+
def path
|
22
|
+
@definition['path']
|
23
|
+
end
|
24
|
+
|
25
|
+
def headers
|
26
|
+
@definition['headers']
|
27
|
+
end
|
28
|
+
|
29
|
+
def params
|
30
|
+
@definition['params'] || {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def execute
|
34
|
+
conn = Faraday.new(:url => uri.to_s) do |faraday|
|
35
|
+
faraday.response :logger if Pacto.configuration.logger.level == :debug
|
36
|
+
faraday.adapter Faraday.default_adapter
|
37
|
+
end
|
38
|
+
conn.send(method) do |req|
|
39
|
+
req.headers = headers
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Pacto
|
2
|
+
class ResponseClause
|
3
|
+
attr_reader :status, :headers, :schema
|
4
|
+
|
5
|
+
def initialize(definition)
|
6
|
+
@status = definition['status']
|
7
|
+
@headers = definition['headers']
|
8
|
+
@schema = definition['body'] || {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def body
|
12
|
+
@body ||= JSON::Generator.generate(schema)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/pacto/rspec.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'pacto'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rspec/core'
|
5
|
+
require 'rspec/expectations'
|
6
|
+
rescue LoadError
|
7
|
+
raise 'pacto/rspec requires rspec 2 or later'
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec::Matchers.define :have_unmatched_requests do |method, uri|
|
11
|
+
@unmatched_validations = Pacto::ValidationRegistry.instance.unmatched_validations
|
12
|
+
match do
|
13
|
+
!@unmatched_validations.empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
failure_message_for_should do
|
17
|
+
'Expected Pacto to have not matched all requests to a Contract, but all requests were matched.'
|
18
|
+
end
|
19
|
+
|
20
|
+
failure_message_for_should_not do
|
21
|
+
unmatched_requests = @unmatched_validations.map(&:request).join("\n ")
|
22
|
+
"Expected Pacto to have matched all requests to a Contract, but the following requests were not matched: \n #{unmatched_requests}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
RSpec::Matchers.define :have_failed_validations do |method, uri|
|
27
|
+
@failed_validations = Pacto::ValidationRegistry.instance.failed_validations
|
28
|
+
match do
|
29
|
+
!@failed_validations.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
failure_message_for_should do
|
33
|
+
'Expected Pacto to have found validation problems, but none were found.'
|
34
|
+
end
|
35
|
+
|
36
|
+
failure_message_for_should_not do
|
37
|
+
"Expected Pacto to have successfully validated all requests, but the following issues were found: #{@failed_validations}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
RSpec::Matchers.define :have_validated do |method, uri|
|
42
|
+
@request_pattern = WebMock::RequestPattern.new(method, uri)
|
43
|
+
match do
|
44
|
+
validated? @request_pattern
|
45
|
+
end
|
46
|
+
|
47
|
+
chain :against_contract do |contract|
|
48
|
+
@contract = contract
|
49
|
+
end
|
50
|
+
|
51
|
+
chain :with do |options|
|
52
|
+
@request_pattern.with options
|
53
|
+
end
|
54
|
+
|
55
|
+
def validated?(request_pattern)
|
56
|
+
@matching_validations = Pacto::ValidationRegistry.instance.validated? @request_pattern
|
57
|
+
validated = !@matching_validations.nil?
|
58
|
+
validated && successfully? && contract_matches?
|
59
|
+
end
|
60
|
+
|
61
|
+
def validation_results
|
62
|
+
@validation_results ||= @matching_validations.map(&:results).flatten.compact
|
63
|
+
end
|
64
|
+
|
65
|
+
def successfully?
|
66
|
+
@matching_validations.map(&:successful?).uniq.eql? [true]
|
67
|
+
end
|
68
|
+
|
69
|
+
def contract_matches?
|
70
|
+
if @contract
|
71
|
+
validated_contracts = @matching_validations.map(&:contract)
|
72
|
+
# Is there a better option than case equality for string & regex support?
|
73
|
+
validated_contracts.map(&:file).index { |file| @contract === file } # rubocop:disable CaseEquality
|
74
|
+
else
|
75
|
+
true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
failure_message_for_should do
|
80
|
+
buffer = StringIO.new
|
81
|
+
buffer.puts "expected Pacto to have validated #{@request_pattern}"
|
82
|
+
if @matching_validations.nil? || @matching_validations.empty?
|
83
|
+
buffer.puts ' but no matching request was received'
|
84
|
+
buffer.puts ' received:'
|
85
|
+
buffer.puts "#{WebMock::RequestRegistry.instance}"
|
86
|
+
elsif @matching_validations.map(&:contract).compact.empty?
|
87
|
+
buffer.puts ' but a matching Contract was not found'
|
88
|
+
elsif !successfully?
|
89
|
+
buffer.puts ' but validation errors were found:'
|
90
|
+
buffer.print ' '
|
91
|
+
buffer.puts validation_results.join "\n "
|
92
|
+
# validation_results.each do |validation_result|
|
93
|
+
# buffer.puts " #{validation_result}"
|
94
|
+
# end
|
95
|
+
elsif @contract
|
96
|
+
validated_against = @matching_validations.map { |v| v.against_contract? @contract }.compact.join ','
|
97
|
+
buffer.puts " against Contract #{@contract}"
|
98
|
+
buffer.puts " but it was validated against #{validated_against}"
|
99
|
+
end
|
100
|
+
buffer.string
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Pacto
|
2
|
+
class UriPattern
|
3
|
+
class << self
|
4
|
+
def for(request)
|
5
|
+
if Pacto.configuration.strict_matchers
|
6
|
+
build_strict_uri_pattern(request)
|
7
|
+
else
|
8
|
+
build_relaxed_uri_pattern(request)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def build_strict_uri_pattern(request)
|
13
|
+
"#{request.host}#{request.path}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_relaxed_uri_pattern(request)
|
17
|
+
path_pattern = request.path.gsub(/\/:\w+/, '/[^\/\?#]+')
|
18
|
+
host_pattern = Regexp.quote(request.host)
|
19
|
+
/#{host_pattern}#{path_pattern}/
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'pacto/stubs/webmock_helper'
|
2
|
+
|
3
|
+
module Pacto
|
4
|
+
module Stubs
|
5
|
+
class WebMockAdapter
|
6
|
+
def initialize
|
7
|
+
register_hooks
|
8
|
+
end
|
9
|
+
|
10
|
+
def stub_request!(request, response)
|
11
|
+
uri_pattern = UriPattern.for(request)
|
12
|
+
stub = WebMock.stub_request(request.method, uri_pattern)
|
13
|
+
|
14
|
+
stub.request_pattern.with(strict_details(request)) if Pacto.configuration.strict_matchers
|
15
|
+
|
16
|
+
stub.to_return(
|
17
|
+
:status => response.status,
|
18
|
+
:headers => response.headers,
|
19
|
+
:body => format_body(response.body)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def reset!
|
24
|
+
WebMock.reset!
|
25
|
+
WebMock.reset_callbacks
|
26
|
+
end
|
27
|
+
|
28
|
+
def process_hooks(request_signature, response)
|
29
|
+
WebMockHelper.generate(request_signature, response) if Pacto.generating?
|
30
|
+
|
31
|
+
contracts = Pacto.contracts_for request_signature
|
32
|
+
Pacto.configuration.hook.process contracts, request_signature, response
|
33
|
+
|
34
|
+
WebMockHelper.validate(request_signature, response) if Pacto.validating?
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def register_hooks
|
40
|
+
WebMock.after_request do |request_signature, response|
|
41
|
+
process_hooks request_signature, response
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def format_body(body)
|
46
|
+
if body.is_a?(Hash) || body.is_a?(Array)
|
47
|
+
body.to_json
|
48
|
+
else
|
49
|
+
body
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def strict_details(request)
|
54
|
+
{}.tap do |details|
|
55
|
+
unless request.params.empty?
|
56
|
+
details[webmock_params_key(request)] = request.params
|
57
|
+
end
|
58
|
+
unless request.headers.empty?
|
59
|
+
details[:headers] = request.headers
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def webmock_params_key(request)
|
65
|
+
request.method == :get ? :query : :body
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Pacto
|
2
|
+
module Stubs
|
3
|
+
class WebMockHelper
|
4
|
+
class << self
|
5
|
+
def validate(request_signature, response)
|
6
|
+
pacto_response = webmock_to_pacto_response(response)
|
7
|
+
contract = Pacto.contracts_for(request_signature).first
|
8
|
+
validation = Validation.new request_signature, pacto_response, contract
|
9
|
+
Pacto::ValidationRegistry.instance.register_validation validation
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate(request_signature, response)
|
13
|
+
logger.debug("Generating Contract for #{request_signature}, #{response}")
|
14
|
+
begin
|
15
|
+
contract_file = load_contract_file(request_signature)
|
16
|
+
|
17
|
+
unless File.exists? contract_file
|
18
|
+
generate_contract(request_signature, response, contract_file)
|
19
|
+
end
|
20
|
+
rescue => e
|
21
|
+
logger.error("Error while generating Contract #{contract_file}: #{e.message}")
|
22
|
+
logger.error("Backtrace: #{e.backtrace}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def generate_contract(request_signature, response, contract_file)
|
29
|
+
uri = URI(request_signature.uri)
|
30
|
+
pacto_request = webmock_to_pacto_request(request_signature)
|
31
|
+
pacto_response = webmock_to_pacto_response(response)
|
32
|
+
generator = Pacto::Generator.new
|
33
|
+
FileUtils.mkdir_p(File.dirname contract_file)
|
34
|
+
File.write(contract_file, generator.save(uri, pacto_request, pacto_response))
|
35
|
+
logger.debug("Generating #{contract_file}")
|
36
|
+
|
37
|
+
Pacto.load_contract contract_file, uri.host
|
38
|
+
end
|
39
|
+
|
40
|
+
def load_contract_file(request_signature)
|
41
|
+
uri = URI(request_signature.uri)
|
42
|
+
basename = File.basename(uri.path, '.json') + '.json'
|
43
|
+
File.join(Pacto.configuration.contracts_path, uri.host, File.dirname(uri.path), basename)
|
44
|
+
end
|
45
|
+
|
46
|
+
def logger
|
47
|
+
@logger ||= Logger.instance
|
48
|
+
end
|
49
|
+
|
50
|
+
def webmock_to_pacto_request(webmock_request)
|
51
|
+
uri = webmock_request.uri
|
52
|
+
Faraday::Request.create webmock_request.method do |req|
|
53
|
+
req.path = uri.path
|
54
|
+
req.params = uri.query_values
|
55
|
+
req.headers = webmock_request.headers
|
56
|
+
req.body = webmock_request.body
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def webmock_to_pacto_response(webmock_response)
|
61
|
+
status, _description = webmock_response.status
|
62
|
+
Faraday::Response.new(
|
63
|
+
:status => status,
|
64
|
+
:response_headers => webmock_response.headers || {},
|
65
|
+
:body => webmock_response.body
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/pacto/ui.rb
ADDED
data/lib/pacto/uri.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Pacto
|
2
|
+
class Validation
|
3
|
+
attr_reader :request, :response, :contract, :results
|
4
|
+
|
5
|
+
def initialize(request, response, contract)
|
6
|
+
@request = request
|
7
|
+
@response = response
|
8
|
+
@contract = contract
|
9
|
+
validate unless contract.nil?
|
10
|
+
end
|
11
|
+
|
12
|
+
def successful?
|
13
|
+
@results.nil? || @results.empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
def against_contract?(contract_pattern)
|
17
|
+
unless @contract.nil?
|
18
|
+
case contract_pattern
|
19
|
+
when String
|
20
|
+
@contract if @contract.file.eql? contract_pattern
|
21
|
+
when Regexp
|
22
|
+
@contract if @contract.file =~ contract_pattern
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
contract_name = @contract.nil? ? 'nil' : contract.name
|
29
|
+
"""
|
30
|
+
Validation:
|
31
|
+
\tRequest: #{@request}
|
32
|
+
\tContract: #{contract_name}
|
33
|
+
\tResults: \n\t\t#{@results.join "\n\t\t"}
|
34
|
+
"""
|
35
|
+
end
|
36
|
+
|
37
|
+
def summary
|
38
|
+
if @contract.nil?
|
39
|
+
"Missing contract for services provided by #{@request.uri.host}"
|
40
|
+
else
|
41
|
+
status = successful? ? 'successful' : 'unsuccessful'
|
42
|
+
"#{status} validation of #{@contract.name}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def logger
|
49
|
+
@logger ||= Logger.instance
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate
|
53
|
+
logger.debug("Validating #{@request}, #{@response} against #{@contract}")
|
54
|
+
@results = contract.validate_consumer(@request, @response)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Pacto
|
2
|
+
module Validators
|
3
|
+
class BodyValidator
|
4
|
+
def self.section_name
|
5
|
+
fail 'section name should be provided by subclass'
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.validate(schema, body)
|
9
|
+
if schema
|
10
|
+
if schema['type'] && schema['type'] == 'string'
|
11
|
+
validate_as_pure_string schema, body.body
|
12
|
+
else
|
13
|
+
body.respond_to?(:body) ? validate_as_json(schema, body.body) : validate_as_json(schema, body)
|
14
|
+
end
|
15
|
+
else
|
16
|
+
[]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.validate_as_pure_string(schema, body)
|
23
|
+
errors = []
|
24
|
+
if schema['required'] && body.nil?
|
25
|
+
errors << "The #{section_name} does not contain a body"
|
26
|
+
end
|
27
|
+
|
28
|
+
pattern = schema['pattern']
|
29
|
+
if pattern && !(body =~ Regexp.new(pattern))
|
30
|
+
errors << "The #{section_name} does not match the pattern #{pattern}"
|
31
|
+
end
|
32
|
+
|
33
|
+
errors
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.validate_as_json(schema, body)
|
37
|
+
JSON::Validator.fully_validate(schema, body, :version => :draft3)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|