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
@@ -1,6 +1,5 @@
1
1
  require 'net/http'
2
2
  require 'pact/reification'
3
- require 'pact/request'
4
3
  require 'pact/consumer_contract/interaction'
5
4
 
6
5
  module Pact
@@ -24,7 +23,7 @@ module Pact
24
23
  end
25
24
 
26
25
  def with(request_details)
27
- interaction.request = Request::Expected.from_hash(request_details)
26
+ interaction.request = Pact::Request::Expected.from_hash(request_details)
28
27
  self
29
28
  end
30
29
 
@@ -1,371 +1,2 @@
1
- require 'net/http'
2
- require 'uri'
3
- require 'json'
4
- require 'hashie'
5
- require 'singleton'
6
- require 'logger'
7
- require 'awesome_print'
8
- require 'awesome_print/core_ext/logger' #For some reason we get an error indicating that the method 'ap' is private unless we load this specifically
9
- require 'json/add/regexp'
10
- require 'pact/matchers'
1
+ require 'pact/consumer/mock_service/app'
11
2
 
12
- AwesomePrint.defaults = {
13
- indent: -2,
14
- plain: true,
15
- index: false
16
- }
17
-
18
- module Pact
19
- module Consumer
20
-
21
- class InteractionList
22
- #include Singleton
23
-
24
- attr_reader :interactions
25
- attr_reader :unexpected_requests
26
-
27
- def initialize
28
- clear
29
- end
30
-
31
- # For testing, sigh
32
- def clear
33
- @interactions = []
34
- @matched_interactions = []
35
- @unexpected_requests = []
36
- end
37
-
38
- def add interactions
39
- @interactions << interactions
40
- end
41
-
42
- def register_matched interaction
43
- @matched_interactions << interaction
44
- end
45
-
46
- # Request::Actual
47
- def register_unexpected request
48
- @unexpected_requests << request
49
- end
50
-
51
- def all_matched?
52
- interaction_diffs.empty?
53
- end
54
-
55
- def missing_interactions
56
- @interactions - @matched_interactions
57
- end
58
-
59
- def interaction_diffs
60
- {
61
- :missing_interactions => missing_interactions,
62
- :unexpected_requests => unexpected_requests.collect(&:as_json)
63
- }.inject({}) do | hash, pair |
64
- hash[pair.first] = pair.last if pair.last.any?
65
- hash
66
- end
67
- end
68
-
69
- end
70
-
71
- module RackHelper
72
- def params_hash env
73
- env["QUERY_STRING"].split("&").collect{| param| param.split("=")}.inject({}){|params, param| params[param.first] = URI.decode(param.last); params }
74
- end
75
- end
76
-
77
- class StartupPoll
78
-
79
- def initialize name, logger
80
- @name = name
81
- @logger = logger
82
- end
83
-
84
- def match? env
85
- env['REQUEST_PATH'] == '/index.html' &&
86
- env['REQUEST_METHOD'] == 'GET'
87
- end
88
-
89
- def respond env
90
- @logger.info "#{@name} started up"
91
- [200, {}, ['Started up fine']]
92
- end
93
- end
94
-
95
- class CapybaraIdentify
96
-
97
- def initialize name, logger
98
- @name = name
99
- @logger = logger
100
- end
101
-
102
- def match? env
103
- env["PATH_INFO"] == "/__identify__"
104
- end
105
-
106
- def respond env
107
- [200, {}, [object_id.to_s]]
108
- end
109
- end
110
-
111
- class InteractionDelete
112
-
113
- include RackHelper
114
-
115
- def initialize name, logger, interaction_list
116
- @name = name
117
- @logger = logger
118
- @interaction_list = interaction_list
119
- end
120
-
121
- def match? env
122
- env['REQUEST_PATH'].start_with?('/interactions') &&
123
- env['REQUEST_METHOD'] == 'DELETE'
124
- end
125
-
126
- def respond env
127
- @interaction_list.clear
128
- @logger.info "Cleared interactions before example \"#{params_hash(env)['example_description']}\""
129
- [200, {}, ['Deleted interactions']]
130
- end
131
- end
132
-
133
- class InteractionPost
134
-
135
- def initialize name, logger, interaction_list
136
- @name = name
137
- @logger = logger
138
- @interaction_list = interaction_list
139
- end
140
-
141
- def match? env
142
- env['REQUEST_PATH'] == '/interactions' &&
143
- env['REQUEST_METHOD'] == 'POST'
144
- end
145
-
146
- def respond env
147
- interactions = Hashie::Mash.new(JSON.load(env['rack.input'].string))
148
- @interaction_list.add interactions
149
- @logger.info "Added interaction to #{@name}"
150
- @logger.ap interactions
151
- [200, {}, ['Added interactions']]
152
- end
153
- end
154
-
155
- module RequestExtractor
156
-
157
- REQUEST_KEYS = Hashie::Mash.new({
158
- 'REQUEST_METHOD' => :method,
159
- 'REQUEST_PATH' => :path,
160
- 'QUERY_STRING' => :query,
161
- 'rack.input' => :body
162
- })
163
-
164
- def request_from env
165
- request = env.inject({}) do |memo, (k, v)|
166
- request_key = REQUEST_KEYS[k]
167
- memo[request_key] = v if request_key
168
- memo
169
- end
170
-
171
- mashed_request = Hashie::Mash.new request
172
- mashed_request[:headers] = headers_from env
173
- body_string = mashed_request[:body].read
174
-
175
- if body_string.empty?
176
- mashed_request.delete :body
177
- else
178
- body_is_json = mashed_request[:headers]['Content-Type'] =~ /json/
179
- mashed_request[:body] = body_is_json ? JSON.parse(body_string) : body_string
180
- end
181
- mashed_request[:method] = mashed_request[:method].downcase
182
- mashed_request
183
- end
184
-
185
- def headers_from env
186
- headers = env.reject{ |key, value| !(key.start_with?("HTTP") || key == 'CONTENT_TYPE')}
187
- headers.inject({}) do | hash, header |
188
- hash[standardise_header(header.first)] = header.last
189
- hash
190
- end
191
- end
192
-
193
- def standardise_header header
194
- header.gsub(/^HTTP_/, '').split("_").collect{|word| word[0] + word[1..-1].downcase}.join("-")
195
- end
196
- end
197
-
198
- class InteractionReplay
199
- include Pact::Matchers
200
- include RequestExtractor
201
-
202
- def initialize name, logger, interaction_list
203
- @name = name
204
- @logger = logger
205
- @interaction_list = interaction_list
206
- end
207
-
208
- def match? env
209
- true # default handler
210
- end
211
-
212
- def respond env
213
- find_response request_from(env)
214
- end
215
-
216
- private
217
-
218
- def find_response raw_request
219
- actual_request = Request::Actual.from_hash(raw_request)
220
- @logger.info "#{@name} received request"
221
- @logger.ap actual_request.as_json
222
- candidates = []
223
- matching_interactions = @interaction_list.interactions.select do |interaction|
224
- expected_request = Request::Expected.from_hash(interaction.request.merge(:description => interaction.description))
225
- candidates << expected_request if expected_request.matches_route? actual_request
226
- expected_request.match actual_request
227
- end
228
- if matching_interactions.size > 1
229
- @logger.error "Multiple interactions found on #{@name}:"
230
- @logger.ap matching_interactions
231
- raise "Multiple interactions found for path #{actual_request.path}!"
232
- end
233
- if matching_interactions.empty?
234
- handle_unrecognised_request(actual_request, candidates)
235
- else
236
- response = response_from(matching_interactions.first.response)
237
- @interaction_list.register_matched matching_interactions.first
238
- @logger.info "Found matching response on #{@name}:"
239
- @logger.ap response
240
- response
241
- end
242
- end
243
-
244
- def handle_unrecognised_request request, candidates
245
- @interaction_list.register_unexpected request
246
- @logger.error "No interaction found on #{@name} amongst expected requests \"#{candidates.map(&:description).join(', ')}\""
247
- @logger.error 'Interaction diffs for that route:'
248
- interaction_diff = candidates.map do |candidate|
249
- candidate.difference(request)
250
- end.to_a
251
- @logger.ap(interaction_diff, :error)
252
- response = {message: "No interaction found for #{request.path}", interaction_diff: interaction_diff}
253
- [500, {'Content-Type' => 'application/json'}, [response.to_json]]
254
- end
255
-
256
- def response_from response
257
- [response.status, (response.headers || {}).to_hash, [render_body(response.body)]]
258
- end
259
-
260
- def render_body body
261
- return '' unless body
262
- body.kind_of?(String) ? body.force_encoding('utf-8') : body.to_json
263
- end
264
-
265
- def logger_info_ap msg
266
- @logger.info msg
267
- end
268
- end
269
-
270
- class MissingInteractionsGet
271
- include RackHelper
272
-
273
- def initialize name, logger, interaction_list
274
- @name = name
275
- @logger = logger
276
- @interaction_list = interaction_list
277
- end
278
-
279
- def match? env
280
- env['REQUEST_PATH'].start_with?('/number_of_missing_interactions') &&
281
- env['REQUEST_METHOD'] == 'GET'
282
- end
283
-
284
- def respond env
285
- number_of_missing_interactions = @interaction_list.missing_interactions.size
286
- @logger.info "Number of missing interactions for mock \"#{@name}\" = #{number_of_missing_interactions}"
287
- [200, {}, ["#{number_of_missing_interactions}"]]
288
- end
289
-
290
- end
291
-
292
- class VerificationGet
293
-
294
- include RackHelper
295
-
296
- def initialize name, logger, log_description, interaction_list
297
- @name = name
298
- @logger = logger
299
- @log_description = log_description
300
- @interaction_list = interaction_list
301
- end
302
-
303
- def match? env
304
- env['REQUEST_PATH'].start_with?('/verify') &&
305
- env['REQUEST_METHOD'] == 'GET'
306
- end
307
-
308
- def respond env
309
- if @interaction_list.all_matched?
310
- @logger.info "Verifying - interactions matched for example \"#{example_description(env)}\""
311
- [200, {}, ['Interactions matched']]
312
- else
313
- @logger.warn "Verifying - actual interactions do not match expected interactions for example \"#{example_description(env)}\". Interaction diffs:"
314
- @logger.ap @interaction_list.interaction_diffs, :warn
315
- [500, {}, ["Actual interactions do not match expected interactions for mock #{@name}. See #{@log_description} for details."]]
316
- end
317
- end
318
-
319
- def example_description env
320
- params_hash(env)['example_description']
321
- end
322
- end
323
-
324
- class MockService
325
-
326
- def initialize options = {}
327
- options = {log_file: STDOUT}.merge options
328
- log_stream = options[:log_file]
329
- @logger = Logger.new log_stream
330
-
331
- log_description = if log_stream.is_a? File
332
- File.absolute_path(log_stream).gsub(Dir.pwd + "/", '')
333
- else
334
- "standard out/err"
335
- end
336
-
337
- interaction_list = InteractionList.new
338
-
339
- @name = options.fetch(:name, "MockService")
340
- @handlers = [
341
- StartupPoll.new(@name, @logger),
342
- CapybaraIdentify.new(@name, @logger),
343
- MissingInteractionsGet.new(@name, @logger, interaction_list),
344
- VerificationGet.new(@name, @logger, log_description, interaction_list),
345
- InteractionPost.new(@name, @logger, interaction_list),
346
- InteractionDelete.new(@name, @logger, interaction_list),
347
- InteractionReplay.new(@name, @logger, interaction_list)
348
- ]
349
- end
350
-
351
- def to_s
352
- "#{@name} #{super.to_s}"
353
- end
354
-
355
- def call env
356
- response = []
357
- begin
358
- relevant_handler = @handlers.detect { |handler| handler.match? env }
359
- response = relevant_handler.respond env
360
- rescue Exception => e
361
- @logger.ap 'Error ocurred in mock service:'
362
- @logger.ap e
363
- @logger.ap e.backtrace
364
- raise e
365
- end
366
- response
367
- end
368
-
369
- end
370
- end
371
- end
@@ -0,0 +1,70 @@
1
+ require 'uri'
2
+ require 'json'
3
+ require 'logger'
4
+ require 'awesome_print'
5
+ require 'awesome_print/core_ext/logger' #For some reason we get an error indicating that the method 'ap' is private unless we load this specifically
6
+ require 'pact/consumer/request'
7
+ require 'pact/consumer/mock_service/interaction_list'
8
+ require 'pact/consumer/mock_service/startup_poll'
9
+ require 'pact/consumer/mock_service/interaction_delete'
10
+ require 'pact/consumer/mock_service/interaction_post'
11
+ require 'pact/consumer/mock_service/interaction_replay'
12
+ require 'pact/consumer/mock_service/missing_interactions_get'
13
+ require 'pact/consumer/mock_service/verification_get'
14
+
15
+ AwesomePrint.defaults = {
16
+ indent: -2,
17
+ plain: true,
18
+ index: false
19
+ }
20
+
21
+ module Pact
22
+ module Consumer
23
+
24
+ class MockService
25
+
26
+ def initialize options = {}
27
+ options = {log_file: STDOUT}.merge options
28
+ log_stream = options[:log_file]
29
+ @logger = Logger.new log_stream
30
+
31
+ log_description = if log_stream.is_a? File
32
+ File.absolute_path(log_stream).gsub(Dir.pwd + "/", '')
33
+ else
34
+ "standard out/err"
35
+ end
36
+
37
+ interaction_list = InteractionList.new
38
+
39
+ @name = options.fetch(:name, "MockService")
40
+ @handlers = [
41
+ StartupPoll.new(@name, @logger),
42
+ MissingInteractionsGet.new(@name, @logger, interaction_list),
43
+ VerificationGet.new(@name, @logger, log_description, interaction_list),
44
+ InteractionPost.new(@name, @logger, interaction_list),
45
+ InteractionDelete.new(@name, @logger, interaction_list),
46
+ InteractionReplay.new(@name, @logger, interaction_list)
47
+ ]
48
+ end
49
+
50
+ def to_s
51
+ "#{@name} #{super.to_s}"
52
+ end
53
+
54
+ def call env
55
+ response = []
56
+ begin
57
+ relevant_handler = @handlers.detect { |handler| handler.match? env }
58
+ response = relevant_handler.respond env
59
+ rescue Exception => e
60
+ @logger.ap 'Error ocurred in mock service:'
61
+ @logger.ap e
62
+ @logger.ap e.backtrace
63
+ raise e
64
+ end
65
+ response
66
+ end
67
+
68
+ end
69
+ end
70
+ end