pacto 0.2.5 → 0.3.0.pre
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.
- data/.gitignore +3 -0
- data/.rspec +0 -2
- data/.rubocop-todo.yml +51 -0
- data/.rubocop.yml +1 -0
- data/.travis.yml +4 -2
- data/Guardfile +28 -14
- data/README.md +81 -51
- data/Rakefile +24 -11
- data/features/generation/generation.feature +25 -0
- data/features/journeys/validation.feature +74 -0
- data/features/support/env.rb +16 -0
- data/lib/pacto.rb +63 -34
- data/lib/pacto/contract.rb +25 -11
- data/lib/pacto/contract_factory.rb +13 -44
- data/lib/pacto/core/callback.rb +11 -0
- data/lib/pacto/core/configuration.rb +34 -0
- data/lib/pacto/core/contract_repository.rb +44 -0
- data/lib/pacto/erb_processor.rb +18 -0
- data/lib/pacto/exceptions/invalid_contract.rb +10 -1
- data/lib/pacto/extensions.rb +2 -2
- data/lib/pacto/generator.rb +75 -0
- data/lib/pacto/hash_merge_processor.rb +14 -0
- data/lib/pacto/hooks/erb_hook.rb +17 -0
- data/lib/pacto/logger.rb +42 -0
- data/lib/pacto/meta_schema.rb +17 -0
- data/lib/pacto/rake_task.rb +75 -12
- data/lib/pacto/request.rb +3 -4
- data/lib/pacto/response.rb +27 -19
- data/lib/pacto/server.rb +2 -0
- data/lib/pacto/server/dummy.rb +45 -0
- data/lib/pacto/server/playback_servlet.rb +21 -0
- data/lib/pacto/stubs/built_in.rb +57 -0
- data/lib/pacto/version.rb +1 -1
- data/pacto.gemspec +8 -2
- data/resources/contract_schema.json +216 -0
- data/spec/coveralls_helper.rb +10 -0
- data/spec/integration/data/strict_contract.json +33 -0
- data/spec/integration/data/templating_contract.json +25 -0
- data/spec/integration/e2e_spec.rb +40 -7
- data/spec/integration/templating_spec.rb +55 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/unit/data/simple_contract.json +22 -0
- data/spec/unit/hooks/erb_hook_spec.rb +51 -0
- data/spec/unit/pacto/configuration_spec.rb +51 -0
- data/spec/unit/pacto/contract_factory_spec.rb +4 -35
- data/spec/unit/pacto/contract_spec.rb +59 -31
- data/spec/unit/pacto/core/configuration_spec.rb +28 -0
- data/spec/unit/pacto/core/contract_repository_spec.rb +133 -0
- data/spec/unit/pacto/erb_processor_spec.rb +23 -0
- data/spec/unit/pacto/extensions_spec.rb +11 -11
- data/spec/unit/pacto/generator_spec.rb +142 -0
- data/spec/unit/pacto/hash_merge_processor_spec.rb +20 -0
- data/spec/unit/pacto/logger_spec.rb +44 -0
- data/spec/unit/pacto/meta_schema_spec.rb +70 -0
- data/spec/unit/pacto/pacto_spec.rb +32 -58
- data/spec/unit/pacto/request_spec.rb +83 -34
- data/spec/unit/pacto/response_adapter_spec.rb +9 -11
- data/spec/unit/pacto/response_spec.rb +68 -68
- data/spec/unit/pacto/server/playback_servlet_spec.rb +24 -0
- data/spec/unit/pacto/stubs/built_in_spec.rb +168 -0
- metadata +291 -147
- data/.rspec_integration +0 -4
- data/.rspec_unit +0 -4
- data/lib/pacto/file_pre_processor.rb +0 -12
- data/lib/pacto/instantiated_contract.rb +0 -62
- data/spec/integration/spec_helper.rb +0 -1
- data/spec/integration/utils/dummy_server.rb +0 -34
- data/spec/unit/pacto/file_pre_processor_spec.rb +0 -13
- data/spec/unit/pacto/instantiated_contract_spec.rb +0 -224
- data/spec/unit/spec_helper.rb +0 -5
data/lib/pacto.rb
CHANGED
@@ -1,46 +1,75 @@
|
|
1
|
-
require
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
|
12
|
-
|
13
|
-
require
|
14
|
-
require
|
15
|
-
require
|
16
|
-
require
|
17
|
-
require
|
18
|
-
require
|
19
|
-
require
|
20
|
-
require
|
1
|
+
require 'pacto/version'
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
require 'hash_deep_merge'
|
5
|
+
require 'multi_json'
|
6
|
+
require 'json-schema'
|
7
|
+
require 'json-generator'
|
8
|
+
require 'webmock'
|
9
|
+
require 'ostruct'
|
10
|
+
require 'erb'
|
11
|
+
require 'logger'
|
12
|
+
|
13
|
+
require 'pacto/core/contract_repository'
|
14
|
+
require 'pacto/core/configuration'
|
15
|
+
require 'pacto/core/callback'
|
16
|
+
require 'pacto/logger'
|
17
|
+
require 'pacto/exceptions/invalid_contract.rb'
|
18
|
+
require 'pacto/extensions'
|
19
|
+
require 'pacto/request'
|
20
|
+
require 'pacto/response_adapter'
|
21
|
+
require 'pacto/response'
|
22
|
+
require 'pacto/stubs/built_in'
|
23
|
+
require 'pacto/contract'
|
24
|
+
require 'pacto/contract_factory'
|
25
|
+
require 'pacto/erb_processor'
|
26
|
+
require 'pacto/hash_merge_processor'
|
27
|
+
require 'pacto/stubs/built_in'
|
28
|
+
require 'pacto/meta_schema'
|
29
|
+
require 'pacto/hooks/erb_hook'
|
30
|
+
require 'pacto/generator'
|
21
31
|
|
22
32
|
module Pacto
|
23
|
-
|
24
|
-
|
33
|
+
class << self
|
34
|
+
|
35
|
+
def configuration
|
36
|
+
@configuration ||= Configuration.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def clear!
|
40
|
+
Pacto.configuration.provider.reset!
|
41
|
+
@configuration = nil
|
42
|
+
unregister_all!
|
43
|
+
end
|
44
|
+
|
45
|
+
def configure
|
46
|
+
yield(configuration)
|
47
|
+
end
|
25
48
|
end
|
26
49
|
|
27
|
-
def self.
|
28
|
-
|
29
|
-
|
50
|
+
def self.validate_contract contract
|
51
|
+
Pacto::MetaSchema.new.validate contract
|
52
|
+
puts 'All contracts successfully meta-validated'
|
53
|
+
true
|
54
|
+
rescue InvalidContract => exception
|
55
|
+
puts 'Validation errors detected'
|
56
|
+
exception.errors.each do |error|
|
57
|
+
puts " Error: #{error}"
|
58
|
+
end
|
59
|
+
false
|
30
60
|
end
|
31
61
|
|
32
|
-
def self.
|
33
|
-
|
34
|
-
instantiated_contract = registered[contract_name].instantiate(values)
|
35
|
-
instantiated_contract.stub!
|
36
|
-
instantiated_contract
|
62
|
+
def self.build_from_file(contract_path, host, file_pre_processor = Pacto.configuration.preprocessor)
|
63
|
+
ContractFactory.build_from_file(contract_path, host, file_pre_processor)
|
37
64
|
end
|
38
65
|
|
39
|
-
def self.
|
40
|
-
|
66
|
+
def self.load(contract_name)
|
67
|
+
build_from_file(path_for(contract_name), nil)
|
41
68
|
end
|
42
69
|
|
43
|
-
|
44
|
-
|
70
|
+
private
|
71
|
+
|
72
|
+
def self.path_for(contract)
|
73
|
+
File.join(configuration.contracts_path, "#{contract}.json")
|
45
74
|
end
|
46
75
|
end
|
data/lib/pacto/contract.rb
CHANGED
@@ -1,22 +1,36 @@
|
|
1
1
|
module Pacto
|
2
2
|
class Contract
|
3
|
-
|
3
|
+
attr_reader :values
|
4
|
+
attr_reader :request, :response
|
5
|
+
|
6
|
+
def initialize(request, response, file = nil)
|
4
7
|
@request = request
|
5
8
|
@response = response
|
9
|
+
@file = file
|
10
|
+
end
|
11
|
+
|
12
|
+
def stub_contract! values = {}
|
13
|
+
@values = values
|
14
|
+
@stub = Pacto.configuration.provider.stub_request!(@request, stub_response) unless @request.nil?
|
6
15
|
end
|
7
16
|
|
8
|
-
def
|
9
|
-
|
10
|
-
instantiated_contract.replace!(values) unless values.nil?
|
11
|
-
instantiated_contract
|
17
|
+
def validate(response_gotten = provider_response, opt = {})
|
18
|
+
@response.validate(response_gotten, opt)
|
12
19
|
end
|
13
20
|
|
14
|
-
def
|
15
|
-
|
16
|
-
if ENV["DEBUG_CONTRACTS"]
|
17
|
-
puts "[DEBUG] Response: #{response_gotten.inspect}"
|
18
|
-
end
|
19
|
-
@response.validate(response_gotten)
|
21
|
+
def matches? request_signature
|
22
|
+
@stub.matches? request_signature unless @stub.nil?
|
20
23
|
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def provider_response
|
28
|
+
@request.execute
|
29
|
+
end
|
30
|
+
|
31
|
+
def stub_response
|
32
|
+
@response.instantiate
|
33
|
+
end
|
34
|
+
|
21
35
|
end
|
22
36
|
end
|
@@ -1,50 +1,19 @@
|
|
1
1
|
module Pacto
|
2
2
|
class ContractFactory
|
3
|
-
def self.build_from_file(contract_path, host,
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
request = Request.new(host, definition["request"])
|
8
|
-
response = Response.new(definition["response"])
|
9
|
-
Contract.new(request, response)
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.validate_contract definition, contract_path
|
13
|
-
contract_format = {
|
14
|
-
type: "object",
|
15
|
-
required: true,
|
16
|
-
properties: {
|
17
|
-
request: {
|
18
|
-
type: "object",
|
19
|
-
required: true,
|
20
|
-
properties: {
|
21
|
-
method: {type: "string", required: true, pattern: "(GET)|(POST)|(PUT)|(DELETE)"},
|
22
|
-
path: {type: "string", required: true},
|
23
|
-
params: {type: "object", required: true},
|
24
|
-
headers: {type: "object", required: true}
|
25
|
-
}
|
26
|
-
},
|
27
|
-
response: {
|
28
|
-
type: "object",
|
29
|
-
required: true,
|
30
|
-
properties: {
|
31
|
-
status: {type: "integer", required: true},
|
32
|
-
headers: {type: "object", required: true},
|
33
|
-
body: {
|
34
|
-
type: "object",
|
35
|
-
required: false,
|
36
|
-
properties: {
|
37
|
-
type: { type: "string", required: true, pattern: "(string)|(object)|(array)"}
|
38
|
-
}
|
39
|
-
}
|
40
|
-
}
|
41
|
-
}
|
42
|
-
}
|
43
|
-
}.to_json
|
44
|
-
errors = JSON::Validator.fully_validate(contract_format, definition)
|
45
|
-
unless errors.empty?
|
46
|
-
raise InvalidContract, errors.join("\n")
|
3
|
+
def self.build_from_file(contract_path, host, preprocessor)
|
4
|
+
contract_definition = File.read(contract_path)
|
5
|
+
if preprocessor
|
6
|
+
contract_definition = preprocessor.process(contract_definition)
|
47
7
|
end
|
8
|
+
definition = JSON.parse(contract_definition)
|
9
|
+
schema.validate definition
|
10
|
+
request = Request.new(host, definition['request'])
|
11
|
+
response = Response.new(definition['response'])
|
12
|
+
Contract.new(request, response, contract_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.schema
|
16
|
+
@schema ||= MetaSchema.new
|
48
17
|
end
|
49
18
|
end
|
50
19
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Pacto
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :preprocessor, :postprocessor, :provider, :strict_matchers, :contracts_path, :logger
|
4
|
+
attr_reader :callback
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@preprocessor = ERBProcessor.new
|
8
|
+
@postprocessor = HashMergeProcessor.new
|
9
|
+
@provider = Pacto::Stubs::BuiltIn.new
|
10
|
+
@strict_matchers = true
|
11
|
+
@contracts_path = nil
|
12
|
+
@logger = Logger.instance
|
13
|
+
if ENV['PACTO_DEBUG']
|
14
|
+
@logger.level = :debug
|
15
|
+
else
|
16
|
+
@logger.level = :default
|
17
|
+
end
|
18
|
+
@callback = Pacto::Hooks::ERBHook.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def register_contract(contract = nil, *tags)
|
22
|
+
Pacto.register_contract(contract, *tags)
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_callback(callback = nil, &block)
|
26
|
+
if block_given?
|
27
|
+
@callback = Pacto::Callback.new(&block)
|
28
|
+
else
|
29
|
+
raise 'Expected a Pacto::Callback' unless callback.is_a? Pacto::Callback
|
30
|
+
@callback = callback
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,44 @@
|
|
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
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Pacto
|
2
|
+
class ERBProcessor
|
3
|
+
def process(contract, values = {})
|
4
|
+
values ||= {}
|
5
|
+
erb = ERB.new(contract)
|
6
|
+
erb_result = erb.result hash_binding(values)
|
7
|
+
Logger.instance.debug "Processed contract: #{erb_result.inspect}"
|
8
|
+
erb_result
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def hash_binding(values)
|
14
|
+
namespace = OpenStruct.new(values)
|
15
|
+
namespace.instance_eval { binding }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/pacto/extensions.rb
CHANGED
@@ -2,11 +2,11 @@ module Pacto
|
|
2
2
|
module Extensions
|
3
3
|
module HashSubsetOf
|
4
4
|
def subset_of?(other)
|
5
|
-
(
|
5
|
+
(to_a - other.to_a).empty?
|
6
6
|
end
|
7
7
|
|
8
8
|
def normalize_keys
|
9
|
-
|
9
|
+
inject({}) do |normalized, (key, value)|
|
10
10
|
normalized[key.to_s.downcase] = value
|
11
11
|
normalized
|
12
12
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'json/schema_generator'
|
2
|
+
|
3
|
+
module Pacto
|
4
|
+
class Generator
|
5
|
+
attr_accessor :request_headers_to_filter
|
6
|
+
attr_accessor :response_headers_to_filter
|
7
|
+
|
8
|
+
INFORMATIONAL_REQUEST_HEADERS =
|
9
|
+
%w{
|
10
|
+
content-length
|
11
|
+
via
|
12
|
+
}
|
13
|
+
|
14
|
+
INFORMATIONAL_RESPONSE_HEADERS =
|
15
|
+
%w{
|
16
|
+
server
|
17
|
+
date
|
18
|
+
content-length
|
19
|
+
connection
|
20
|
+
}
|
21
|
+
|
22
|
+
def initialize(schema_version = 'draft3',
|
23
|
+
schema_generator = JSON::SchemaGenerator,
|
24
|
+
validator = Pacto::MetaSchema.new)
|
25
|
+
@schema_version = schema_version
|
26
|
+
@validator = validator
|
27
|
+
@schema_generator = schema_generator
|
28
|
+
@response_headers_to_filter = INFORMATIONAL_RESPONSE_HEADERS
|
29
|
+
@request_headers_to_filter = INFORMATIONAL_REQUEST_HEADERS
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate(request_file, host)
|
33
|
+
contract = Pacto.build_from_file request_file, host
|
34
|
+
request = contract.request
|
35
|
+
response = request.execute
|
36
|
+
save(request_file, request, response)
|
37
|
+
end
|
38
|
+
|
39
|
+
def save(source, request, response)
|
40
|
+
body_schema = JSON::SchemaGenerator.generate source, response.body, @schema_version
|
41
|
+
contract = {
|
42
|
+
:request => {
|
43
|
+
:headers => filter_request_headers(request.headers),
|
44
|
+
:method => request.method,
|
45
|
+
:params => request.params,
|
46
|
+
:path => request.path
|
47
|
+
},
|
48
|
+
:response => {
|
49
|
+
:headers => filter_response_headers(response.headers),
|
50
|
+
:status => response.status,
|
51
|
+
:body => MultiJson.load(body_schema)
|
52
|
+
}
|
53
|
+
}
|
54
|
+
pretty_contract = MultiJson.encode(contract, :pretty => true)
|
55
|
+
# This is because of a discrepency w/ jruby vs MRI pretty json
|
56
|
+
pretty_contract.gsub! /^$\n/, ''
|
57
|
+
@validator.validate pretty_contract
|
58
|
+
pretty_contract
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def filter_request_headers headers
|
64
|
+
headers.reject do |header|
|
65
|
+
@request_headers_to_filter.include? header.downcase
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def filter_response_headers headers
|
70
|
+
headers.reject do |header|
|
71
|
+
@response_headers_to_filter.include? header.downcase
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,14 @@
|
|
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
|