pact 0.1.28 → 0.1.35
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +5 -5
- data/README.md +52 -53
- data/example/zoo-app/Gemfile +15 -0
- data/example/zoo-app/Gemfile.lock +77 -0
- data/example/zoo-app/lib/zoo_app/animal_service_client.rb +36 -0
- data/example/zoo-app/lib/zoo_app/models/alligator.rb +15 -0
- data/example/zoo-app/spec/pacts/zoo_app-animal_service.json +100 -0
- data/example/zoo-app/spec/service_providers/animal_service_spec.rb +92 -0
- data/example/zoo-app/spec/service_providers/pact_helper.rb +11 -0
- data/example/zoo-app/spec/spec_helper.rb +8 -0
- data/lib/pact/consumer/configuration_dsl.rb +2 -0
- data/lib/pact/consumer/consumer_contract_builder.rb +13 -12
- data/lib/pact/consumer/dsl.rb +51 -2
- data/lib/pact/consumer/interaction.rb +15 -12
- data/lib/pact/consumer/mock_service.rb +31 -9
- data/lib/pact/consumer/mock_service_client.rb +47 -0
- data/lib/pact/consumer/rspec.rb +2 -1
- data/lib/pact/consumer_contract.rb +2 -1
- data/lib/pact/matchers/matchers.rb +59 -18
- data/lib/pact/producer/dsl.rb +61 -0
- data/lib/pact/producer/producer_state.rb +7 -0
- data/lib/pact/producer/rspec.rb +2 -0
- data/lib/pact/provider/rspec.rb +1 -0
- data/lib/pact/request.rb +42 -6
- data/lib/pact/verification_task.rb +2 -5
- data/lib/pact/version.rb +1 -1
- data/spec/features/consumption_spec.rb +56 -18
- data/spec/features/production_spec.rb +7 -16
- data/spec/integration/pact/consumer_configuration_spec.rb +144 -0
- data/spec/integration/pact/provider_configuration_spec.rb +24 -0
- data/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +13 -13
- data/spec/lib/pact/consumer/dsl_spec.rb +1 -0
- data/spec/lib/pact/consumer/interaction_spec.rb +34 -0
- data/spec/lib/pact/consumer_contract_spec.rb +13 -2
- data/spec/lib/pact/matchers/matchers_spec.rb +32 -12
- data/spec/lib/pact/producer/rspec_spec.rb +1 -1
- data/spec/lib/pact/request_spec.rb +43 -1
- data/spec/support/pact_rake_support.rb +5 -8
- metadata +17 -10
- data/spec/integration/pact/configuration_spec.rb +0 -65
@@ -1,30 +1,27 @@
|
|
1
1
|
require 'uri'
|
2
2
|
require 'json/add/regexp'
|
3
|
-
require 'pact/json_warning'
|
4
3
|
require 'pact/logging'
|
4
|
+
require 'pact/consumer/mock_service_client'
|
5
5
|
|
6
6
|
module Pact
|
7
7
|
module Consumer
|
8
|
+
|
8
9
|
class ConsumerContractBuilder
|
9
10
|
|
10
|
-
include Pact::JsonWarning
|
11
11
|
include Pact::Logging
|
12
12
|
|
13
|
-
attr_reader :uri
|
14
13
|
attr_reader :consumer_contract
|
15
|
-
attr_reader :
|
14
|
+
attr_reader :mock_service_client
|
16
15
|
|
17
16
|
def initialize(attributes)
|
18
17
|
@interactions = {}
|
19
18
|
@producer_state = nil
|
20
|
-
@pactfile_write_mode = attributes[:pactfile_write_mode]
|
21
19
|
@consumer_contract = Pact::ConsumerContract.new(
|
22
20
|
:consumer => ServiceConsumer.new(name: attributes[:consumer_name]),
|
23
21
|
:producer => ServiceProducer.new(name: attributes[:producer_name])
|
24
22
|
)
|
25
|
-
@
|
26
|
-
|
27
|
-
if pactfile_write_mode == :update && File.exist?(consumer_contract.pactfile_path)
|
23
|
+
@mock_service_client = MockServiceClient.new(attributes[:producer_name], attributes[:port])
|
24
|
+
if attributes[:pactfile_write_mode] == :update && File.exist?(consumer_contract.pactfile_path)
|
28
25
|
load_existing_interactions
|
29
26
|
end
|
30
27
|
end
|
@@ -57,15 +54,19 @@ module Pact
|
|
57
54
|
end
|
58
55
|
|
59
56
|
def handle_interaction_fully_defined interaction
|
60
|
-
|
57
|
+
mock_service_client.add_expected_interaction interaction
|
61
58
|
@producer_state = nil
|
62
59
|
consumer_contract.update_pactfile
|
63
60
|
end
|
64
61
|
|
65
62
|
def verify example_description
|
66
|
-
|
67
|
-
|
68
|
-
|
63
|
+
mock_service_client.verify example_description
|
64
|
+
end
|
65
|
+
|
66
|
+
def wait_for_interactions options
|
67
|
+
wait_max_seconds = options.fetch(:wait_max_seconds, 3)
|
68
|
+
poll_interval = options.fetch(:poll_interval, 0.1)
|
69
|
+
mock_service_client.wait_for_interactions wait_max_seconds, poll_interval
|
69
70
|
end
|
70
71
|
|
71
72
|
private
|
data/lib/pact/consumer/dsl.rb
CHANGED
@@ -2,14 +2,63 @@ require_relative 'consumer_contract_builders'
|
|
2
2
|
|
3
3
|
module Pact::Consumer
|
4
4
|
module DSL
|
5
|
+
|
6
|
+
def service_consumer name, &block
|
7
|
+
ServiceConsumerDSL.new(name, &block).create_service_consumer
|
8
|
+
end
|
9
|
+
|
10
|
+
class ServiceConsumerDSL
|
11
|
+
|
12
|
+
def initialize name, &block
|
13
|
+
@name = name
|
14
|
+
consumer = Pact::Consumer::ServiceConsumer.new name: @name
|
15
|
+
@app = nil
|
16
|
+
@port = nil
|
17
|
+
instance_eval(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate
|
21
|
+
raise "Please provide a consumer name" unless (@name && !@name.empty?)
|
22
|
+
raise "Please provide a port for the consumer app" if @app && !@port
|
23
|
+
end
|
24
|
+
|
25
|
+
def app app
|
26
|
+
@app = app
|
27
|
+
end
|
28
|
+
|
29
|
+
def port port
|
30
|
+
@port = port
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_pact_with service_provider_name, &block
|
34
|
+
Producer.new(service_provider_name, @name, &block).create_consumer_contract_builder
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_service_consumer
|
38
|
+
validate
|
39
|
+
register_consumer_app if @app
|
40
|
+
end
|
41
|
+
|
42
|
+
def register_consumer_app
|
43
|
+
Pact::Consumer::AppManager.instance.register @app, @port
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
#OLD ####
|
5
51
|
def with_producer name, &block
|
6
52
|
Producer.new(name, &block).create_consumer_contract_builder
|
7
53
|
end
|
8
54
|
|
55
|
+
alias_method :with_service_provider, :with_producer
|
56
|
+
|
9
57
|
class Producer
|
10
|
-
def initialize name, &block
|
58
|
+
def initialize name, consumer_name = Pact.configuration.consumer.name, &block
|
11
59
|
@name = name
|
12
60
|
@service = nil
|
61
|
+
@consumer_name = consumer_name
|
13
62
|
instance_eval(&block)
|
14
63
|
end
|
15
64
|
|
@@ -30,7 +79,7 @@ module Pact::Consumer
|
|
30
79
|
|
31
80
|
def consumer_contract_builder_from_attributes
|
32
81
|
consumer_contract_builder_fields = {
|
33
|
-
:consumer_name =>
|
82
|
+
:consumer_name => @consumer_name,
|
34
83
|
:producer_name => @name,
|
35
84
|
:pactfile_write_mode => Pact.configuration.pactfile_write_mode
|
36
85
|
}
|
@@ -10,18 +10,18 @@ module Pact
|
|
10
10
|
|
11
11
|
attr_accessor :description, :request, :response, :producer_state
|
12
12
|
|
13
|
-
def initialize
|
14
|
-
@description =
|
15
|
-
@request =
|
16
|
-
@response =
|
17
|
-
@producer_state =
|
13
|
+
def initialize attributes
|
14
|
+
@description = attributes[:description]
|
15
|
+
@request = attributes[:request]
|
16
|
+
@response = attributes[:response]
|
17
|
+
@producer_state = attributes[:producer_state]
|
18
18
|
end
|
19
19
|
|
20
|
-
def self.from_hash
|
21
|
-
new(:description =>
|
22
|
-
:producer_state =>
|
23
|
-
:request => Pact::Request::Expected.from_hash(
|
24
|
-
:response =>
|
20
|
+
def self.from_hash hash
|
21
|
+
new(:description => hash['description'],
|
22
|
+
:producer_state => hash['producer_state'],
|
23
|
+
:request => Pact::Request::Expected.from_hash(hash['request']),
|
24
|
+
:response => hash['response']
|
25
25
|
)
|
26
26
|
end
|
27
27
|
|
@@ -37,9 +37,12 @@ module Pact
|
|
37
37
|
as_json.to_json(options)
|
38
38
|
end
|
39
39
|
|
40
|
+
def as_json_for_mock_service
|
41
|
+
{:response => Reification.from_term(response), :request => @request.as_json_with_options, :description => description }
|
42
|
+
end
|
40
43
|
|
41
|
-
def
|
42
|
-
|
44
|
+
def to_json_for_mock_service
|
45
|
+
as_json_for_mock_service.to_json
|
43
46
|
end
|
44
47
|
end
|
45
48
|
|
@@ -56,7 +56,7 @@ module Pact
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def interaction_diffs
|
59
|
-
{
|
59
|
+
{
|
60
60
|
:missing_interactions => missing_interactions,
|
61
61
|
:unexpected_requests => unexpected_requests
|
62
62
|
}.inject({}) do | hash, pair |
|
@@ -222,9 +222,9 @@ module Pact
|
|
222
222
|
expected_request.match actual_request
|
223
223
|
end
|
224
224
|
if matching_interactions.size > 1
|
225
|
-
@logger.
|
225
|
+
@logger.error "Multiple interactions found on #{@name}:"
|
226
226
|
@logger.ap matching_interactions
|
227
|
-
raise
|
227
|
+
raise "Multiple interactions found for path #{actual_request.path}!"
|
228
228
|
end
|
229
229
|
if matching_interactions.empty?
|
230
230
|
handle_unrecognised_request(actual_request, candidates)
|
@@ -238,12 +238,12 @@ module Pact
|
|
238
238
|
end
|
239
239
|
|
240
240
|
def handle_unrecognised_request request, candidates
|
241
|
-
@logger.
|
242
|
-
@logger.
|
241
|
+
@logger.error "No interaction found on #{@name} amongst expected requests \"#{candidates.map(&:description).join(', ')}\""
|
242
|
+
@logger.error 'Interaction diffs for that route:'
|
243
243
|
interaction_diff = candidates.map do |candidate|
|
244
|
-
|
244
|
+
candidate.difference(request)
|
245
245
|
end.to_a
|
246
|
-
@logger.ap(interaction_diff)
|
246
|
+
@logger.ap(interaction_diff, :error)
|
247
247
|
response = {message: "No interaction found for #{request.path}", interaction_diff: interaction_diff}
|
248
248
|
[500, {'Content-Type' => 'application/json'}, [response.to_json]]
|
249
249
|
end
|
@@ -262,6 +262,27 @@ module Pact
|
|
262
262
|
end
|
263
263
|
end
|
264
264
|
|
265
|
+
class MissingInteractionsGet
|
266
|
+
include RackHelper
|
267
|
+
|
268
|
+
def initialize name, logger
|
269
|
+
@name = name
|
270
|
+
@logger = logger
|
271
|
+
end
|
272
|
+
|
273
|
+
def match? env
|
274
|
+
env['REQUEST_PATH'].start_with?('/number_of_missing_interactions') &&
|
275
|
+
env['REQUEST_METHOD'] == 'GET'
|
276
|
+
end
|
277
|
+
|
278
|
+
def respond env
|
279
|
+
number_of_missing_interactions = InteractionList.instance.missing_interactions.size
|
280
|
+
@logger.info "Number of missing interactions for mock \"#{@name}\" = #{number_of_missing_interactions}"
|
281
|
+
[200, {}, ["#{number_of_missing_interactions}"]]
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
285
|
+
|
265
286
|
class VerificationGet
|
266
287
|
|
267
288
|
include RackHelper
|
@@ -279,10 +300,10 @@ module Pact
|
|
279
300
|
|
280
301
|
def respond env
|
281
302
|
if InteractionList.instance.all_matched?
|
282
|
-
@logger.info "
|
303
|
+
@logger.info "Verifying - interactions matched for example \"#{example_description(env)}\""
|
283
304
|
[200, {}, ['Interactions matched']]
|
284
305
|
else
|
285
|
-
@logger.warn "Verifying - actual interactions do not match expected interactions for example \"#{example_description(env)}\".
|
306
|
+
@logger.warn "Verifying - actual interactions do not match expected interactions for example \"#{example_description(env)}\". Interaction diffs:"
|
286
307
|
@logger.ap InteractionList.instance.interaction_diffs, :warn
|
287
308
|
[500, {}, ["Actual interactions do not match expected interactions for mock #{@name}. See #{@log_description} for details."]]
|
288
309
|
end
|
@@ -310,6 +331,7 @@ module Pact
|
|
310
331
|
@handlers = [
|
311
332
|
StartupPoll.new(@name, @logger),
|
312
333
|
CapybaraIdentify.new(@name, @logger),
|
334
|
+
MissingInteractionsGet.new(@name, @logger),
|
313
335
|
VerificationGet.new(@name, @logger, log_description),
|
314
336
|
InteractionPost.new(@name, @logger),
|
315
337
|
InteractionDelete.new(@name, @logger),
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Pact
|
2
|
+
module Consumer
|
3
|
+
class MockServiceClient
|
4
|
+
def initialize name, port
|
5
|
+
@http = Net::HTTP.new('localhost', port)
|
6
|
+
end
|
7
|
+
|
8
|
+
def verify example_description
|
9
|
+
response = http.request_get("/verify?example_description=#{URI.encode(example_description)}")
|
10
|
+
raise response.body unless response.is_a? Net::HTTPSuccess
|
11
|
+
end
|
12
|
+
|
13
|
+
def wait_for_interactions wait_max_seconds, poll_interval
|
14
|
+
wait_until_true(wait_max_seconds, poll_interval) do
|
15
|
+
response = http.request_get("/number_of_missing_interactions")
|
16
|
+
response.body == '0'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear_interactions example_description
|
21
|
+
http.delete("/interactions?example_description=#{URI.encode(example_description)}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_expected_interaction interaction
|
25
|
+
http.request_post('/interactions', interaction.to_json_for_mock_service)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.clear_interactions port, example_description
|
29
|
+
Net::HTTP.new("localhost", port).delete("/interactions?example_description=#{URI.encode(example_description)}")
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
attr_reader :http
|
34
|
+
|
35
|
+
#todo: in need a better home (where can we move it?)
|
36
|
+
def wait_until_true timeout=3, interval=0.1
|
37
|
+
time_limit = Time.now + timeout
|
38
|
+
loop do
|
39
|
+
result = yield
|
40
|
+
return if result || Time.now >= time_limit
|
41
|
+
sleep interval
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/pact/consumer/rspec.rb
CHANGED
@@ -2,6 +2,7 @@ require_relative '../configuration'
|
|
2
2
|
require_relative '../consumer'
|
3
3
|
require_relative 'dsl'
|
4
4
|
require_relative 'configuration_dsl'
|
5
|
+
require 'pact/consumer/consumer_contract_builder'
|
5
6
|
|
6
7
|
module Pact
|
7
8
|
module Consumer
|
@@ -23,7 +24,7 @@ RSpec.configure do |config|
|
|
23
24
|
example_description = "#{example.example.example_group.description} #{example.example.description}"
|
24
25
|
Pact.configuration.logger.info "Clearing all expectations"
|
25
26
|
Pact::Consumer::AppManager.instance.ports_of_mock_services.each do | port |
|
26
|
-
|
27
|
+
Pact::Consumer::MockServiceClient.clear_interactions port, example_description
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
@@ -4,12 +4,13 @@ require 'pact/consumer/interaction'
|
|
4
4
|
require 'pact/logging'
|
5
5
|
require 'pact/json_warning'
|
6
6
|
require 'date'
|
7
|
+
require 'pact/version'
|
7
8
|
|
8
9
|
module Pact
|
9
10
|
class ConsumerContract
|
10
11
|
|
11
12
|
include Pact::Logging
|
12
|
-
include Pact::JsonWarning
|
13
|
+
include Pact::JsonWarning
|
13
14
|
|
14
15
|
attr_accessor :interactions
|
15
16
|
attr_accessor :consumer
|
@@ -5,8 +5,29 @@ module Pact
|
|
5
5
|
module Matchers
|
6
6
|
|
7
7
|
NO_DIFF_INDICATOR = 'no difference here!'
|
8
|
+
UNEXPECTED_KEY = '<key not to be present>'
|
9
|
+
DEFAULT_OPTIONS = {allow_unexpected_keys: true, structure: false}.freeze
|
8
10
|
|
9
|
-
|
11
|
+
class KeyNotFound
|
12
|
+
def == other
|
13
|
+
other.is_a? KeyNotFound
|
14
|
+
end
|
15
|
+
|
16
|
+
def eql? other
|
17
|
+
self == other
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"<key not found>"
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_json options = {}
|
25
|
+
to_s
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def diff expected, actual, opts = {}
|
30
|
+
options = DEFAULT_OPTIONS.merge(opts)
|
10
31
|
case expected
|
11
32
|
when Hash then hash_diff(expected, actual, options)
|
12
33
|
when Array then array_diff(expected, actual, options)
|
@@ -31,17 +52,7 @@ module Pact
|
|
31
52
|
def array_diff expected, actual, options
|
32
53
|
if actual.is_a? Array
|
33
54
|
if expected.length == actual.length
|
34
|
-
|
35
|
-
diff_found = false
|
36
|
-
expected.each_with_index do | item, index|
|
37
|
-
if (item_diff = diff(item, actual[index], options)).any?
|
38
|
-
diff_found = true
|
39
|
-
difference << item_diff
|
40
|
-
else
|
41
|
-
difference << NO_DIFF_INDICATOR
|
42
|
-
end
|
43
|
-
end
|
44
|
-
diff_found ? difference : {}
|
55
|
+
actual_array_diff expected, actual, options
|
45
56
|
else
|
46
57
|
{expected: expected, actual: actual}
|
47
58
|
end
|
@@ -50,14 +61,44 @@ module Pact
|
|
50
61
|
end
|
51
62
|
end
|
52
63
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
64
|
+
def actual_array_diff expected, actual, options
|
65
|
+
difference = []
|
66
|
+
diff_found = false
|
67
|
+
expected.each_with_index do | item, index|
|
68
|
+
if (item_diff = diff(item, actual.fetch(index, KeyNotFound.new), options)).any?
|
69
|
+
diff_found = true
|
70
|
+
difference << item_diff
|
71
|
+
else
|
72
|
+
difference << NO_DIFF_INDICATOR
|
73
|
+
end
|
74
|
+
end
|
75
|
+
diff_found ? difference : {}
|
76
|
+
end
|
77
|
+
|
78
|
+
def actual_hash_diff expected, actual, options
|
79
|
+
difference = expected.keys.inject({}) do |diff, key|
|
80
|
+
if (diff_at_key = diff(expected[key], actual.fetch(key, KeyNotFound.new), options)).any?
|
81
|
+
diff[key] = diff_at_key
|
82
|
+
end
|
83
|
+
diff
|
84
|
+
end
|
85
|
+
difference.merge(check_for_unexpected_keys(expected, actual, options))
|
86
|
+
end
|
87
|
+
|
88
|
+
def check_for_unexpected_keys expected, actual, options
|
89
|
+
if options[:allow_unexpected_keys]
|
90
|
+
{}
|
91
|
+
else
|
92
|
+
(actual.keys - expected.keys).inject({}) do | diff, key |
|
93
|
+
diff[key] = {:expected => UNEXPECTED_KEY, :actual => actual[key]}
|
59
94
|
diff
|
60
95
|
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def hash_diff expected, actual, options
|
100
|
+
if actual.is_a? Hash
|
101
|
+
actual_hash_diff expected, actual, options
|
61
102
|
else
|
62
103
|
{expected: expected, actual: actual}
|
63
104
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Pact
|
2
|
+
|
3
|
+
module Producer
|
4
|
+
module DSL
|
5
|
+
|
6
|
+
module Configuration
|
7
|
+
def provider= provider
|
8
|
+
@producer = provider
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
Pact::Configuration.send(:include, Configuration)
|
13
|
+
|
14
|
+
def service_provider name, &block
|
15
|
+
service_provider = ServiceProviderDSL.new(name, &block).create_service_provider
|
16
|
+
Pact.configuration.provider = service_provider
|
17
|
+
service_provider
|
18
|
+
end
|
19
|
+
|
20
|
+
class ServiceProviderConfig
|
21
|
+
attr_accessor :name
|
22
|
+
|
23
|
+
def initialize name, &app_block
|
24
|
+
@name = name
|
25
|
+
@app_block = app_block
|
26
|
+
end
|
27
|
+
|
28
|
+
def app
|
29
|
+
@app_block.call
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class ServiceProviderDSL
|
34
|
+
|
35
|
+
def initialize name, &block
|
36
|
+
@name = name
|
37
|
+
@app = nil
|
38
|
+
instance_eval(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate
|
42
|
+
raise "Please provide a name for the Producer" unless @name
|
43
|
+
raise "Please configure an app for the Producer" unless @app_block
|
44
|
+
end
|
45
|
+
|
46
|
+
def name name
|
47
|
+
@name = name
|
48
|
+
end
|
49
|
+
|
50
|
+
def app &block
|
51
|
+
@app_block = block
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_service_provider
|
55
|
+
validate
|
56
|
+
ServiceProviderConfig.new(@name, &@app_block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -11,6 +11,9 @@ module Pact
|
|
11
11
|
instance_eval(&block)
|
12
12
|
ProducerState.current_namespaces.pop
|
13
13
|
end
|
14
|
+
|
15
|
+
alias_method :provider_states_for, :with_consumer
|
16
|
+
alias_method :provider_state, :producer_state
|
14
17
|
end
|
15
18
|
|
16
19
|
class ProducerState
|
@@ -22,6 +25,10 @@ module Pact
|
|
22
25
|
ProducerState.new(name, current_namespaces.join('.'), &block)
|
23
26
|
end
|
24
27
|
|
28
|
+
def self.provider_state name, &block
|
29
|
+
producer_state name, &block
|
30
|
+
end
|
31
|
+
|
25
32
|
def self.register name, producer_state
|
26
33
|
producer_states[name] = producer_state
|
27
34
|
end
|
data/lib/pact/producer/rspec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'pact/json_warning'
|
|
4
4
|
require_relative 'matchers'
|
5
5
|
require_relative 'test_methods'
|
6
6
|
require_relative 'configuration_dsl'
|
7
|
+
require_relative 'dsl'
|
7
8
|
|
8
9
|
module Pact
|
9
10
|
module Producer
|
@@ -111,6 +112,7 @@ module Pact
|
|
111
112
|
end
|
112
113
|
|
113
114
|
def save_pactfile_to_tmp pact, name
|
115
|
+
FileUtils.mkdir_p Pact.configuration.tmp_dir
|
114
116
|
File.open(Pact.configuration.tmp_dir + "/#{name}", "w") { |file| file << pact}
|
115
117
|
end
|
116
118
|
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'pact/producer/rspec'
|
data/lib/pact/request.rb
CHANGED
@@ -35,15 +35,15 @@ module Pact
|
|
35
35
|
end
|
36
36
|
|
37
37
|
class Base
|
38
|
-
|
39
|
-
|
38
|
+
include Pact::Matchers
|
39
|
+
extend Pact::Matchers
|
40
40
|
|
41
|
-
|
41
|
+
NULL_EXPECTATION = NullExpectation.new
|
42
42
|
|
43
|
-
attr_reader :method, :path, :headers, :body, :query
|
43
|
+
attr_reader :method, :path, :headers, :body, :query, :options
|
44
44
|
|
45
45
|
def self.from_hash(hash)
|
46
|
-
sym_hash = hash
|
46
|
+
sym_hash = symbolize_keys hash
|
47
47
|
method = sym_hash.fetch(:method)
|
48
48
|
path = sym_hash.fetch(:path)
|
49
49
|
query = sym_hash.fetch(:query, NULL_EXPECTATION)
|
@@ -52,6 +52,10 @@ module Pact
|
|
52
52
|
new(method, path, headers, body, query)
|
53
53
|
end
|
54
54
|
|
55
|
+
def self.symbolize_keys hash
|
56
|
+
hash.inject({}) { |memo, (k,v)| memo[k.to_sym] = v; memo }
|
57
|
+
end
|
58
|
+
|
55
59
|
def initialize(method, path, headers, body, query)
|
56
60
|
@method = method.to_s
|
57
61
|
@path = path.chomp('/')
|
@@ -76,18 +80,31 @@ module Pact
|
|
76
80
|
base_json
|
77
81
|
end
|
78
82
|
|
83
|
+
def as_json_without_body
|
84
|
+
keep_keys = [:method, :path, :headers, :query]
|
85
|
+
as_json.reject{ |key, value| !keep_keys.include? key }
|
86
|
+
end
|
87
|
+
|
79
88
|
end
|
80
89
|
|
81
90
|
class Expected < Base
|
82
91
|
|
92
|
+
DEFAULT_OPTIONS = {:allow_unexpected_keys => false}.freeze
|
83
93
|
attr_accessor :description
|
94
|
+
attr_accessor :options
|
84
95
|
|
85
96
|
def self.from_hash(hash)
|
86
97
|
request = super
|
87
98
|
request.description = hash.fetch(:description, nil)
|
99
|
+
request.options = symbolize_keys(hash).fetch(:options, {})
|
88
100
|
request
|
89
101
|
end
|
90
102
|
|
103
|
+
def initialize(method, path, headers, body, query, options = {})
|
104
|
+
super(method, path, headers, body, query)
|
105
|
+
@options = options
|
106
|
+
end
|
107
|
+
|
91
108
|
def match(actual_request)
|
92
109
|
difference(actual_request).empty?
|
93
110
|
end
|
@@ -97,7 +114,26 @@ module Pact
|
|
97
114
|
end
|
98
115
|
|
99
116
|
def difference(actual_request)
|
100
|
-
diff(
|
117
|
+
request_diff = diff(as_json_without_body, actual_request.as_json_without_body)
|
118
|
+
unless body.is_a? NullExpectation
|
119
|
+
request_diff.merge(body_difference(actual_request.body))
|
120
|
+
else
|
121
|
+
request_diff
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def body_difference(actual_body)
|
126
|
+
diff({:body => body}, {body: actual_body}, allow_unexpected_keys: runtime_options[:allow_unexpected_keys_in_body])
|
127
|
+
end
|
128
|
+
|
129
|
+
def as_json_with_options
|
130
|
+
as_json.merge( options.empty? ? {} : { options: options} )
|
131
|
+
end
|
132
|
+
|
133
|
+
# Don't want to put the default options in the pact json just yet, so calculating these at run time, rather than assigning
|
134
|
+
# the result to @options
|
135
|
+
def runtime_options
|
136
|
+
DEFAULT_OPTIONS.merge(self.class.symbolize_keys(options))
|
101
137
|
end
|
102
138
|
|
103
139
|
end
|
@@ -15,11 +15,8 @@ require 'pact/producer/pact_spec_runner'
|
|
15
15
|
The support_file should include code that makes your rack app available for the rack testing framework.
|
16
16
|
Eg.
|
17
17
|
|
18
|
-
Pact.
|
19
|
-
|
20
|
-
name "My Producer"
|
21
|
-
app { TestApp.new }
|
22
|
-
end
|
18
|
+
Pact.service_provider "My Provider" do
|
19
|
+
app { TestApp.new }
|
23
20
|
end
|
24
21
|
|
25
22
|
It should also load all your app's dependencies (eg by calling out to spec_helper)
|
data/lib/pact/version.rb
CHANGED