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.
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