pact 0.1.28

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/.gitignore +28 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +5 -0
  4. data/Gemfile.lock +83 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +238 -0
  7. data/Rakefile +33 -0
  8. data/bin/pact +4 -0
  9. data/lib/pact/app.rb +32 -0
  10. data/lib/pact/configuration.rb +54 -0
  11. data/lib/pact/consumer/app_manager.rb +177 -0
  12. data/lib/pact/consumer/configuration_dsl.rb +71 -0
  13. data/lib/pact/consumer/consumer_contract_builder.rb +79 -0
  14. data/lib/pact/consumer/consumer_contract_builders.rb +10 -0
  15. data/lib/pact/consumer/dsl.rb +98 -0
  16. data/lib/pact/consumer/interaction.rb +70 -0
  17. data/lib/pact/consumer/mock_service.rb +340 -0
  18. data/lib/pact/consumer/rspec.rb +43 -0
  19. data/lib/pact/consumer/run_condor.rb +4 -0
  20. data/lib/pact/consumer/run_mock_contract_service.rb +13 -0
  21. data/lib/pact/consumer/service_consumer.rb +22 -0
  22. data/lib/pact/consumer/service_producer.rb +23 -0
  23. data/lib/pact/consumer.rb +7 -0
  24. data/lib/pact/consumer_contract.rb +110 -0
  25. data/lib/pact/json_warning.rb +23 -0
  26. data/lib/pact/logging.rb +14 -0
  27. data/lib/pact/matchers/matchers.rb +85 -0
  28. data/lib/pact/matchers.rb +1 -0
  29. data/lib/pact/producer/configuration_dsl.rb +62 -0
  30. data/lib/pact/producer/matchers.rb +22 -0
  31. data/lib/pact/producer/pact_spec_runner.rb +57 -0
  32. data/lib/pact/producer/producer_state.rb +81 -0
  33. data/lib/pact/producer/rspec.rb +127 -0
  34. data/lib/pact/producer/test_methods.rb +89 -0
  35. data/lib/pact/producer.rb +1 -0
  36. data/lib/pact/rake_task.rb +64 -0
  37. data/lib/pact/reification.rb +26 -0
  38. data/lib/pact/request.rb +109 -0
  39. data/lib/pact/term.rb +40 -0
  40. data/lib/pact/verification_task.rb +57 -0
  41. data/lib/pact/version.rb +3 -0
  42. data/lib/pact.rb +5 -0
  43. data/lib/tasks/pact.rake +6 -0
  44. data/pact.gemspec +36 -0
  45. data/scratchpad.txt +36 -0
  46. data/spec/features/consumption_spec.rb +146 -0
  47. data/spec/features/producer_states/zebras.rb +28 -0
  48. data/spec/features/production_spec.rb +160 -0
  49. data/spec/integration/pact/configuration_spec.rb +65 -0
  50. data/spec/lib/pact/configuration_spec.rb +35 -0
  51. data/spec/lib/pact/consumer/app_manager_spec.rb +41 -0
  52. data/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +87 -0
  53. data/spec/lib/pact/consumer/dsl_spec.rb +52 -0
  54. data/spec/lib/pact/consumer/interaction_spec.rb +108 -0
  55. data/spec/lib/pact/consumer/mock_service_spec.rb +147 -0
  56. data/spec/lib/pact/consumer/service_consumer_spec.rb +11 -0
  57. data/spec/lib/pact/consumer_contract_spec.rb +125 -0
  58. data/spec/lib/pact/matchers/matchers_spec.rb +354 -0
  59. data/spec/lib/pact/producer/configuration_dsl_spec.rb +101 -0
  60. data/spec/lib/pact/producer/producer_state_spec.rb +103 -0
  61. data/spec/lib/pact/producer/rspec_spec.rb +48 -0
  62. data/spec/lib/pact/reification_spec.rb +43 -0
  63. data/spec/lib/pact/request_spec.rb +316 -0
  64. data/spec/lib/pact/term_spec.rb +36 -0
  65. data/spec/lib/pact/verification_task_spec.rb +64 -0
  66. data/spec/spec_helper.rb +5 -0
  67. data/spec/support/a_consumer-a_producer.json +34 -0
  68. data/spec/support/pact_rake_support.rb +41 -0
  69. data/spec/support/test_app_fail.json +22 -0
  70. data/spec/support/test_app_pass.json +21 -0
  71. data/tasks/pact-test.rake +19 -0
  72. metadata +381 -0
@@ -0,0 +1,71 @@
1
+ require_relative 'service_consumer'
2
+ require_relative 'consumer_contract_builders'
3
+ require_relative '../configuration'
4
+
5
+ module Pact
6
+ module Consumer
7
+
8
+
9
+ module Configuration
10
+ def add_producer_verification &block
11
+ producer_verifications << block
12
+ end
13
+ def producer_verifications
14
+ @producer_verifications ||= []
15
+ end
16
+ end
17
+
18
+ module ConfigurationDSL
19
+
20
+ def consumer &block
21
+ if block_given?
22
+ @consumer = ConsumerDSL.new(&block).create_service_consumer
23
+ elsif @consumer
24
+ @consumer
25
+ else
26
+ raise "Please configure a consumer before configuring producers"
27
+ end
28
+ end
29
+
30
+ class ConsumerDSL
31
+
32
+ def initialize &block
33
+ @app = nil
34
+ @port = nil
35
+ @name = nil
36
+ instance_eval(&block)
37
+ end
38
+
39
+ def validate
40
+ raise "Please provide a consumer name" unless @name
41
+ raise "Please provide a port for the consumer app" if @app && !@port
42
+ end
43
+
44
+ def name name
45
+ @name = name
46
+ end
47
+
48
+ def app app
49
+ @app = app
50
+ end
51
+
52
+ def port port
53
+ @port = port
54
+ end
55
+
56
+ def create_service_consumer
57
+ validate
58
+ register_consumer_app if @app
59
+ Pact::Consumer::ServiceConsumer.new name: @name
60
+ end
61
+
62
+ def register_consumer_app
63
+ Pact::Consumer::AppManager.instance.register @app, @port
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ Pact::Configuration.send(:include, Pact::Consumer::ConfigurationDSL)
71
+ Pact::Configuration.send(:include, Pact::Consumer::Configuration)
@@ -0,0 +1,79 @@
1
+ require 'uri'
2
+ require 'json/add/regexp'
3
+ require 'pact/json_warning'
4
+ require 'pact/logging'
5
+
6
+ module Pact
7
+ module Consumer
8
+ class ConsumerContractBuilder
9
+
10
+ include Pact::JsonWarning
11
+ include Pact::Logging
12
+
13
+ attr_reader :uri
14
+ attr_reader :consumer_contract
15
+ attr_reader :pactfile_write_mode
16
+
17
+ def initialize(attributes)
18
+ @interactions = {}
19
+ @producer_state = nil
20
+ @pactfile_write_mode = attributes[:pactfile_write_mode]
21
+ @consumer_contract = Pact::ConsumerContract.new(
22
+ :consumer => ServiceConsumer.new(name: attributes[:consumer_name]),
23
+ :producer => ServiceProducer.new(name: attributes[:producer_name])
24
+ )
25
+ @uri = URI("http://localhost:#{attributes[:port]}")
26
+ @http = Net::HTTP.new(uri.host, uri.port)
27
+ if pactfile_write_mode == :update && File.exist?(consumer_contract.pactfile_path)
28
+ load_existing_interactions
29
+ end
30
+ end
31
+
32
+ def load_existing_interactions
33
+ json = File.read(consumer_contract.pactfile_path)
34
+ if json.size > 0
35
+ existing_consumer_contract = Pact::ConsumerContract.from_json json
36
+ existing_consumer_contract.interactions.each do | interaction |
37
+ @interactions["#{interaction.description} given #{interaction.producer_state}"] = interaction
38
+ end
39
+ consumer_contract.interactions = @interactions.values
40
+ end
41
+ end
42
+
43
+ def given(producer_state)
44
+ @producer_state = producer_state
45
+ self
46
+ end
47
+
48
+ def upon_receiving(description)
49
+ interaction_builder = InteractionBuilder.new(description, @producer_state)
50
+ producer = self
51
+ interaction_builder.on_interaction_fully_defined do | interaction |
52
+ producer.handle_interaction_fully_defined(interaction)
53
+ end
54
+ @interactions["#{description} given #{@producer_state}"] ||= interaction_builder.interaction
55
+ consumer_contract.interactions = @interactions.values
56
+ interaction_builder
57
+ end
58
+
59
+ def handle_interaction_fully_defined interaction
60
+ @http.request_post('/interactions', interaction.to_json_with_generated_response)
61
+ @producer_state = nil
62
+ consumer_contract.update_pactfile
63
+ end
64
+
65
+ def verify example_description
66
+ http = Net::HTTP.new(uri.host, uri.port)
67
+ response = http.request_get("/verify?example_description=#{URI.encode(example_description)}")
68
+ raise response.body unless response.is_a? Net::HTTPSuccess
69
+ end
70
+
71
+ private
72
+
73
+ def filenamify name
74
+ name.downcase.gsub(/\s/, '_')
75
+ end
76
+
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,10 @@
1
+ module Pact
2
+ module Consumer
3
+ module ConsumerContractBuilders
4
+ # Placeholder for mock service methods which will be dynamically created
5
+ # from the Pact configuration.
6
+
7
+ # To be included in RSpec
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,98 @@
1
+ require_relative 'consumer_contract_builders'
2
+
3
+ module Pact::Consumer
4
+ module DSL
5
+ def with_producer name, &block
6
+ Producer.new(name, &block).create_consumer_contract_builder
7
+ end
8
+
9
+ class Producer
10
+ def initialize name, &block
11
+ @name = name
12
+ @service = nil
13
+ instance_eval(&block)
14
+ end
15
+
16
+ def service name, &block
17
+ @service = Service.new(name, &block)
18
+ end
19
+
20
+ alias_method :mock_service, :service
21
+
22
+ def create_consumer_contract_builder
23
+ validate
24
+ consumer_contract_builder_from_attributes
25
+ end
26
+
27
+ def validate
28
+ raise "Please configure a service for #{@name}" unless @service
29
+ end
30
+
31
+ def consumer_contract_builder_from_attributes
32
+ consumer_contract_builder_fields = {
33
+ :consumer_name => Pact.configuration.consumer.name,
34
+ :producer_name => @name,
35
+ :pactfile_write_mode => Pact.configuration.pactfile_write_mode
36
+ }
37
+ @service.configure_consumer_contract_builder consumer_contract_builder_fields
38
+ end
39
+ end
40
+
41
+ class Service
42
+ def initialize name, &block
43
+ @name = name
44
+ @port = nil
45
+ @standalone = false
46
+ @verify = false
47
+ instance_eval(&block)
48
+ end
49
+
50
+ def port port
51
+ @port = port
52
+ end
53
+
54
+ def standalone standalone
55
+ @standalone = standalone
56
+ end
57
+
58
+ def verify verify
59
+ @verify = verify
60
+ end
61
+
62
+ def configure_consumer_contract_builder consumer_contract_builder_fields
63
+ validate
64
+ unless @standalone
65
+ AppManager.instance.register_mock_service_for consumer_contract_builder_fields[:producer_name], "http://localhost:#{@port}"
66
+ end
67
+ consumer_contract_builder = Pact::Consumer::ConsumerContractBuilder.new consumer_contract_builder_fields.merge({port: @port})
68
+ create_mock_services_module_method consumer_contract_builder
69
+ setup_verification(consumer_contract_builder) if @verify
70
+ consumer_contract_builder
71
+ end
72
+
73
+
74
+ def setup_verification consumer_contract_builder
75
+ Pact.configuration.add_producer_verification do | example_description |
76
+ consumer_contract_builder.verify example_description
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ # This makes the consumer_contract_builder available via a module method with the given identifier
83
+ # as the method name.
84
+ # I feel this should be defined somewhere else, but I'm not sure where
85
+ def create_mock_services_module_method consumer_contract_builder
86
+ Pact::Consumer::ConsumerContractBuilders.send(:define_method, @name.to_sym) do
87
+ consumer_contract_builder
88
+ end
89
+ end
90
+
91
+ def validate
92
+ raise "Please provide a port for service #{@name}" unless @port
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ Pact.send(:extend, Pact::Consumer::DSL)
@@ -0,0 +1,70 @@
1
+ require 'net/http'
2
+ require 'pact/reification'
3
+ require 'pact/request'
4
+ #require 'json/add/core'
5
+
6
+ module Pact
7
+ module Consumer
8
+
9
+ class Interaction
10
+
11
+ attr_accessor :description, :request, :response, :producer_state
12
+
13
+ def initialize options
14
+ @description = options[:description]
15
+ @request = options[:request]
16
+ @response = options[:response]
17
+ @producer_state = options[:producer_state]
18
+ end
19
+
20
+ def self.from_hash options
21
+ new(:description => options['description'],
22
+ :producer_state => options['producer_state'],
23
+ :request => Pact::Request::Expected.from_hash(options['request']),
24
+ :response => options['response']
25
+ )
26
+ end
27
+
28
+ def as_json
29
+ {
30
+ :description => @description,
31
+ :request => @request.as_json,
32
+ :response => @response,
33
+ }.tap{ | hash | hash[:producer_state] = @producer_state if @producer_state }
34
+ end
35
+
36
+ def to_json(options = {})
37
+ as_json.to_json(options)
38
+ end
39
+
40
+
41
+ def to_json_with_generated_response
42
+ as_json.tap { | hash | hash[:response] = Reification.from_term(response) }.to_json
43
+ end
44
+ end
45
+
46
+ class InteractionBuilder
47
+
48
+ attr_reader :interaction
49
+
50
+ def initialize(description, producer_state)
51
+ producer_state = producer_state.nil? ? nil : producer_state.to_s
52
+ @interaction = Interaction.new(:description => description, :producer_state => producer_state)
53
+ end
54
+
55
+ def with(request_details)
56
+ interaction.request = Request::Expected.from_hash(request_details)
57
+ self
58
+ end
59
+
60
+ def will_respond_with(response)
61
+ interaction.response = response
62
+ @callback.call interaction
63
+ end
64
+
65
+ def on_interaction_fully_defined &block
66
+ @callback = block
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,340 @@
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'
11
+
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
+ def register_unexpected request
47
+ @unexpected_requests << request
48
+ end
49
+
50
+ def all_matched?
51
+ interaction_diffs.empty?
52
+ end
53
+
54
+ def missing_interactions
55
+ @interactions - @matched_interactions
56
+ end
57
+
58
+ def interaction_diffs
59
+ {
60
+ :missing_interactions => missing_interactions,
61
+ :unexpected_requests => unexpected_requests
62
+ }.inject({}) do | hash, pair |
63
+ hash[pair.first] = pair.last if pair.last.any?
64
+ hash
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ module RackHelper
71
+ def params_hash env
72
+ env["QUERY_STRING"].split("&").collect{| param| param.split("=")}.inject({}){|params, param| params[param.first] = URI.decode(param.last); params }
73
+ end
74
+ end
75
+
76
+ class StartupPoll
77
+
78
+ def initialize name, logger
79
+ @name = name
80
+ @logger = logger
81
+ end
82
+
83
+ def match? env
84
+ env['REQUEST_PATH'] == '/index.html' &&
85
+ env['REQUEST_METHOD'] == 'GET'
86
+ end
87
+
88
+ def respond env
89
+ @logger.info "#{@name} started up"
90
+ [200, {}, ['Started up fine']]
91
+ end
92
+ end
93
+
94
+ class CapybaraIdentify
95
+
96
+ def initialize name, logger
97
+ @name = name
98
+ @logger = logger
99
+ end
100
+
101
+ def match? env
102
+ env["PATH_INFO"] == "/__identify__"
103
+ end
104
+
105
+ def respond env
106
+ [200, {}, [object_id.to_s]]
107
+ end
108
+ end
109
+
110
+ class InteractionDelete
111
+
112
+ include RackHelper
113
+
114
+ def initialize name, logger
115
+ @name = name
116
+ @logger = logger
117
+ end
118
+
119
+ def match? env
120
+ env['REQUEST_PATH'].start_with?('/interactions') &&
121
+ env['REQUEST_METHOD'] == 'DELETE'
122
+ end
123
+
124
+ def respond env
125
+ InteractionList.instance.clear
126
+ @logger.info "Cleared interactions before example \"#{params_hash(env)['example_description']}\""
127
+ [200, {}, ['Deleted interactions']]
128
+ end
129
+ end
130
+
131
+ class InteractionPost
132
+
133
+ def initialize name, logger
134
+ @name = name
135
+ @logger = logger
136
+ end
137
+
138
+ def match? env
139
+ env['REQUEST_PATH'] == '/interactions' &&
140
+ env['REQUEST_METHOD'] == 'POST'
141
+ end
142
+
143
+ def respond env
144
+ interactions = Hashie::Mash.new(JSON.load(env['rack.input'].string))
145
+ InteractionList.instance.add interactions
146
+ @logger.info "Added interaction to #{@name}"
147
+ @logger.ap interactions
148
+ [200, {}, ['Added interactions']]
149
+ end
150
+ end
151
+
152
+ module RequestExtractor
153
+
154
+ REQUEST_KEYS = Hashie::Mash.new({
155
+ 'REQUEST_METHOD' => :method,
156
+ 'REQUEST_PATH' => :path,
157
+ 'QUERY_STRING' => :query,
158
+ 'rack.input' => :body
159
+ })
160
+
161
+ def request_from env
162
+ request = env.inject({}) do |memo, (k, v)|
163
+ request_key = REQUEST_KEYS[k]
164
+ memo[request_key] = v if request_key
165
+ memo
166
+ end
167
+
168
+ mashed_request = Hashie::Mash.new request
169
+ mashed_request[:headers] = headers_from env
170
+ body_string = mashed_request[:body].read
171
+
172
+ if body_string.empty?
173
+ mashed_request.delete :body
174
+ else
175
+ body_is_json = mashed_request[:headers]['Content-Type'] =~ /json/
176
+ mashed_request[:body] = body_is_json ? JSON.parse(body_string) : body_string
177
+ end
178
+ mashed_request[:method] = mashed_request[:method].downcase
179
+ mashed_request
180
+ end
181
+
182
+ def headers_from env
183
+ headers = env.reject{ |key, value| !(key.start_with?("HTTP") || key == 'CONTENT_TYPE')}
184
+ headers.inject({}) do | hash, header |
185
+ hash[standardise_header(header.first)] = header.last
186
+ hash
187
+ end
188
+ end
189
+
190
+ def standardise_header header
191
+ header.gsub(/^HTTP_/, '').split("_").collect{|word| word[0] + word[1..-1].downcase}.join("-")
192
+ end
193
+ end
194
+
195
+ class InteractionReplay
196
+ include Pact::Matchers
197
+ include RequestExtractor
198
+
199
+ def initialize name, logger
200
+ @name = name
201
+ @logger = logger
202
+ end
203
+
204
+ def match? env
205
+ true # default handler
206
+ end
207
+
208
+ def respond env
209
+ find_response request_from(env)
210
+ end
211
+
212
+ private
213
+
214
+ def find_response raw_request
215
+ actual_request = Request::Actual.from_hash(raw_request)
216
+ @logger.info "#{@name} received request"
217
+ @logger.ap actual_request.as_json
218
+ candidates = []
219
+ matching_interactions = InteractionList.instance.interactions.select do |interaction|
220
+ expected_request = Request::Expected.from_hash(interaction.request.merge(:description => interaction.description))
221
+ candidates << expected_request if expected_request.matches_route? actual_request
222
+ expected_request.match actual_request
223
+ end
224
+ if matching_interactions.size > 1
225
+ @logger.info "Multiple interactions found on #{@name}:"
226
+ @logger.ap matching_interactions
227
+ raise 'Multiple interactions found!'
228
+ end
229
+ if matching_interactions.empty?
230
+ handle_unrecognised_request(actual_request, candidates)
231
+ else
232
+ response = response_from(matching_interactions.first.response)
233
+ InteractionList.instance.register_matched matching_interactions.first
234
+ @logger.info "Found matching response on #{@name}:"
235
+ @logger.ap response
236
+ response
237
+ end
238
+ end
239
+
240
+ def handle_unrecognised_request request, candidates
241
+ @logger.ap "No interaction found on #{@name} for request \"#{candidates.map(&:description).join(', ')}\""
242
+ @logger.ap 'Interaction diffs for that route:'
243
+ interaction_diff = candidates.map do |candidate|
244
+ diff(candidate.as_json, request.as_json)
245
+ end.to_a
246
+ @logger.ap(interaction_diff)
247
+ response = {message: "No interaction found for #{request.path}", interaction_diff: interaction_diff}
248
+ [500, {'Content-Type' => 'application/json'}, [response.to_json]]
249
+ end
250
+
251
+ def response_from response
252
+ [response.status, (response.headers || {}).to_hash, [render_body(response.body)]]
253
+ end
254
+
255
+ def render_body body
256
+ return '' unless body
257
+ body.kind_of?(String) ? body.force_encoding('utf-8') : body.to_json
258
+ end
259
+
260
+ def logger_info_ap msg
261
+ @logger.info msg
262
+ end
263
+ end
264
+
265
+ class VerificationGet
266
+
267
+ include RackHelper
268
+
269
+ def initialize name, logger, log_description
270
+ @name = name
271
+ @logger = logger
272
+ @log_description = log_description
273
+ end
274
+
275
+ def match? env
276
+ env['REQUEST_PATH'].start_with?('/verify') &&
277
+ env['REQUEST_METHOD'] == 'GET'
278
+ end
279
+
280
+ def respond env
281
+ if InteractionList.instance.all_matched?
282
+ @logger.info "Veryifying - interactions matched for example \"#{example_description(env)}\""
283
+ [200, {}, ['Interactions matched']]
284
+ else
285
+ @logger.warn "Verifying - actual interactions do not match expected interactions for example \"#{example_description(env)}\". Missing interactions:"
286
+ @logger.ap InteractionList.instance.interaction_diffs, :warn
287
+ [500, {}, ["Actual interactions do not match expected interactions for mock #{@name}. See #{@log_description} for details."]]
288
+ end
289
+ end
290
+
291
+ def example_description env
292
+ params_hash(env)['example_description']
293
+ end
294
+ end
295
+
296
+ class MockService
297
+
298
+ def initialize options = {}
299
+ options = {log_file: STDOUT}.merge options
300
+ log_stream = options[:log_file]
301
+ @logger = Logger.new log_stream
302
+
303
+ log_description = if log_stream.is_a? File
304
+ File.absolute_path(log_stream).gsub(Dir.pwd + "/", '')
305
+ else
306
+ "standard out/err"
307
+ end
308
+
309
+ @name = options.fetch(:name, "MockService")
310
+ @handlers = [
311
+ StartupPoll.new(@name, @logger),
312
+ CapybaraIdentify.new(@name, @logger),
313
+ VerificationGet.new(@name, @logger, log_description),
314
+ InteractionPost.new(@name, @logger),
315
+ InteractionDelete.new(@name, @logger),
316
+ InteractionReplay.new(@name, @logger)
317
+ ]
318
+ end
319
+
320
+ def to_s
321
+ "#{@name} #{super.to_s}"
322
+ end
323
+
324
+ def call env
325
+ response = []
326
+ begin
327
+ relevant_handler = @handlers.detect { |handler| handler.match? env }
328
+ response = relevant_handler.respond env
329
+ rescue Exception => e
330
+ @logger.ap 'Error ocurred in mock service:'
331
+ @logger.ap e
332
+ @logger.ap e.backtrace
333
+ raise e
334
+ end
335
+ response
336
+ end
337
+
338
+ end
339
+ end
340
+ end