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