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.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rubocop-todo.yml +0 -27
  4. data/.rubocop.yml +9 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +4 -5
  8. data/CONTRIBUTING.md +112 -0
  9. data/Gemfile +5 -0
  10. data/Guardfile +18 -13
  11. data/README.md +157 -101
  12. data/Rakefile +3 -3
  13. data/features/configuration/strict_matchers.feature +97 -0
  14. data/features/evolve/README.md +11 -0
  15. data/features/evolve/existing_services.feature +82 -0
  16. data/features/generate/README.md +5 -0
  17. data/features/generate/generation.feature +28 -0
  18. data/features/steps/pacto_steps.rb +75 -0
  19. data/features/stub/README.md +2 -0
  20. data/features/stub/templates.feature +46 -0
  21. data/features/support/env.rb +11 -5
  22. data/features/validate/README.md +1 -0
  23. data/features/validate/body_only.feature +85 -0
  24. data/features/{journeys/validation.feature → validate/meta_validation.feature} +41 -24
  25. data/features/validate/validation.feature +36 -0
  26. data/lib/pacto.rb +61 -33
  27. data/lib/pacto/contract.rb +18 -15
  28. data/lib/pacto/contract_factory.rb +14 -11
  29. data/lib/pacto/contract_files.rb +17 -0
  30. data/lib/pacto/contract_list.rb +17 -0
  31. data/lib/pacto/contract_validator.rb +29 -0
  32. data/lib/pacto/core/configuration.rb +19 -17
  33. data/lib/pacto/core/contract_registry.rb +43 -0
  34. data/lib/pacto/core/{callback.rb → hook.rb} +3 -3
  35. data/lib/pacto/core/modes.rb +33 -0
  36. data/lib/pacto/core/validation_registry.rb +45 -0
  37. data/lib/pacto/erb_processor.rb +0 -1
  38. data/lib/pacto/extensions.rb +18 -4
  39. data/lib/pacto/generator.rb +34 -49
  40. data/lib/pacto/generator/filters.rb +41 -0
  41. data/lib/pacto/hooks/erb_hook.rb +4 -3
  42. data/lib/pacto/logger.rb +4 -2
  43. data/lib/pacto/meta_schema.rb +4 -2
  44. data/lib/pacto/rake_task.rb +28 -25
  45. data/lib/pacto/request_clause.rb +43 -0
  46. data/lib/pacto/request_pattern.rb +8 -0
  47. data/lib/pacto/response_clause.rb +15 -0
  48. data/lib/pacto/rspec.rb +102 -0
  49. data/lib/pacto/stubs/uri_pattern.rb +23 -0
  50. data/lib/pacto/stubs/webmock_adapter.rb +69 -0
  51. data/lib/pacto/stubs/webmock_helper.rb +71 -0
  52. data/lib/pacto/ui.rb +7 -0
  53. data/lib/pacto/uri.rb +9 -0
  54. data/lib/pacto/validation.rb +57 -0
  55. data/lib/pacto/validators/body_validator.rb +41 -0
  56. data/lib/pacto/validators/request_body_validator.rb +23 -0
  57. data/lib/pacto/validators/response_body_validator.rb +23 -0
  58. data/lib/pacto/validators/response_header_validator.rb +49 -0
  59. data/lib/pacto/validators/response_status_validator.rb +24 -0
  60. data/lib/pacto/version.rb +1 -1
  61. data/pacto.gemspec +33 -29
  62. data/resources/contract_schema.json +8 -176
  63. data/resources/draft-03.json +174 -0
  64. data/spec/integration/data/strict_contract.json +2 -2
  65. data/spec/integration/e2e_spec.rb +22 -31
  66. data/spec/integration/rspec_spec.rb +94 -0
  67. data/spec/integration/templating_spec.rb +9 -12
  68. data/{lib → spec}/pacto/server.rb +0 -0
  69. data/{lib → spec}/pacto/server/dummy.rb +11 -8
  70. data/{lib → spec}/pacto/server/playback_servlet.rb +1 -1
  71. data/spec/spec_helper.rb +2 -0
  72. data/spec/unit/hooks/erb_hook_spec.rb +15 -15
  73. data/spec/unit/pacto/configuration_spec.rb +2 -10
  74. data/spec/unit/pacto/contract_factory_spec.rb +16 -13
  75. data/spec/unit/pacto/contract_files_spec.rb +42 -0
  76. data/spec/unit/pacto/contract_list_spec.rb +35 -0
  77. data/spec/unit/pacto/contract_spec.rb +43 -44
  78. data/spec/unit/pacto/contract_validator_spec.rb +85 -0
  79. data/spec/unit/pacto/core/configuration_spec.rb +4 -11
  80. data/spec/unit/pacto/core/contract_registry_spec.rb +119 -0
  81. data/spec/unit/pacto/core/modes_spec.rb +18 -0
  82. data/spec/unit/pacto/core/validation_registry_spec.rb +76 -0
  83. data/spec/unit/pacto/core/validation_spec.rb +60 -0
  84. data/spec/unit/pacto/extensions_spec.rb +14 -23
  85. data/spec/unit/pacto/generator/filters_spec.rb +99 -0
  86. data/spec/unit/pacto/generator_spec.rb +34 -73
  87. data/spec/unit/pacto/meta_schema_spec.rb +46 -6
  88. data/spec/unit/pacto/pacto_spec.rb +17 -15
  89. data/spec/unit/pacto/{request_spec.rb → request_clause_spec.rb} +32 -44
  90. data/spec/unit/pacto/request_pattern_spec.rb +22 -0
  91. data/spec/unit/pacto/response_clause_spec.rb +54 -0
  92. data/spec/unit/pacto/stubs/uri_pattern_spec.rb +28 -0
  93. data/spec/unit/pacto/stubs/webmock_adapter_spec.rb +205 -0
  94. data/spec/unit/pacto/stubs/webmock_helper_spec.rb +20 -0
  95. data/spec/unit/pacto/uri_spec.rb +20 -0
  96. data/spec/unit/pacto/validators/body_validator_spec.rb +105 -0
  97. data/spec/unit/pacto/validators/response_header_validator_spec.rb +94 -0
  98. data/spec/unit/pacto/validators/response_status_validator_spec.rb +20 -0
  99. metadata +230 -146
  100. data/features/generation/generation.feature +0 -25
  101. data/lib/pacto/core/contract_repository.rb +0 -44
  102. data/lib/pacto/hash_merge_processor.rb +0 -14
  103. data/lib/pacto/request.rb +0 -57
  104. data/lib/pacto/response.rb +0 -63
  105. data/lib/pacto/response_adapter.rb +0 -24
  106. data/lib/pacto/stubs/built_in.rb +0 -57
  107. data/spec/unit/pacto/core/contract_repository_spec.rb +0 -133
  108. data/spec/unit/pacto/hash_merge_processor_spec.rb +0 -20
  109. data/spec/unit/pacto/response_adapter_spec.rb +0 -25
  110. data/spec/unit/pacto/response_spec.rb +0 -201
  111. 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,8 @@
1
+ module Pacto
2
+ class RequestPattern
3
+ def self.for(base_request)
4
+ uri_pattern = UriPattern.for(base_request)
5
+ WebMock::RequestPattern.new(base_request.method, uri_pattern)
6
+ end
7
+ end
8
+ 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
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'term/ansicolor'
2
+
3
+ module Pacto
4
+ module UI
5
+ extend Term::ANSIColor
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module Pacto
2
+ class URI
3
+ def self.for(host, path, params = {})
4
+ Addressable::URI.heuristic_parse("#{host}#{path}").tap do |uri|
5
+ uri.query_values = params unless params.empty?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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