pact 0.1.28 → 0.1.35
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/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