pact 1.0.9 → 1.0.10

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