pact 0.1.28
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.
- data/.gitignore +28 -0
- data/.rspec +2 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +83 -0
- data/LICENSE.txt +22 -0
- data/README.md +238 -0
- data/Rakefile +33 -0
- data/bin/pact +4 -0
- data/lib/pact/app.rb +32 -0
- data/lib/pact/configuration.rb +54 -0
- data/lib/pact/consumer/app_manager.rb +177 -0
- data/lib/pact/consumer/configuration_dsl.rb +71 -0
- data/lib/pact/consumer/consumer_contract_builder.rb +79 -0
- data/lib/pact/consumer/consumer_contract_builders.rb +10 -0
- data/lib/pact/consumer/dsl.rb +98 -0
- data/lib/pact/consumer/interaction.rb +70 -0
- data/lib/pact/consumer/mock_service.rb +340 -0
- data/lib/pact/consumer/rspec.rb +43 -0
- data/lib/pact/consumer/run_condor.rb +4 -0
- data/lib/pact/consumer/run_mock_contract_service.rb +13 -0
- data/lib/pact/consumer/service_consumer.rb +22 -0
- data/lib/pact/consumer/service_producer.rb +23 -0
- data/lib/pact/consumer.rb +7 -0
- data/lib/pact/consumer_contract.rb +110 -0
- data/lib/pact/json_warning.rb +23 -0
- data/lib/pact/logging.rb +14 -0
- data/lib/pact/matchers/matchers.rb +85 -0
- data/lib/pact/matchers.rb +1 -0
- data/lib/pact/producer/configuration_dsl.rb +62 -0
- data/lib/pact/producer/matchers.rb +22 -0
- data/lib/pact/producer/pact_spec_runner.rb +57 -0
- data/lib/pact/producer/producer_state.rb +81 -0
- data/lib/pact/producer/rspec.rb +127 -0
- data/lib/pact/producer/test_methods.rb +89 -0
- data/lib/pact/producer.rb +1 -0
- data/lib/pact/rake_task.rb +64 -0
- data/lib/pact/reification.rb +26 -0
- data/lib/pact/request.rb +109 -0
- data/lib/pact/term.rb +40 -0
- data/lib/pact/verification_task.rb +57 -0
- data/lib/pact/version.rb +3 -0
- data/lib/pact.rb +5 -0
- data/lib/tasks/pact.rake +6 -0
- data/pact.gemspec +36 -0
- data/scratchpad.txt +36 -0
- data/spec/features/consumption_spec.rb +146 -0
- data/spec/features/producer_states/zebras.rb +28 -0
- data/spec/features/production_spec.rb +160 -0
- data/spec/integration/pact/configuration_spec.rb +65 -0
- data/spec/lib/pact/configuration_spec.rb +35 -0
- data/spec/lib/pact/consumer/app_manager_spec.rb +41 -0
- data/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +87 -0
- data/spec/lib/pact/consumer/dsl_spec.rb +52 -0
- data/spec/lib/pact/consumer/interaction_spec.rb +108 -0
- data/spec/lib/pact/consumer/mock_service_spec.rb +147 -0
- data/spec/lib/pact/consumer/service_consumer_spec.rb +11 -0
- data/spec/lib/pact/consumer_contract_spec.rb +125 -0
- data/spec/lib/pact/matchers/matchers_spec.rb +354 -0
- data/spec/lib/pact/producer/configuration_dsl_spec.rb +101 -0
- data/spec/lib/pact/producer/producer_state_spec.rb +103 -0
- data/spec/lib/pact/producer/rspec_spec.rb +48 -0
- data/spec/lib/pact/reification_spec.rb +43 -0
- data/spec/lib/pact/request_spec.rb +316 -0
- data/spec/lib/pact/term_spec.rb +36 -0
- data/spec/lib/pact/verification_task_spec.rb +64 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/support/a_consumer-a_producer.json +34 -0
- data/spec/support/pact_rake_support.rb +41 -0
- data/spec/support/test_app_fail.json +22 -0
- data/spec/support/test_app_pass.json +21 -0
- data/tasks/pact-test.rake +19 -0
- 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,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
|