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.
Files changed (70) hide show
  1. data/.gitignore +3 -0
  2. data/.rspec +0 -2
  3. data/.rubocop-todo.yml +51 -0
  4. data/.rubocop.yml +1 -0
  5. data/.travis.yml +4 -2
  6. data/Guardfile +28 -14
  7. data/README.md +81 -51
  8. data/Rakefile +24 -11
  9. data/features/generation/generation.feature +25 -0
  10. data/features/journeys/validation.feature +74 -0
  11. data/features/support/env.rb +16 -0
  12. data/lib/pacto.rb +63 -34
  13. data/lib/pacto/contract.rb +25 -11
  14. data/lib/pacto/contract_factory.rb +13 -44
  15. data/lib/pacto/core/callback.rb +11 -0
  16. data/lib/pacto/core/configuration.rb +34 -0
  17. data/lib/pacto/core/contract_repository.rb +44 -0
  18. data/lib/pacto/erb_processor.rb +18 -0
  19. data/lib/pacto/exceptions/invalid_contract.rb +10 -1
  20. data/lib/pacto/extensions.rb +2 -2
  21. data/lib/pacto/generator.rb +75 -0
  22. data/lib/pacto/hash_merge_processor.rb +14 -0
  23. data/lib/pacto/hooks/erb_hook.rb +17 -0
  24. data/lib/pacto/logger.rb +42 -0
  25. data/lib/pacto/meta_schema.rb +17 -0
  26. data/lib/pacto/rake_task.rb +75 -12
  27. data/lib/pacto/request.rb +3 -4
  28. data/lib/pacto/response.rb +27 -19
  29. data/lib/pacto/server.rb +2 -0
  30. data/lib/pacto/server/dummy.rb +45 -0
  31. data/lib/pacto/server/playback_servlet.rb +21 -0
  32. data/lib/pacto/stubs/built_in.rb +57 -0
  33. data/lib/pacto/version.rb +1 -1
  34. data/pacto.gemspec +8 -2
  35. data/resources/contract_schema.json +216 -0
  36. data/spec/coveralls_helper.rb +10 -0
  37. data/spec/integration/data/strict_contract.json +33 -0
  38. data/spec/integration/data/templating_contract.json +25 -0
  39. data/spec/integration/e2e_spec.rb +40 -7
  40. data/spec/integration/templating_spec.rb +55 -0
  41. data/spec/spec_helper.rb +17 -0
  42. data/spec/unit/data/simple_contract.json +22 -0
  43. data/spec/unit/hooks/erb_hook_spec.rb +51 -0
  44. data/spec/unit/pacto/configuration_spec.rb +51 -0
  45. data/spec/unit/pacto/contract_factory_spec.rb +4 -35
  46. data/spec/unit/pacto/contract_spec.rb +59 -31
  47. data/spec/unit/pacto/core/configuration_spec.rb +28 -0
  48. data/spec/unit/pacto/core/contract_repository_spec.rb +133 -0
  49. data/spec/unit/pacto/erb_processor_spec.rb +23 -0
  50. data/spec/unit/pacto/extensions_spec.rb +11 -11
  51. data/spec/unit/pacto/generator_spec.rb +142 -0
  52. data/spec/unit/pacto/hash_merge_processor_spec.rb +20 -0
  53. data/spec/unit/pacto/logger_spec.rb +44 -0
  54. data/spec/unit/pacto/meta_schema_spec.rb +70 -0
  55. data/spec/unit/pacto/pacto_spec.rb +32 -58
  56. data/spec/unit/pacto/request_spec.rb +83 -34
  57. data/spec/unit/pacto/response_adapter_spec.rb +9 -11
  58. data/spec/unit/pacto/response_spec.rb +68 -68
  59. data/spec/unit/pacto/server/playback_servlet_spec.rb +24 -0
  60. data/spec/unit/pacto/stubs/built_in_spec.rb +168 -0
  61. metadata +291 -147
  62. data/.rspec_integration +0 -4
  63. data/.rspec_unit +0 -4
  64. data/lib/pacto/file_pre_processor.rb +0 -12
  65. data/lib/pacto/instantiated_contract.rb +0 -62
  66. data/spec/integration/spec_helper.rb +0 -1
  67. data/spec/integration/utils/dummy_server.rb +0 -34
  68. data/spec/unit/pacto/file_pre_processor_spec.rb +0 -13
  69. data/spec/unit/pacto/instantiated_contract_spec.rb +0 -224
  70. 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
@@ -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
@@ -17,49 +17,70 @@ module Pacto
17
17
  end
18
18
 
19
19
  def install
20
- desc "Tasks for Pacto gem"
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 "Validates all contracts in a given directory against a given host"
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 "USAGE: rake pacto:validate[<host>, <contract_dir>]".colorize(:yellow)
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 validate_contracts(host, dir)
38
- WebMock.allow_net_connect!
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
- contracts = Dir[File.join(dir, '*{.json.erb,.json}')]
41
- if contracts.empty?
42
- fail "No contracts found in directory #{dir}".colorize(:yellow)
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
- contracts.sort.each do |contract_file|
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 " OK!".colorize(:green)
75
+ puts ' OK!'.colorize(:green)
55
76
  else
56
77
  @exit_with_error = true
57
78
  total_failed += 1
58
- puts " FAILED!".colorize(:red)
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
@@ -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' => @definition['status'],
10
- 'headers' => @definition['headers'],
11
- 'body' => JSON::Generator.generate(@definition['body'])
14
+ 'status' => @status,
15
+ 'headers' => @headers,
16
+ 'body' => JSON::Generator.generate(@schema)
12
17
  })
13
18
  end
14
19
 
15
- def validate(response)
16
- if @definition['status'] != response.status
17
- return [ "Invalid status: expected #{@definition['status']} but got #{response.status}" ]
18
- end
19
-
20
- unless @definition['headers'].normalize_keys.subset_of?(response.headers.normalize_keys)
21
- return [ "Invalid headers: expected #{@definition['headers'].inspect} to be a subset of #{response.headers.inspect}" ]
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 response.body
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 << "The response does not contain a body"
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
@@ -0,0 +1,2 @@
1
+ require_relative 'server/dummy'
2
+ require_relative 'server/playback_servlet'
@@ -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