pacto 0.4.0.rc1 → 0.4.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/Gemfile +2 -7
  4. data/Rakefile +0 -5
  5. data/appveyor.yml +12 -0
  6. data/features/configuration/strict_matchers.feature +4 -4
  7. data/features/generate/generation.feature +8 -10
  8. data/features/support/env.rb +3 -7
  9. data/features/validate/validation.feature +3 -3
  10. data/lib/pacto.rb +5 -4
  11. data/lib/pacto/actors/json_generator.rb +1 -1
  12. data/lib/pacto/body_parsing.rb +42 -0
  13. data/lib/pacto/consumer/faraday_driver.rb +5 -2
  14. data/lib/pacto/contract.rb +23 -24
  15. data/lib/pacto/contract_factory.rb +4 -4
  16. data/lib/pacto/core/configuration.rb +18 -10
  17. data/lib/pacto/core/http_middleware.rb +1 -1
  18. data/lib/pacto/core/pacto_request.rb +3 -15
  19. data/lib/pacto/core/pacto_response.rb +3 -13
  20. data/lib/pacto/errors.rb +68 -0
  21. data/lib/pacto/formats/legacy/contract.rb +49 -0
  22. data/lib/pacto/formats/legacy/contract_builder.rb +129 -0
  23. data/lib/pacto/formats/legacy/contract_factory.rb +63 -0
  24. data/lib/pacto/formats/legacy/contract_generator.rb +77 -0
  25. data/lib/pacto/formats/legacy/generator/filters.rb +46 -0
  26. data/lib/pacto/formats/legacy/generator_hint.rb +36 -0
  27. data/lib/pacto/formats/legacy/request_clause.rb +39 -0
  28. data/lib/pacto/formats/legacy/response_clause.rb +31 -0
  29. data/lib/pacto/formats/swagger/contract.rb +86 -0
  30. data/lib/pacto/formats/swagger/contract_factory.rb +45 -0
  31. data/lib/pacto/formats/swagger/request_clause.rb +53 -0
  32. data/lib/pacto/formats/swagger/response_clause.rb +31 -0
  33. data/lib/pacto/generator.rb +4 -4
  34. data/lib/pacto/handlers/json_handler.rb +19 -0
  35. data/lib/pacto/handlers/text_handler.rb +17 -0
  36. data/lib/pacto/request_clause.rb +9 -19
  37. data/lib/pacto/response_clause.rb +4 -4
  38. data/lib/pacto/server.rb +41 -2
  39. data/lib/pacto/stubs/uri_pattern.rb +5 -5
  40. data/lib/pacto/stubs/webmock_adapter.rb +4 -1
  41. data/lib/pacto/test_helper.rb +16 -13
  42. data/lib/pacto/version.rb +1 -1
  43. data/pacto-server.gemspec +1 -3
  44. data/pacto.gemspec +1 -1
  45. data/sample_apis/user_api.rb +16 -0
  46. data/samples/contracts/user.json +51 -0
  47. data/samples/cops.rb +3 -0
  48. data/samples/server_cli.sh +3 -3
  49. data/spec/fabricators/contract_fabricator.rb +17 -8
  50. data/spec/fixtures/{deprecated_contracts → contracts/deprecated}/deprecated_contract.json +2 -2
  51. data/spec/fixtures/contracts/{contract.json → legacy/contract.json} +0 -0
  52. data/spec/fixtures/contracts/{contract_with_examples.json → legacy/contract_with_examples.json} +0 -0
  53. data/spec/fixtures/contracts/{simple_contract.json → legacy/simple_contract.json} +1 -1
  54. data/spec/fixtures/contracts/{strict_contract.json → legacy/strict_contract.json} +0 -0
  55. data/spec/fixtures/contracts/{templating_contract.json → legacy/templating_contract.json} +0 -0
  56. data/spec/fixtures/{swagger → contracts/swagger}/petstore.yaml +0 -0
  57. data/spec/integration/e2e_spec.rb +6 -12
  58. data/spec/integration/forensics/integration_matcher_spec.rb +5 -11
  59. data/spec/integration/rspec_spec.rb +12 -12
  60. data/spec/integration/templating_spec.rb +1 -1
  61. data/spec/spec_helper.rb +14 -2
  62. data/spec/unit/pacto/contract_factory_spec.rb +1 -2
  63. data/spec/unit/pacto/contract_spec.rb +44 -70
  64. data/spec/unit/pacto/core/investigation_spec.rb +4 -3
  65. data/spec/unit/pacto/formats/legacy/contract_builder_spec.rb +93 -0
  66. data/spec/unit/pacto/formats/legacy/contract_factory_spec.rb +29 -0
  67. data/spec/unit/pacto/formats/legacy/contract_generator_spec.rb +173 -0
  68. data/spec/unit/pacto/formats/legacy/contract_spec.rb +41 -0
  69. data/spec/unit/pacto/formats/legacy/generator/filters_spec.rb +104 -0
  70. data/spec/unit/pacto/formats/legacy/request_clause_spec.rb +79 -0
  71. data/spec/unit/pacto/formats/legacy/response_clause_spec.rb +45 -0
  72. data/spec/unit/pacto/formats/swagger/contract_factory_spec.rb +58 -0
  73. data/spec/unit/pacto/formats/swagger/contract_spec.rb +47 -0
  74. data/spec/unit/pacto/investigation_registry_spec.rb +1 -2
  75. data/spec/unit/pacto/pacto_spec.rb +6 -4
  76. data/spec/unit/pacto/stubs/uri_pattern_spec.rb +7 -8
  77. data/spec/unit/pacto/stubs/webmock_adapter_spec.rb +2 -4
  78. data/tasks/release.rake +1 -1
  79. metadata +53 -53
  80. data/lib/pacto/contract_builder.rb +0 -125
  81. data/lib/pacto/exceptions/invalid_contract.rb +0 -12
  82. data/lib/pacto/generator/filters.rb +0 -42
  83. data/lib/pacto/generator/hint.rb +0 -26
  84. data/lib/pacto/generator/native_contract_generator.rb +0 -74
  85. data/lib/pacto/native_contract_factory.rb +0 -60
  86. data/lib/pacto/swagger_contract_factory.rb +0 -90
  87. data/spec/pacto/dummy_server.rb +0 -4
  88. data/spec/pacto/dummy_server/dummy.rb +0 -51
  89. data/spec/pacto/dummy_server/jruby_workaround_helper.rb +0 -23
  90. data/spec/pacto/dummy_server/playback_servlet.rb +0 -22
  91. data/spec/unit/pacto/contract_builder_spec.rb +0 -89
  92. data/spec/unit/pacto/generator/filters_spec.rb +0 -100
  93. data/spec/unit/pacto/generator/native_contract_generator_spec.rb +0 -171
  94. data/spec/unit/pacto/native_contract_factory_spec.rb +0 -26
  95. data/spec/unit/pacto/request_clause_spec.rb +0 -75
  96. data/spec/unit/pacto/response_clause_spec.rb +0 -41
  97. data/spec/unit/pacto/server/playback_servlet_spec.rb +0 -27
  98. data/spec/unit/pacto/swagger_contract_factory_spec.rb +0 -56
@@ -1,26 +0,0 @@
1
- # -*- encoding : utf-8 -*-
2
- module Pacto
3
- module Generator
4
- class Hint < Pacto::RequestClause
5
- property :service_name, required: true
6
- property :target_file
7
-
8
- def initialize(data)
9
- super
10
- self.target_file ||= "#{slugify(service_name)}.json"
11
- self
12
- end
13
-
14
- def matches?(pacto_request)
15
- return false if pacto_request.nil?
16
- Pacto::RequestPattern.for(self).matches?(pacto_request)
17
- end
18
-
19
- private
20
-
21
- def slugify(path)
22
- path.downcase.gsub(' ', '_')
23
- end
24
- end
25
- end
26
- end
@@ -1,74 +0,0 @@
1
- # -*- encoding : utf-8 -*-
2
- require 'json/schema_generator'
3
- require 'pacto/contract_builder'
4
-
5
- module Pacto
6
- module Generator
7
- class NativeContractGenerator
8
- include Logger
9
-
10
- def initialize(_schema_version = 'draft3',
11
- schema_generator = JSON::SchemaGenerator,
12
- validator = Pacto::MetaSchema.new,
13
- filters = Pacto::Generator::Filters.new,
14
- consumer = Pacto::Consumer.new)
15
- @contract_builder = ContractBuilder.new(schema_generator: schema_generator, filters: filters)
16
- @consumer = consumer
17
- @validator = validator
18
- end
19
-
20
- def generate(pacto_request, pacto_response)
21
- return unless Pacto.generating?
22
- logger.debug("Generating Contract for #{pacto_request}, #{pacto_response}")
23
- begin
24
- contract_file = load_contract_file(pacto_request)
25
-
26
- unless File.exist? contract_file
27
- uri = URI(pacto_request.uri)
28
- FileUtils.mkdir_p(File.dirname contract_file)
29
- raw_contract = save(uri, pacto_request, pacto_response)
30
- File.write(contract_file, raw_contract)
31
- logger.debug("Generating #{contract_file}")
32
-
33
- Pacto.load_contract contract_file, uri.host
34
- end
35
- rescue => e
36
- raise StandardError, "Error while generating Contract #{contract_file}: #{e.message}", e.backtrace
37
- end
38
- end
39
-
40
- def generate_from_partial_contract(request_file, host)
41
- contract = Pacto.load_contract request_file, host
42
- request, response = @consumer.request(contract)
43
- save(request_file, request, response)
44
- end
45
-
46
- def save(source, request, response)
47
- @contract_builder.source = source
48
- # TODO: Get rid of the generate_contract call, just use add_example/infer_all
49
- @contract_builder.add_example('default', request, response).generate_contract(request, response) # .infer_all
50
- @contract_builder.without_examples if Pacto.configuration.generator_options[:no_examples]
51
- contract = @contract_builder.build_hash
52
- pretty_contract = MultiJson.encode(contract, pretty: true)
53
- # This is because of a discrepency w/ jruby vs MRI pretty json
54
- pretty_contract.gsub!(/^$\n/, '')
55
- @validator.validate pretty_contract
56
- pretty_contract
57
- end
58
-
59
- private
60
-
61
- def load_contract_file(pacto_request)
62
- hint = Pacto::Generator.hint_for(pacto_request)
63
- if hint.nil?
64
- uri = URI(pacto_request.uri)
65
- path = uri.path
66
- basename = File.basename(path, '.json') + '.json'
67
- File.join(Pacto.configuration.contracts_path, uri.host, File.dirname(path), basename)
68
- else
69
- File.expand_path(hint.target_file, Pacto.configuration.contracts_path)
70
- end
71
- end
72
- end
73
- end
74
- end
@@ -1,60 +0,0 @@
1
- # -*- encoding : utf-8 -*-
2
- module Pacto
3
- # Builds {Pacto::Contract} instances from Pacto's native Contract format.
4
- class NativeContractFactory
5
- attr_reader :schema
6
-
7
- def initialize(options = {})
8
- @schema = options[:schema] || MetaSchema.new
9
- end
10
-
11
- def build_from_file(contract_path, host)
12
- contract_definition = File.read(contract_path)
13
- definition = JSON.parse(contract_definition)
14
- schema.validate definition
15
- definition['request'].merge!('host' => host)
16
- body_to_schema(definition, 'request', contract_path)
17
- body_to_schema(definition, 'response', contract_path)
18
- method_to_http_method(definition, contract_path)
19
- request = RequestClause.new(definition['request'])
20
- response = ResponseClause.new(definition['response'])
21
- Contract.new(request: request, response: response, file: contract_path, name: definition['name'], examples: definition['examples'])
22
- end
23
-
24
- def files_for(contracts_dir)
25
- full_path = Pathname.new(contracts_dir).realpath
26
-
27
- if full_path.directory?
28
- all_json_files = "#{full_path}/**/*.json"
29
- Dir.glob(all_json_files).map do |f|
30
- Pathname.new(f)
31
- end
32
- else
33
- [full_path]
34
- end
35
- end
36
-
37
- private
38
-
39
- def body_to_schema(definition, section, file)
40
- schema = definition[section].delete 'body'
41
- return nil unless schema
42
-
43
- Pacto::UI.deprecation "Contract format deprecation: #{section}:body will be moved to #{section}:schema (#{file})"
44
- definition[section]['schema'] = schema
45
- end
46
-
47
- def method_to_http_method(definition, file)
48
- method = definition['request'].delete 'method'
49
- return nil unless method
50
-
51
- Pacto::UI.deprecation "Contract format deprecation: request:method will be moved to request:http_method (#{file})"
52
- definition['request']['http_method'] = method
53
- end
54
- end
55
- end
56
-
57
- factory = Pacto::NativeContractFactory.new
58
-
59
- Pacto::ContractFactory.add_factory(:native, factory)
60
- Pacto::ContractFactory.add_factory(:default, factory)
@@ -1,90 +0,0 @@
1
- # -*- encoding : utf-8 -*-
2
- require 'swagger'
3
-
4
- module Pacto
5
- # Builds {Pacto::Contract} instances from Swagger documents
6
- class SwaggerContractFactory
7
- include Logger
8
-
9
- def load_hints(contract_path, host = nil)
10
- app = Swagger.load(contract_path)
11
- app.operations.map do |op|
12
- Pacto::Generator::Hint.new(request_clause_hash(op, host).merge(
13
- service_name: op.fetch(:summary)
14
- ))
15
- end
16
- end
17
-
18
- def build_from_file(contract_path, host = nil)
19
- app = Swagger.load(contract_path)
20
- app.operations.map do |op|
21
- default_response = op.default_response
22
- request = Pacto::RequestClause.new(request_clause_hash(op, host))
23
- response = Pacto::ResponseClause.new(response_clause_hash(op, default_response, host))
24
- Contract.new(
25
- id: op.operationId,
26
- name: op.full_name,
27
- file: contract_path,
28
- request: request, response: response,
29
- examples: build_examples(op, default_response)
30
- )
31
- end
32
- rescue ArgumentError => e
33
- raise "Could not load #{contract_path}: #{e.message}"
34
- end
35
-
36
- def files_for(contracts_dir)
37
- full_path = Pathname.new(contracts_dir).realpath
38
-
39
- if full_path.directory?
40
- all_json_files = "#{full_path}/**/*.{json,yaml,yml}"
41
- Dir.glob(all_json_files).map do |f|
42
- Pathname.new(f)
43
- end
44
- else
45
- [full_path]
46
- end
47
- end
48
-
49
- private
50
-
51
- def request_clause_hash(op, host)
52
- {
53
- host: op.host || host,
54
- http_method: op.verb,
55
- path: op.path
56
- }
57
- end
58
-
59
- def response_clause_hash(op, response, _host)
60
- if response.nil?
61
- logger.warn("No response defined for #{op.full_name}")
62
- return Pacto::ResponseClause.new(status: 200)
63
- end
64
-
65
- {}.tap do | response_clause |
66
- response_clause[:status] = response.status_code || 200
67
- response_clause[:schema] = response.schema.parse unless response.schema.nil?
68
- end
69
- end
70
-
71
- def build_examples(op, response)
72
- return nil if response.nil? || response.examples.nil? || response.examples.empty?
73
-
74
- {
75
- default: {
76
- request: {}, # Swagger doesn't have a clear way to capture request examples
77
- response: {
78
- body: response.examples.values.first.parse
79
- }
80
- }
81
- }
82
- rescue => e # FIXME: Only parsing errors?
83
- logger.warn("Error while trying to parse response example for #{op.full_name}")
84
- logger.debug(" Error details: #{e.inspect}")
85
- nil
86
- end
87
- end
88
- end
89
-
90
- Pacto::ContractFactory.add_factory(:swagger, Pacto::SwaggerContractFactory.new)
@@ -1,4 +0,0 @@
1
- # -*- encoding : utf-8 -*-
2
- require_relative 'dummy_server/dummy'
3
- require_relative 'dummy_server/playback_servlet'
4
- require_relative 'dummy_server/jruby_workaround_helper'
@@ -1,51 +0,0 @@
1
- # -*- encoding : utf-8 -*-
2
- require 'webrick'
3
- require 'forwardable'
4
- require 'tempfile'
5
-
6
- module Pacto
7
- module DummyServer
8
- class Servlet < WEBrick::HTTPServlet::AbstractServlet
9
- extend Forwardable
10
-
11
- def initialize(server, json)
12
- super(server)
13
-
14
- @doer = PlaybackServlet.new(
15
- status: 200,
16
- headers: { 'Content-Type' => 'application/json', 'Vary' => 'Accept' },
17
- body: json
18
- )
19
- end
20
-
21
- def_delegator :@doer, :do_GET # rubocop:disable SymbolName
22
- end
23
-
24
- class Dummy
25
- def initialize(port, path, response)
26
- log_file = File.exist?('/dev/null') ? '/dev/null' : Tempfile.new('log') # So tests run on Windows
27
- params = {
28
- Port: port,
29
- AccessLog: [],
30
- Logger: WEBrick::Log.new(log_file, 7)
31
- }
32
- @server = WEBrick::HTTPServer.new params
33
- @server.mount path, Servlet, response
34
- end
35
-
36
- def start
37
- @pid = Thread.new do
38
- trap 'INT' do
39
- @server.shutdown
40
- end
41
- @server.start
42
- end
43
- end
44
-
45
- def terminate
46
- @server.shutdown
47
- @pid.kill
48
- end
49
- end
50
- end
51
- end
@@ -1,23 +0,0 @@
1
- # -*- encoding : utf-8 -*-
2
- module Pacto
3
- module DummyServer
4
- module JRubyWorkaroundHelper
5
- include Pacto::TestHelper
6
-
7
- def run_pacto
8
- WebMock.allow_net_connect!
9
- # There are issues with EventMachine on JRuby, so it can't currently us with_pacto
10
- if RUBY_PLATFORM == 'java'
11
- @server = Pacto::DummyServer::Dummy.new 8000, '/hello', '{"message": "Hello World!"}'
12
- @server.start
13
- yield
14
- @server.terminate
15
- else
16
- with_pacto(port: 8000, strip_port: true) do
17
- yield
18
- end
19
- end
20
- end
21
- end
22
- end
23
- end
@@ -1,22 +0,0 @@
1
- # -*- encoding : utf-8 -*-
2
- module Pacto
3
- module DummyServer
4
- class PlaybackServlet
5
- attr_reader :status, :headers, :body
6
-
7
- def initialize(attributes)
8
- @status = attributes.fetch(:status, 200)
9
- @headers = attributes.fetch(:headers, [])
10
- @body = attributes.fetch(:body, nil)
11
- end
12
-
13
- def do_GET(_request, response) # rubocop:disable MethodName
14
- response.status = status
15
- headers.each do |key, value|
16
- response[key] = value
17
- end
18
- response.body = body
19
- end
20
- end
21
- end
22
- end
@@ -1,89 +0,0 @@
1
- # -*- encoding : utf-8 -*-
2
- module Pacto
3
- describe ContractBuilder do
4
- let(:data) { subject.build_hash }
5
- describe '#name' do
6
- it 'sets the contract name' do
7
- subject.name = 'foo'
8
- expect(data).to include(name: 'foo')
9
- end
10
- end
11
-
12
- describe '#add_example' do
13
- let(:examples) { subject.build_hash[:examples] }
14
- it 'adds named examples to the contract' do
15
- subject.add_example 'foo', Fabricate(:pacto_request), Fabricate(:pacto_response)
16
- subject.add_example 'bar', Fabricate(:pacto_request), Fabricate(:pacto_response)
17
- expect(examples).to be_a(Hash)
18
- expect(examples.keys).to include('foo', 'bar')
19
- expect(examples['foo'][:response]).to include(status: 200)
20
- expect(data)
21
- end
22
- end
23
-
24
- context 'without examples' do
25
- describe '#infer_schemas' do
26
- it 'does not add schemas' do
27
- subject.name = 'test'
28
- subject.infer_schemas
29
- expect(data[:request][:schema]).to be_nil
30
- expect(data[:response][:schema]).to be_nil
31
- end
32
- end
33
- end
34
-
35
- context 'with examples' do
36
- before(:each) do
37
- subject.add_example 'success', Fabricate(:pacto_request), Fabricate(:pacto_response)
38
- subject.add_example 'not found', Fabricate(:pacto_request), Fabricate(:pacto_response)
39
- end
40
-
41
- describe '#without_examples' do
42
- it 'stops the builder from including examples in the final data' do
43
- expect(subject.build_hash.keys).to include(:examples)
44
- expect(subject.without_examples.build_hash.keys).to_not include(:examples)
45
- end
46
- end
47
-
48
- describe '#infer_schemas' do
49
- it 'adds schemas' do
50
- subject.name = 'test'
51
- subject.infer_schemas
52
- contract = subject.build
53
- expect(contract.request.schema).to_not be_nil
54
- expect(contract.request.schema).to_not be_nil
55
- end
56
- end
57
- end
58
-
59
- context 'generating from interactions' do
60
- let(:request) { Fabricate(:pacto_request) }
61
- let(:response) { Fabricate(:pacto_response) }
62
- let(:data) { subject.generate_response(request, response).build_hash }
63
- let(:contract) { subject.generate_contract(request, response).build }
64
-
65
- describe '#generate_response' do
66
- it 'sets the response status' do
67
- expect(data[:response]).to include(
68
- status: 200
69
- )
70
- end
71
-
72
- it 'sets response headers' do
73
- expect(data[:response][:headers]).to be_a(Hash)
74
- end
75
- end
76
-
77
- describe '#infer_schemas' do
78
- it 'sets the schemas based on the examples' do
79
- expect(contract.request.schema).to_not be_nil
80
- expect(contract.request.schema).to_not be_nil
81
- end
82
- end
83
- end
84
-
85
- skip '#add_request_header'
86
- skip '#add_response_header'
87
- skip '#filter'
88
- end
89
- end