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
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pacto
|
2
|
+
module Hooks
|
3
|
+
class ERBHook < Pacto::Callback
|
4
|
+
def initialize
|
5
|
+
@processor = ERBProcessor.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def process(contracts, request_signature, response)
|
9
|
+
bound_values = contracts.empty? ? {} : contracts.first.values
|
10
|
+
bound_values.merge!({:req => { 'HEADERS' => request_signature.headers}})
|
11
|
+
response.body = @processor.process response.body, bound_values
|
12
|
+
response.body
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/pacto/logger.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Pacto
|
2
|
+
class Logger
|
3
|
+
include Singleton
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
def_delegators :@log, :debug, :info, :warn, :error, :fatal
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
log ::Logger.new STDOUT
|
10
|
+
end
|
11
|
+
|
12
|
+
def log log
|
13
|
+
@log = log
|
14
|
+
@log.level = default_level
|
15
|
+
@log.progname = 'Pacto'
|
16
|
+
end
|
17
|
+
|
18
|
+
def level= level
|
19
|
+
@log.level = log_levels.fetch(level, default_level)
|
20
|
+
end
|
21
|
+
|
22
|
+
def level
|
23
|
+
log_levels.key @log.level
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def default_level
|
29
|
+
::Logger::ERROR
|
30
|
+
end
|
31
|
+
|
32
|
+
def log_levels
|
33
|
+
{
|
34
|
+
debug: ::Logger::DEBUG,
|
35
|
+
info: ::Logger::INFO,
|
36
|
+
warn: ::Logger::WARN,
|
37
|
+
error: ::Logger::ERROR,
|
38
|
+
fatal: ::Logger::FATAL
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pacto
|
2
|
+
class MetaSchema
|
3
|
+
attr_accessor :schema, :engine
|
4
|
+
|
5
|
+
def initialize(engine = JSON::Validator)
|
6
|
+
@schema = File.join(File.dirname(File.expand_path(__FILE__)), '../../resources/contract_schema.json')
|
7
|
+
@engine = engine
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate definition
|
11
|
+
errors = engine.fully_validate(schema, definition, :version => :draft3)
|
12
|
+
unless errors.empty?
|
13
|
+
raise InvalidContract.new(errors)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/pacto/rake_task.rb
CHANGED
@@ -17,49 +17,70 @@ module Pacto
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def install
|
20
|
-
desc
|
20
|
+
desc 'Tasks for Pacto gem'
|
21
21
|
namespace :pacto do
|
22
22
|
validate_task
|
23
|
+
generate_task
|
24
|
+
meta_validate
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
28
|
def validate_task
|
27
|
-
desc
|
29
|
+
desc 'Validates all contracts in a given directory against a given host'
|
28
30
|
task :validate, :host, :dir do |t, args|
|
29
31
|
if args.to_a.size < 2
|
30
|
-
fail
|
32
|
+
fail 'USAGE: rake pacto:validate[<host>, <contract_dir>]'.colorize(:yellow)
|
31
33
|
end
|
32
34
|
|
33
35
|
validate_contracts(args[:host], args[:dir])
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
37
|
-
def
|
38
|
-
|
39
|
+
def generate_task
|
40
|
+
desc 'Generates contracts from partial contracts'
|
41
|
+
task :generate, :input_dir, :output_dir, :host do |t, args|
|
42
|
+
if args.to_a.size < 3
|
43
|
+
fail 'USAGE: rake pacto:generate[<request_contract_dir>, <output_dir>, <record_host>]'.colorize(:yellow)
|
44
|
+
end
|
45
|
+
|
46
|
+
generate_contracts(args[:input_dir], args[:output_dir], args[:host])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def meta_validate
|
51
|
+
desc 'Validates a directory of contract definitions'
|
52
|
+
task :meta_validate, :dir do |t, args|
|
53
|
+
if args.to_a.size < 1
|
54
|
+
fail 'USAGE: rake pacto:meta_validate[<contract_dir>]'.colorize(:yellow)
|
55
|
+
end
|
39
56
|
|
40
|
-
|
41
|
-
|
42
|
-
|
57
|
+
each_contract(args[:dir]) do |contract_file|
|
58
|
+
puts "Validating #{contract_file}"
|
59
|
+
fail unless Pacto.validate_contract contract_file
|
60
|
+
end
|
43
61
|
end
|
62
|
+
end
|
44
63
|
|
64
|
+
def validate_contracts(host, dir)
|
65
|
+
WebMock.allow_net_connect!
|
45
66
|
puts "Validating contracts in directory #{dir} against host #{host}\n\n"
|
46
67
|
|
47
68
|
total_failed = 0
|
48
|
-
|
69
|
+
each_contract(dir) do |contact_file|
|
49
70
|
print "#{contract_file.split('/').last}:"
|
50
71
|
contract = Pacto.build_from_file(contract_file, host)
|
51
72
|
errors = contract.validate
|
52
73
|
|
53
74
|
if errors.empty?
|
54
|
-
puts
|
75
|
+
puts ' OK!'.colorize(:green)
|
55
76
|
else
|
56
77
|
@exit_with_error = true
|
57
78
|
total_failed += 1
|
58
|
-
puts
|
79
|
+
puts ' FAILED!'.colorize(:red)
|
59
80
|
errors.each do |error|
|
60
81
|
puts "\t* #{error}".colorize(:light_red)
|
61
82
|
end
|
62
|
-
puts
|
83
|
+
puts ''
|
63
84
|
end
|
64
85
|
end
|
65
86
|
|
@@ -69,6 +90,48 @@ module Pacto
|
|
69
90
|
puts "#{contracts.size} valid contract#{contracts.size > 1 ? 's' : nil}".colorize(:green)
|
70
91
|
end
|
71
92
|
end
|
93
|
+
|
94
|
+
def generate_contracts(input_dir, output_dir, host)
|
95
|
+
WebMock.allow_net_connect!
|
96
|
+
generator = Pacto::Generator.new
|
97
|
+
puts "Generating contracts from partial contracts in #{input_dir} and recording to #{output_dir}\n\n"
|
98
|
+
|
99
|
+
failed_contracts = []
|
100
|
+
each_contract(input_dir) do |contract_file|
|
101
|
+
begin
|
102
|
+
contract = generator.generate(contract_file, host)
|
103
|
+
output_file = File.expand_path(File.basename(contract_file), output_dir)
|
104
|
+
output_file = File.open(output_file, 'wb')
|
105
|
+
output_file.write contract
|
106
|
+
output_file.flush
|
107
|
+
output_file.close
|
108
|
+
rescue InvalidContract => e
|
109
|
+
failed_contracts << contract_file
|
110
|
+
puts e.message.colorize(:red)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if failed_contracts.empty?
|
115
|
+
puts 'Successfully generated all contracts'.colorize(:green)
|
116
|
+
else
|
117
|
+
fail "The following contracts could not be generated: #{failed_contracts.join ','}".colorize(:red)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def each_contract(dir)
|
124
|
+
if File.file? dir
|
125
|
+
yield dir
|
126
|
+
else
|
127
|
+
contracts = Dir[File.join(dir, '*{.json.erb,.json}')]
|
128
|
+
fail "No contracts found in directory #{dir}".colorize(:yellow) if contracts.empty?
|
129
|
+
|
130
|
+
contracts.sort.each do |contract_file|
|
131
|
+
yield contract_file
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
72
135
|
end
|
73
136
|
end
|
74
137
|
|
data/lib/pacto/request.rb
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
module Pacto
|
2
2
|
class Request
|
3
|
+
attr_reader :host
|
4
|
+
|
3
5
|
def initialize(host, definition)
|
4
6
|
@host = host
|
5
7
|
@definition = definition
|
6
8
|
end
|
7
9
|
|
8
|
-
def host
|
9
|
-
@host
|
10
|
-
end
|
11
|
-
|
12
10
|
def method
|
13
11
|
@definition['method'].to_s.downcase.to_sym
|
14
12
|
end
|
@@ -47,6 +45,7 @@ module Pacto
|
|
47
45
|
end
|
48
46
|
|
49
47
|
private
|
48
|
+
|
50
49
|
def httparty_params_key
|
51
50
|
method == :get ? :query : :body
|
52
51
|
end
|
data/lib/pacto/response.rb
CHANGED
@@ -1,55 +1,63 @@
|
|
1
1
|
module Pacto
|
2
2
|
class Response
|
3
|
+
attr_reader :status, :headers, :schema
|
4
|
+
|
3
5
|
def initialize(definition)
|
4
6
|
@definition = definition
|
7
|
+
@status = @definition['status']
|
8
|
+
@headers = @definition['headers']
|
9
|
+
@schema = @definition['body']
|
5
10
|
end
|
6
11
|
|
7
12
|
def instantiate
|
8
13
|
OpenStruct.new({
|
9
|
-
'status' => @
|
10
|
-
'headers' => @
|
11
|
-
'body' => JSON::Generator.generate(@
|
14
|
+
'status' => @status,
|
15
|
+
'headers' => @headers,
|
16
|
+
'body' => JSON::Generator.generate(@schema)
|
12
17
|
})
|
13
18
|
end
|
14
19
|
|
15
|
-
def validate(response)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
def validate(response, opt = {})
|
21
|
+
|
22
|
+
unless opt[:body_only]
|
23
|
+
if @definition['status'] != response.status
|
24
|
+
return ["Invalid status: expected #{@definition['status']} but got #{response.status}"]
|
25
|
+
end
|
26
|
+
|
27
|
+
unless @definition['headers'].normalize_keys.subset_of?(response.headers.normalize_keys)
|
28
|
+
return ["Invalid headers: expected #{@definition['headers'].inspect} to be a subset of #{response.headers.inspect}"]
|
29
|
+
end
|
22
30
|
end
|
23
|
-
|
31
|
+
|
24
32
|
if @definition['body']
|
25
33
|
if @definition['body']['type'] && @definition['body']['type'] == 'string'
|
26
34
|
validate_as_pure_string response.body
|
27
35
|
else
|
28
|
-
validate_as_json
|
36
|
+
response.respond_to?(:body) ? validate_as_json(response.body) : validate_as_json(response)
|
29
37
|
end
|
30
38
|
else
|
31
39
|
[]
|
32
40
|
end
|
33
41
|
end
|
34
|
-
|
42
|
+
|
35
43
|
private
|
36
|
-
|
44
|
+
|
37
45
|
def validate_as_pure_string response_body
|
38
46
|
errors = []
|
39
47
|
if @definition['body']['required'] && response_body.nil?
|
40
|
-
errors <<
|
48
|
+
errors << 'The response does not contain a body'
|
41
49
|
end
|
42
|
-
|
50
|
+
|
43
51
|
pattern = @definition['body']['pattern']
|
44
52
|
if pattern && !(response_body =~ Regexp.new(pattern))
|
45
53
|
errors << "The response does not match the pattern #{pattern}"
|
46
54
|
end
|
47
|
-
|
55
|
+
|
48
56
|
errors
|
49
57
|
end
|
50
|
-
|
58
|
+
|
51
59
|
def validate_as_json response_body
|
52
|
-
JSON::Validator.fully_validate(@definition['body'], response_body)
|
60
|
+
JSON::Validator.fully_validate(@definition['body'], response_body, :version => :draft3)
|
53
61
|
end
|
54
62
|
end
|
55
63
|
end
|
data/lib/pacto/server.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'webrick'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Pacto
|
5
|
+
module Server
|
6
|
+
class Servlet < WEBrick::HTTPServlet::AbstractServlet
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def initialize server, json
|
10
|
+
super(server)
|
11
|
+
|
12
|
+
@doer = PlaybackServlet.new({
|
13
|
+
status: 200,
|
14
|
+
headers: {'Content-Type' => 'application/json'},
|
15
|
+
body: json
|
16
|
+
})
|
17
|
+
end
|
18
|
+
|
19
|
+
def_delegator :@doer, :do_GET
|
20
|
+
end
|
21
|
+
|
22
|
+
class Dummy
|
23
|
+
def initialize port, path, response
|
24
|
+
params = {
|
25
|
+
:Port => port,
|
26
|
+
:AccessLog => [],
|
27
|
+
:Logger => WEBrick::Log::new('/dev/null', 7)
|
28
|
+
}
|
29
|
+
@server = WEBrick::HTTPServer.new params
|
30
|
+
@server.mount path, Servlet, response
|
31
|
+
end
|
32
|
+
|
33
|
+
def start
|
34
|
+
@pid = Thread.new do
|
35
|
+
trap 'INT' do @server.shutdown end
|
36
|
+
@server.start
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def terminate
|
41
|
+
@pid.kill
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Pacto
|
2
|
+
module Server
|
3
|
+
class PlaybackServlet
|
4
|
+
attr_reader :status, :headers, :body
|
5
|
+
|
6
|
+
def initialize(attributes)
|
7
|
+
@status = attributes.fetch(:status, 200)
|
8
|
+
@headers = attributes.fetch(:headers, [])
|
9
|
+
@body = attributes.fetch(:body, nil)
|
10
|
+
end
|
11
|
+
|
12
|
+
def do_GET(request, response)
|
13
|
+
response.status = status
|
14
|
+
headers.each do |key, value|
|
15
|
+
response[key] = value
|
16
|
+
end
|
17
|
+
response.body = body
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Pacto
|
2
|
+
module Stubs
|
3
|
+
class BuiltIn
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
register_callbacks
|
7
|
+
end
|
8
|
+
|
9
|
+
def stub_request! request, response
|
10
|
+
stub = WebMock.stub_request(request.method, "#{request.host}#{request.path}")
|
11
|
+
stub = stub.with(request_details(request)) if Pacto.configuration.strict_matchers
|
12
|
+
stub.to_return({
|
13
|
+
:status => response.status,
|
14
|
+
:headers => response.headers,
|
15
|
+
:body => format_body(response.body)
|
16
|
+
})
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset!
|
20
|
+
WebMock.reset!
|
21
|
+
WebMock.reset_callbacks
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def register_callbacks
|
27
|
+
WebMock.after_request do |request_signature, response|
|
28
|
+
contracts = Pacto.contract_for request_signature
|
29
|
+
Pacto.configuration.callback.process contracts, request_signature, response
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def format_body(body)
|
34
|
+
if body.is_a?(Hash) || body.is_a?(Array)
|
35
|
+
body.to_json
|
36
|
+
else
|
37
|
+
body
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def request_details request
|
42
|
+
details = {}
|
43
|
+
unless request.params.empty?
|
44
|
+
details[webmock_params_key(request)] = request.params
|
45
|
+
end
|
46
|
+
unless request.headers.empty?
|
47
|
+
details[:headers] = request.headers
|
48
|
+
end
|
49
|
+
details
|
50
|
+
end
|
51
|
+
|
52
|
+
def webmock_params_key request
|
53
|
+
request.method == :get ? :query : :body
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|