pact 1.0.9 → 1.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/CHANGELOG.md +20 -0
  2. data/Gemfile.lock +3 -3
  3. data/example/animal-service/Gemfile +14 -0
  4. data/example/animal-service/Gemfile.lock +67 -0
  5. data/example/animal-service/Rakefile +3 -0
  6. data/example/animal-service/spec/service_consumers/pact_helper.rb +24 -0
  7. data/example/animal-service/spec/service_consumers/provider_states_for_zoo_app.rb +9 -0
  8. data/example/zoo-app/Gemfile.lock +2 -15
  9. data/example/zoo-app/spec/pacts/zoo_app-animal_service.json +21 -6
  10. data/example/zoo-app/spec/service_providers/animal_service_spec.rb +1 -1
  11. data/lib/pact/consumer.rb +10 -11
  12. data/lib/pact/consumer/app_manager.rb +1 -0
  13. data/lib/pact/consumer/configuration.rb +179 -0
  14. data/lib/pact/consumer/interaction_builder.rb +1 -2
  15. data/lib/pact/consumer/mock_service.rb +1 -370
  16. data/lib/pact/consumer/mock_service/app.rb +70 -0
  17. data/lib/pact/consumer/mock_service/interaction_delete.rb +28 -0
  18. data/lib/pact/consumer/mock_service/interaction_list.rb +57 -0
  19. data/lib/pact/consumer/mock_service/interaction_post.rb +25 -0
  20. data/lib/pact/consumer/mock_service/interaction_replay.rb +126 -0
  21. data/lib/pact/consumer/mock_service/missing_interactions_get.rb +26 -0
  22. data/lib/pact/consumer/mock_service/rack_request_helper.rb +51 -0
  23. data/lib/pact/consumer/mock_service/startup_poll.rb +22 -0
  24. data/lib/pact/consumer/mock_service/verification_get.rb +35 -0
  25. data/lib/pact/consumer/mock_service_client.rb +3 -1
  26. data/lib/pact/consumer/mock_service_interaction_expectation.rb +33 -0
  27. data/lib/pact/consumer/request.rb +27 -0
  28. data/lib/pact/consumer/rspec.rb +1 -4
  29. data/lib/pact/consumer_contract/consumer_contract.rb +11 -8
  30. data/lib/pact/consumer_contract/interaction.rb +9 -22
  31. data/lib/pact/consumer_contract/request.rb +68 -0
  32. data/lib/pact/consumer_contract/service_consumer.rb +6 -2
  33. data/lib/pact/consumer_contract/service_provider.rb +6 -2
  34. data/lib/pact/matchers/index_not_found.rb +20 -0
  35. data/lib/pact/matchers/matchers.rb +14 -28
  36. data/lib/pact/matchers/unexpected_index.rb +17 -0
  37. data/lib/pact/matchers/unexpected_key.rb +17 -0
  38. data/lib/pact/provider.rb +1 -1
  39. data/lib/pact/provider/configuration.rb +129 -0
  40. data/lib/pact/provider/request.rb +59 -0
  41. data/lib/pact/provider/rspec.rb +4 -5
  42. data/lib/pact/provider/test_methods.rb +14 -52
  43. data/lib/pact/shared/dsl.rb +20 -0
  44. data/lib/pact/shared/key_not_found.rb +24 -0
  45. data/lib/pact/shared/null_expectation.rb +31 -0
  46. data/lib/pact/shared/request.rb +68 -0
  47. data/lib/pact/something_like.rb +7 -1
  48. data/lib/pact/symbolize_keys.rb +12 -0
  49. data/lib/pact/tasks.rb +1 -1
  50. data/lib/pact/tasks/task_helper.rb +23 -0
  51. data/lib/pact/{verification_task.rb → tasks/verification_task.rb} +4 -4
  52. data/lib/pact/version.rb +1 -1
  53. data/lib/tasks/pact.rake +5 -4
  54. data/pact.gemspec +1 -1
  55. data/spec/features/consumption_spec.rb +1 -73
  56. data/spec/features/production_spec.rb +3 -0
  57. data/spec/integration/consumer_spec.rb +170 -0
  58. data/spec/integration/pact/consumer_configuration_spec.rb +1 -1
  59. data/spec/lib/pact/consumer/{dsl_spec.rb → configuration_spec.rb} +10 -9
  60. data/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +2 -2
  61. data/spec/lib/pact/consumer/interaction_builder_spec.rb +1 -1
  62. data/spec/lib/pact/consumer/mock_service/interaction_list_spec.rb +66 -0
  63. data/spec/lib/pact/consumer/mock_service/rack_request_helper_spec.rb +82 -0
  64. data/spec/lib/pact/consumer/mock_service_interaction_expectation_spec.rb +54 -0
  65. data/spec/lib/pact/consumer/request_spec.rb +24 -0
  66. data/spec/lib/pact/consumer_contract/consumer_contract_spec.rb +1 -1
  67. data/spec/lib/pact/consumer_contract/interaction_spec.rb +0 -34
  68. data/spec/lib/pact/{request_spec.rb → consumer_contract/request_spec.rb} +45 -121
  69. data/spec/lib/pact/matchers/matchers_spec.rb +29 -13
  70. data/spec/lib/pact/provider/configuration_spec.rb +163 -0
  71. data/spec/lib/pact/provider/request_spec.rb +78 -0
  72. data/spec/lib/pact/provider/test_methods_spec.rb +0 -30
  73. data/spec/lib/pact/verification_task_spec.rb +1 -1
  74. data/spec/spec_helper.rb +3 -0
  75. data/spec/support/factories.rb +5 -1
  76. data/spec/support/pact_helper.rb +6 -0
  77. data/spec/support/shared_examples_for_request.rb +83 -0
  78. data/spec/support/test_app_fail.json +3 -0
  79. data/spec/support/test_app_pass.json +18 -1
  80. metadata +68 -31
  81. data/lib/pact/consumer/dsl.rb +0 -157
  82. data/lib/pact/pact_task_helper.rb +0 -21
  83. data/lib/pact/provider/dsl.rb +0 -115
  84. data/lib/pact/request.rb +0 -167
  85. data/spec/lib/pact/consumer/mock_service_spec.rb +0 -143
  86. data/spec/lib/pact/provider/dsl_spec.rb +0 -179
@@ -0,0 +1,28 @@
1
+ require 'pact/consumer/mock_service/rack_request_helper'
2
+
3
+ module Pact
4
+ module Consumer
5
+
6
+ class InteractionDelete
7
+
8
+ include RackRequestHelper
9
+
10
+ def initialize name, logger, interaction_list
11
+ @name = name
12
+ @logger = logger
13
+ @interaction_list = interaction_list
14
+ end
15
+
16
+ def match? env
17
+ env['REQUEST_PATH'].start_with?('/interactions') &&
18
+ env['REQUEST_METHOD'] == 'DELETE'
19
+ end
20
+
21
+ def respond env
22
+ @interaction_list.clear
23
+ @logger.info "Cleared interactions before example \"#{params_hash(env)['example_description']}\""
24
+ [200, {}, ['Deleted interactions']]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ module Pact
2
+ module Consumer
3
+ class InteractionList
4
+
5
+ attr_reader :interactions
6
+ attr_reader :unexpected_requests
7
+
8
+ def initialize
9
+ clear
10
+ end
11
+
12
+ # For testing, sigh
13
+ def clear
14
+ @interactions = []
15
+ @matched_interactions = []
16
+ @unexpected_requests = []
17
+ end
18
+
19
+ def add interactions
20
+ @interactions << interactions
21
+ end
22
+
23
+ def register_matched interaction
24
+ @matched_interactions << interaction
25
+ end
26
+
27
+ def register_unexpected_request request
28
+ @unexpected_requests << request
29
+ end
30
+
31
+ def all_matched?
32
+ interaction_diffs.empty?
33
+ end
34
+
35
+ def missing_interactions
36
+ @interactions - @matched_interactions
37
+ end
38
+
39
+ def interaction_diffs
40
+ {
41
+ :missing_interactions => missing_interactions.collect(&:as_json),
42
+ :unexpected_requests => unexpected_requests.collect(&:as_json)
43
+ }.inject({}) do | hash, pair |
44
+ hash[pair.first] = pair.last if pair.last.any?
45
+ hash
46
+ end
47
+ end
48
+
49
+ def find_candidate_interactions actual_request
50
+ interactions.select do | interaction |
51
+ interaction.request.matches_route? actual_request
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,25 @@
1
+ module Pact
2
+ module Consumer
3
+ class InteractionPost
4
+
5
+ def initialize name, logger, interaction_list
6
+ @name = name
7
+ @logger = logger
8
+ @interaction_list = interaction_list
9
+ end
10
+
11
+ def match? env
12
+ env['REQUEST_PATH'] == '/interactions' &&
13
+ env['REQUEST_METHOD'] == 'POST'
14
+ end
15
+
16
+ def respond env
17
+ interaction = Interaction.from_hash(JSON.load(env['rack.input'].string))
18
+ @interaction_list.add interaction
19
+ @logger.info "Registered expected interaction #{interaction.request.method_and_path} for #{@name}"
20
+ @logger.ap interaction.as_json
21
+ [200, {}, ['Added interactions']]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,126 @@
1
+ require 'pact/matchers'
2
+ require 'pact/consumer/mock_service/rack_request_helper'
3
+
4
+ module Pact
5
+ module Consumer
6
+
7
+ class InteractionReplay
8
+ include Pact::Matchers
9
+ include RackRequestHelper
10
+
11
+ def initialize name, logger, interaction_list
12
+ @name = name
13
+ @logger = logger
14
+ @interaction_list = interaction_list
15
+ end
16
+
17
+ def match? env
18
+ true # default handler
19
+ end
20
+
21
+ def respond env
22
+ find_response request_as_hash_from(env)
23
+ end
24
+
25
+ private
26
+
27
+ def find_response request_hash
28
+ actual_request = Request::Actual.from_hash(request_hash)
29
+ @logger.info "#{@name} received request #{actual_request.method_and_path}"
30
+ @logger.ap actual_request.as_json
31
+ candidate_interactions = @interaction_list.find_candidate_interactions actual_request
32
+ matching_interactions = find_matching_interactions actual_request, from: candidate_interactions
33
+
34
+ case matching_interactions.size
35
+ when 0 then handle_unrecognised_request actual_request, candidate_interactions
36
+ when 1 then handle_matched_interaction matching_interactions.first
37
+ else
38
+ handle_more_than_one_matching_interaction actual_request, matching_interactions
39
+ end
40
+ end
41
+
42
+ def find_matching_interactions actual_request, opts
43
+ candidate_interactions = opts.fetch(:from)
44
+ candidate_interactions.select do | candidate_interaction |
45
+ candidate_interaction.request.matches? actual_request
46
+ end
47
+ end
48
+
49
+ def handle_matched_interaction interaction
50
+ @interaction_list.register_matched interaction
51
+ response = response_from(interaction.response)
52
+ @logger.info "Found matching response on #{@name}:"
53
+ @logger.ap interaction.response
54
+ response
55
+ end
56
+
57
+ def multiple_interactions_found_response actual_request, matching_interactions
58
+ response = {
59
+ message: "Multiple interaction found for #{actual_request.method_and_path}",
60
+ matching_interactions: matching_interactions.collect{ | interaction | request_summary_for(interaction) }
61
+ }
62
+ [500, {'Content-Type' => 'application/json'}, [response.to_json]]
63
+ end
64
+
65
+ def handle_more_than_one_matching_interaction actual_request, matching_interactions
66
+ @logger.error "Multiple interactions found on #{@name}:"
67
+ @logger.ap matching_interactions.collect(&:as_json)
68
+ multiple_interactions_found_response actual_request, matching_interactions
69
+ end
70
+
71
+ def interaction_diffs actual_request, candidate_interactions
72
+ candidate_interactions.collect do | candidate_interaction |
73
+ diff = candidate_interaction.request.difference(actual_request)
74
+ diff_summary_for candidate_interaction, diff
75
+ end
76
+ end
77
+
78
+ def diff_summary_for interaction, diff
79
+ summary = {:description => interaction.description}
80
+ summary[:provider_state] = interaction.provider_state if interaction.provider_state
81
+ summary.merge(diff)
82
+ end
83
+
84
+ def request_summary_for interaction
85
+ summary = {:description => interaction.description}
86
+ summary[:provider_state] if interaction.provider_state
87
+ summary[:request] = interaction.request
88
+ summary
89
+ end
90
+
91
+ def unrecognised_request_response actual_request, interaction_diffs
92
+ response = {
93
+ message: "No interaction found for #{actual_request.method_and_path}",
94
+ interaction_diffs: interaction_diffs
95
+ }
96
+ [500, {'Content-Type' => 'application/json'}, [response.to_json]]
97
+ end
98
+
99
+ def log_unrecognised_request_and_interaction_diff actual_request, interaction_diffs, candidate_interactions
100
+ @logger.error "No interaction found on #{@name} for #{actual_request.method_and_path}"
101
+ @logger.error 'Interaction diffs for that route:'
102
+ @logger.ap(interaction_diffs, :error)
103
+ end
104
+
105
+ def handle_unrecognised_request actual_request, candidate_interactions
106
+ @interaction_list.register_unexpected_request actual_request
107
+ interaction_diffs = interaction_diffs(actual_request, candidate_interactions)
108
+ log_unrecognised_request_and_interaction_diff actual_request, interaction_diffs, candidate_interactions
109
+ unrecognised_request_response actual_request, interaction_diffs
110
+ end
111
+
112
+ def response_from response
113
+ [response['status'], (response['headers'] || {}).to_hash, [render_body(response['body'])]]
114
+ end
115
+
116
+ def render_body body
117
+ return '' unless body
118
+ body.kind_of?(String) ? body.force_encoding('utf-8') : body.to_json
119
+ end
120
+
121
+ def logger_info_ap msg
122
+ @logger.info msg
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,26 @@
1
+ module Pact
2
+ module Consumer
3
+
4
+ class MissingInteractionsGet
5
+ include RackRequestHelper
6
+
7
+ def initialize name, logger, interaction_list
8
+ @name = name
9
+ @logger = logger
10
+ @interaction_list = interaction_list
11
+ end
12
+
13
+ def match? env
14
+ env['REQUEST_PATH'].start_with?('/number_of_missing_interactions') &&
15
+ env['REQUEST_METHOD'] == 'GET'
16
+ end
17
+
18
+ def respond env
19
+ number_of_missing_interactions = @interaction_list.missing_interactions.size
20
+ @logger.info "Number of missing interactions for mock \"#{@name}\" = #{number_of_missing_interactions}"
21
+ [200, {}, ["#{number_of_missing_interactions}"]]
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ module Pact
2
+ module Consumer
3
+
4
+ module RackRequestHelper
5
+ REQUEST_KEYS = {
6
+ 'REQUEST_METHOD' => :method,
7
+ 'REQUEST_PATH' => :path,
8
+ 'QUERY_STRING' => :query,
9
+ 'rack.input' => :body
10
+ }
11
+
12
+ def params_hash env
13
+ env["QUERY_STRING"].split("&").collect{| param| param.split("=")}.inject({}){|params, param| params[param.first] = URI.decode(param.last); params }
14
+ end
15
+
16
+ def request_as_hash_from env
17
+ request = env.inject({}) do |memo, (k, v)|
18
+ request_key = REQUEST_KEYS[k]
19
+ memo[request_key] = v if request_key
20
+ memo
21
+ end
22
+
23
+ request[:headers] = headers_from env
24
+ body_string = request[:body].read
25
+
26
+ if body_string.empty?
27
+ request.delete :body
28
+ else
29
+ body_is_json = request[:headers]['Content-Type'] =~ /json/
30
+ request[:body] = body_is_json ? JSON.parse(body_string) : body_string
31
+ end
32
+ request[:method] = request[:method].downcase
33
+ request
34
+ end
35
+
36
+ private
37
+
38
+ def headers_from env
39
+ headers = env.reject{ |key, value| !(key.start_with?("HTTP") || key == 'CONTENT_TYPE')}
40
+ headers.inject({}) do | hash, header |
41
+ hash[standardise_header(header.first)] = header.last
42
+ hash
43
+ end
44
+ end
45
+
46
+ def standardise_header header
47
+ header.gsub(/^HTTP_/, '').split("_").collect{|word| word[0] + word[1..-1].downcase}.join("-")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,22 @@
1
+ module Pact
2
+ module Consumer
3
+
4
+ class StartupPoll
5
+
6
+ def initialize name, logger
7
+ @name = name
8
+ @logger = logger
9
+ end
10
+
11
+ def match? env
12
+ env['REQUEST_PATH'] == '/index.html' &&
13
+ env['REQUEST_METHOD'] == 'GET'
14
+ end
15
+
16
+ def respond env
17
+ @logger.info "#{@name} started up"
18
+ [200, {}, ['Started up fine']]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ module Pact
2
+ module Consumer
3
+ class VerificationGet
4
+
5
+ include RackRequestHelper
6
+
7
+ def initialize name, logger, log_description, interaction_list
8
+ @name = name
9
+ @logger = logger
10
+ @log_description = log_description
11
+ @interaction_list = interaction_list
12
+ end
13
+
14
+ def match? env
15
+ env['REQUEST_PATH'].start_with?('/verify') &&
16
+ env['REQUEST_METHOD'] == 'GET'
17
+ end
18
+
19
+ def respond env
20
+ if @interaction_list.all_matched?
21
+ @logger.info "Verifying - interactions matched for example \"#{example_description(env)}\""
22
+ [200, {}, ['Interactions matched']]
23
+ else
24
+ @logger.warn "Verifying - actual interactions do not match expected interactions for example \"#{example_description(env)}\". Interaction diffs:"
25
+ @logger.ap @interaction_list.interaction_diffs, :warn
26
+ [500, {}, ["Actual interactions do not match expected interactions for mock #{@name}. See #{@log_description} for details."]]
27
+ end
28
+ end
29
+
30
+ def example_description env
31
+ params_hash(env)['example_description']
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,5 @@
1
+ require 'pact/consumer/mock_service_interaction_expectation'
2
+
1
3
  module Pact
2
4
  module Consumer
3
5
  class MockServiceClient
@@ -22,7 +24,7 @@ module Pact
22
24
  end
23
25
 
24
26
  def add_expected_interaction interaction
25
- http.request_post('/interactions', interaction.to_json_for_mock_service)
27
+ http.request_post('/interactions', MockServiceInteractionExpectation.new(interaction).to_json)
26
28
  end
27
29
 
28
30
  def self.clear_interactions port, example_description
@@ -0,0 +1,33 @@
1
+ require 'pact/reification'
2
+
3
+ # Represents the Interaction in the form required by the MockService
4
+ # The json generated will be posted to the MockService to register the expectation
5
+ module Pact
6
+ module Consumer
7
+ class MockServiceInteractionExpectation
8
+
9
+
10
+ def initialize interaction
11
+ @interaction = interaction
12
+ end
13
+
14
+ def as_json
15
+ hash = {:description => interaction.description}
16
+ hash[:provider_state] = interaction.provider_state if interaction.provider_state
17
+ options = interaction.request.options.empty? ? {} : { options: interaction.request.options}
18
+ hash[:request] = interaction.request.as_json.merge(options)
19
+ hash[:response] = Reification.from_term(interaction.response)
20
+ hash
21
+ end
22
+
23
+ def to_json opts = {}
24
+ as_json.to_json(opts)
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :interaction
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ require 'pact/shared/request'
2
+ require 'pact/shared/key_not_found'
3
+
4
+ module Pact
5
+ module Consumer
6
+ module Request
7
+ class Actual < Pact::Request::Base
8
+
9
+ def self.from_hash(hash)
10
+ sym_hash = symbolize_keys hash
11
+ method = sym_hash.fetch(:method)
12
+ path = sym_hash.fetch(:path)
13
+ query = sym_hash.fetch(:query)
14
+ headers = sym_hash.fetch(:headers)
15
+ body = sym_hash.fetch(:body, nil)
16
+ new(method, path, headers, body, query)
17
+ end
18
+
19
+ protected
20
+
21
+ def self.key_not_found
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end