pacto 0.2.5 → 0.3.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|