pacto 0.3.0.pre → 0.3.0

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.
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