pact-v2 2.0.0.pre.preview2 → 2.0.0.pre.preview3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/lib/pact/provider/request.rb +14 -1
- data/lib/pact/v2/consumer/grpc_interaction_builder.rb +10 -3
- data/lib/pact/v2/consumer/http_interaction_builder.rb +1 -2
- data/lib/pact/v2/consumer/interaction_contents.rb +8 -0
- data/lib/pact/v2/consumer/message_interaction_builder.rb +9 -2
- data/lib/pact/v2/consumer/mock_server.rb +4 -3
- data/lib/pact/v2/consumer/pact_config/plugin_async_message.rb +26 -0
- data/lib/pact/v2/consumer/pact_config/plugin_http.rb +26 -0
- data/lib/pact/v2/consumer/pact_config/plugin_sync_message.rb +26 -0
- data/lib/pact/v2/consumer/pact_config.rb +6 -0
- data/lib/pact/v2/consumer/plugin_async_message_interaction_builder.rb +171 -0
- data/lib/pact/v2/consumer/plugin_http_interaction_builder.rb +201 -0
- data/lib/pact/v2/consumer/plugin_sync_message_interaction_builder.rb +180 -0
- data/lib/pact/v2/generators/base.rb +287 -0
- data/lib/pact/v2/generators.rb +49 -0
- data/lib/pact/v2/matchers/base.rb +13 -6
- data/lib/pact/v2/matchers/v3/content_type.rb +32 -0
- data/lib/pact/v2/matchers/v3/null.rb +16 -0
- data/lib/pact/v2/matchers/v3/semver.rb +23 -0
- data/lib/pact/v2/matchers/v3/values.rb +16 -0
- data/lib/pact/v2/matchers/v4/not_empty.rb +10 -3
- data/lib/pact/v2/matchers/v4/status_code.rb +17 -0
- data/lib/pact/v2/matchers.rb +16 -0
- data/lib/pact/v2/provider/pact_broker_proxy.rb +0 -5
- data/lib/pact/v2/rspec/support/pact_consumer_helpers.rb +14 -1
- data/lib/pact/v2/version.rb +2 -2
- data/lib/pact/version.rb +1 -1
- data/lib/pact.rb +7 -13
- data/pact.gemspec +57 -67
- metadata +203 -204
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f4ee103d30ead3e3bb7c57842add4eb6e0aea19da1ab945c4007631d49ffa94
|
4
|
+
data.tar.gz: 514b47642e7634b2b3232a353869277be5ca9f536a687a19f7eca366215dd9ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db69fe7c250ffa31e0a19c5d11eb3626e843262146339267931c8aa4e8c8dc05d5016d73cedb287e72f7c6a1a558afd493a8ea5af649df24e12a5a8beff848f1
|
7
|
+
data.tar.gz: a3c315ac1a9beb4dc8da7778fcaae331496b880e287ea124b1b50ca53f36f260bfcf746fac8065e808935db5b12794f57ae75608154beda679589da053b7ffd6
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
<a name="v1.66.2"></a>
|
2
|
+
### v1.66.2 (2025-10-03)
|
3
|
+
|
4
|
+
#### Bug Fixes
|
5
|
+
|
6
|
+
* **spec**
|
7
|
+
* update rspec keyword vs arg changes https://github.com/rspec/rspec-mocks/pull/1473 ([c1c17fc](/../../commit/c1c17fc))
|
8
|
+
|
9
|
+
* correct generators import and update specs ([d025342](/../../commit/d025342))
|
10
|
+
* example/animal-service/Gemfile to reduce vulnerabilities ([4dfb3f5](/../../commit/4dfb3f5))
|
11
|
+
* example/animal-service/Gemfile to reduce vulnerabilities ([3d8fa31](/../../commit/3d8fa31))
|
12
|
+
* example/zoo-app/Gemfile to reduce vulnerabilities ([5ff8945](/../../commit/5ff8945))
|
13
|
+
|
1
14
|
<a name="v1.66.1"></a>
|
2
15
|
### v1.66.1 (2025-01-20)
|
3
16
|
|
@@ -29,7 +29,7 @@ module Pact
|
|
29
29
|
when String then expected_request.body
|
30
30
|
when NullExpectation then ''
|
31
31
|
else
|
32
|
-
|
32
|
+
generated_body
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -60,6 +60,19 @@ module Pact
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
|
+
def generated_body
|
64
|
+
result = Pact::Generators.apply_generators(expected_request, "body", reified_body, @state_params)
|
65
|
+
|
66
|
+
case result
|
67
|
+
when Hash
|
68
|
+
result.to_json
|
69
|
+
when String
|
70
|
+
result
|
71
|
+
else
|
72
|
+
raise "Expected body to be a String or Hash, but was #{result.class} with value #{result.inspect}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
63
76
|
def rack_request_header_for header
|
64
77
|
with_http_prefix(header.to_s.upcase).tr('-', '_')
|
65
78
|
end
|
@@ -68,6 +68,13 @@ module Pact
|
|
68
68
|
self
|
69
69
|
end
|
70
70
|
|
71
|
+
def with_pact_protobuf_plugin_version(version)
|
72
|
+
raise InteractionBuilderError.new("version is required") if version.blank?
|
73
|
+
|
74
|
+
@proto_plugin_version = version
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
71
78
|
def given(provider_state, metadata = {})
|
72
79
|
@provider_state_meta = {provider_state => metadata}
|
73
80
|
self
|
@@ -142,13 +149,13 @@ module Pact
|
|
142
149
|
|
143
150
|
yield(message_pact, mock_server)
|
144
151
|
|
152
|
+
ensure
|
145
153
|
if mock_server.matched?
|
146
154
|
mock_server.write_pacts!(@pact_config.pact_dir)
|
147
155
|
else
|
148
156
|
msg = mismatches_error_msg(mock_server)
|
149
157
|
raise InteractionMismatchesError.new(msg)
|
150
158
|
end
|
151
|
-
ensure
|
152
159
|
@used = true
|
153
160
|
mock_server&.cleanup
|
154
161
|
PactFfi::PluginConsumer.cleanup_plugins(pact_handle)
|
@@ -175,11 +182,11 @@ module Pact
|
|
175
182
|
end
|
176
183
|
|
177
184
|
def init_plugin!(pact_handle)
|
178
|
-
result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, PROTOBUF_PLUGIN_VERSION)
|
185
|
+
result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, @proto_plugin_version || PROTOBUF_PLUGIN_VERSION)
|
179
186
|
return result if INIT_PLUGIN_ERRORS[result].blank?
|
180
187
|
|
181
188
|
error = INIT_PLUGIN_ERRORS[result]
|
182
|
-
raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status])
|
189
|
+
raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{@proto_plugin_version || PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status])
|
183
190
|
end
|
184
191
|
end
|
185
192
|
end
|
@@ -9,7 +9,6 @@ module Pact
|
|
9
9
|
module V2
|
10
10
|
module Consumer
|
11
11
|
class HttpInteractionBuilder
|
12
|
-
DESCRIPTION_PREFIX = "http: "
|
13
12
|
|
14
13
|
# https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html
|
15
14
|
CREATE_INTERACTION_ERRORS = {
|
@@ -114,13 +113,13 @@ module Pact
|
|
114
113
|
|
115
114
|
yield(mock_server)
|
116
115
|
|
116
|
+
ensure
|
117
117
|
if mock_server.matched?
|
118
118
|
mock_server.write_pacts!(pact_config.pact_dir)
|
119
119
|
else
|
120
120
|
msg = mismatches_error_msg(mock_server)
|
121
121
|
raise InteractionMismatchesError.new(msg)
|
122
122
|
end
|
123
|
-
ensure
|
124
123
|
@used = true
|
125
124
|
mock_server&.cleanup
|
126
125
|
# Reset the pact handle to allow for a new interaction to be built
|
@@ -33,6 +33,10 @@ module Pact
|
|
33
33
|
return hash.as_basic if format == :basic
|
34
34
|
return hash.as_plugin if format == :plugin
|
35
35
|
end
|
36
|
+
if hash.is_a?(Pact::V2::Generators::Base)
|
37
|
+
return hash.as_basic if format == :basic
|
38
|
+
return hash.as_plugin if format == :plugin
|
39
|
+
end
|
36
40
|
hash.each_pair do |key, value|
|
37
41
|
next serialize(value, format) if value.is_a?(Hash)
|
38
42
|
next hash[key] = value.map { |v| serialize(v, format) } if value.is_a?(Array)
|
@@ -40,6 +44,10 @@ module Pact
|
|
40
44
|
hash[key] = value.as_basic if format == :basic
|
41
45
|
hash[key] = value.as_plugin if format == :plugin
|
42
46
|
end
|
47
|
+
if value.is_a?(Pact::V2::Generators::Base)
|
48
|
+
hash[key] = value.as_basic if format == :basic
|
49
|
+
hash[key] = value.as_plugin if format == :plugin
|
50
|
+
end
|
43
51
|
end
|
44
52
|
|
45
53
|
hash
|
@@ -86,6 +86,13 @@ module Pact
|
|
86
86
|
self
|
87
87
|
end
|
88
88
|
|
89
|
+
def with_pact_protobuf_plugin_version(version)
|
90
|
+
raise InteractionBuilderError.new("version is required") if version.blank?
|
91
|
+
|
92
|
+
@proto_plugin_version = version
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
89
96
|
def with_proto_contents(contents_hash)
|
90
97
|
@proto_contents = InteractionContents.plugin(contents_hash)
|
91
98
|
self
|
@@ -257,11 +264,11 @@ module Pact
|
|
257
264
|
end
|
258
265
|
|
259
266
|
def init_plugin!(pact_handle)
|
260
|
-
result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, PROTOBUF_PLUGIN_VERSION)
|
267
|
+
result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, @proto_plugin_version || PROTOBUF_PLUGIN_VERSION)
|
261
268
|
return result if INIT_PLUGIN_ERRORS[result].blank?
|
262
269
|
|
263
270
|
error = INIT_PLUGIN_ERRORS[result]
|
264
|
-
raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status])
|
271
|
+
raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{@proto_plugin_version || PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status])
|
265
272
|
end
|
266
273
|
|
267
274
|
def serialize_metadata(metadata_hash)
|
@@ -11,8 +11,6 @@ module Pact
|
|
11
11
|
TRANSPORT_HTTP = "http"
|
12
12
|
TRANSPORT_GRPC = "grpc"
|
13
13
|
|
14
|
-
TRANSPORTS = [TRANSPORT_HTTP, TRANSPORT_GRPC].freeze
|
15
|
-
|
16
14
|
class MockServerCreateError < Pact::V2::FfiError; end
|
17
15
|
|
18
16
|
class WritePactsError < Pact::V2::FfiError; end
|
@@ -41,8 +39,11 @@ module Pact
|
|
41
39
|
new(pact: pact, transport: TRANSPORT_HTTP, host: host, port: port)
|
42
40
|
end
|
43
41
|
|
42
|
+
def self.create_for_transport!(pact:, transport:, host: "127.0.0.1", port: 0)
|
43
|
+
new(pact: pact, transport: transport, host: host, port: port)
|
44
|
+
end
|
45
|
+
|
44
46
|
def initialize(pact:, transport:, host:, port:)
|
45
|
-
raise "Transport #{transport} is not supported yet, available transports are: #{TRANSPORTS.join(",")}" unless TRANSPORTS.include?(transport)
|
46
47
|
|
47
48
|
@pact = pact
|
48
49
|
@transport = transport
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module V2
|
7
|
+
module Consumer
|
8
|
+
module PactConfig
|
9
|
+
class PluginAsyncMessage < Base
|
10
|
+
attr_reader :mock_host, :mock_port
|
11
|
+
|
12
|
+
def initialize(consumer_name:, provider_name:, opts: {})
|
13
|
+
super
|
14
|
+
|
15
|
+
@mock_host = opts[:mock_host] || "127.0.0.1"
|
16
|
+
@mock_port = opts[:mock_port] || 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def new_interaction(description = nil)
|
20
|
+
PluginAsyncMessageInteractionBuilder.new(self, description: description)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module V2
|
7
|
+
module Consumer
|
8
|
+
module PactConfig
|
9
|
+
class PluginHttp < Base
|
10
|
+
attr_reader :mock_host, :mock_port
|
11
|
+
|
12
|
+
def initialize(consumer_name:, provider_name:, opts: {})
|
13
|
+
super
|
14
|
+
|
15
|
+
@mock_host = opts[:mock_host] || "127.0.0.1"
|
16
|
+
@mock_port = opts[:mock_port] || 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def new_interaction(description = nil)
|
20
|
+
PluginHttpInteractionBuilder.new(self, description: description)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module V2
|
7
|
+
module Consumer
|
8
|
+
module PactConfig
|
9
|
+
class PluginSyncMessage < Base
|
10
|
+
attr_reader :mock_host, :mock_port
|
11
|
+
|
12
|
+
def initialize(consumer_name:, provider_name:, opts: {})
|
13
|
+
super
|
14
|
+
|
15
|
+
@mock_host = opts[:mock_host] || "127.0.0.1"
|
16
|
+
@mock_port = opts[:mock_port] || 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def new_interaction(description = nil)
|
20
|
+
PluginSyncMessageInteractionBuilder.new(self, description: description)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -14,6 +14,12 @@ module Pact
|
|
14
14
|
Grpc.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
|
15
15
|
when :message
|
16
16
|
Message.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
|
17
|
+
when :plugin_sync_message
|
18
|
+
PluginSyncMessage.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
|
19
|
+
when :plugin_async_message
|
20
|
+
PluginAsyncMessage.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
|
21
|
+
when :plugin_http
|
22
|
+
PluginHttp.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
|
17
23
|
else
|
18
24
|
raise ArgumentError, "unknown transport_type: #{transport_type}"
|
19
25
|
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/async_message_pact"
|
4
|
+
require "pact/ffi/plugin_consumer"
|
5
|
+
require "pact/ffi/logger"
|
6
|
+
|
7
|
+
module Pact
|
8
|
+
module V2
|
9
|
+
module Consumer
|
10
|
+
class PluginAsyncMessageInteractionBuilder
|
11
|
+
|
12
|
+
class PluginInitError < Pact::V2::FfiError; end
|
13
|
+
|
14
|
+
# https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html
|
15
|
+
INIT_PLUGIN_ERRORS = {
|
16
|
+
1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
|
17
|
+
2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"},
|
18
|
+
3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"}
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html
|
22
|
+
CREATE_INTERACTION_ERRORS = {
|
23
|
+
1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
|
24
|
+
2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"},
|
25
|
+
3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"},
|
26
|
+
4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"},
|
27
|
+
5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"},
|
28
|
+
6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"}
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
class CreateInteractionError < Pact::V2::FfiError; end
|
32
|
+
|
33
|
+
class InteractionMismatchesError < Pact::V2::Error; end
|
34
|
+
|
35
|
+
class InteractionBuilderError < Pact::V2::Error; end
|
36
|
+
|
37
|
+
def initialize(pact_config, description: nil)
|
38
|
+
@pact_config = pact_config
|
39
|
+
@description = description || ""
|
40
|
+
@contents = nil
|
41
|
+
@provider_state_meta = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def with_plugin(plugin_name, plugin_version)
|
45
|
+
raise InteractionBuilderError.new("plugin_name is required") if plugin_name.blank?
|
46
|
+
raise InteractionBuilderError.new("plugin_version is required") if plugin_version.blank?
|
47
|
+
|
48
|
+
@plugin_name = plugin_name
|
49
|
+
@plugin_version = plugin_version
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def given(provider_state, metadata = {})
|
54
|
+
@provider_state_meta = {provider_state => metadata}
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def upon_receiving(description)
|
59
|
+
@description = description
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def with_contents(contents_hash)
|
64
|
+
@contents = InteractionContents.plugin(contents_hash)
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def with_content_type(content_type)
|
69
|
+
@interaction_content_type = content_type || @content_type
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def with_plugin_metadata(meta_hash)
|
74
|
+
@plugin_metadata = meta_hash
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
def with_transport(transport)
|
79
|
+
@transport = transport
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def interaction_json
|
84
|
+
result = {
|
85
|
+
contents: @contents
|
86
|
+
}
|
87
|
+
result.merge!(@plugin_metadata) if @plugin_metadata.is_a?(Hash)
|
88
|
+
JSON.dump(result)
|
89
|
+
end
|
90
|
+
|
91
|
+
def validate!
|
92
|
+
raise InteractionBuilderError.new("invalid contents format, should be a hash") unless @contents.is_a?(Hash)
|
93
|
+
end
|
94
|
+
|
95
|
+
def execute(&block)
|
96
|
+
raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used)
|
97
|
+
|
98
|
+
validate!
|
99
|
+
|
100
|
+
pact_handle = init_pact
|
101
|
+
init_plugin!(pact_handle)
|
102
|
+
|
103
|
+
interaction = PactFfi::AsyncMessageConsumer.new(pact_handle, @description)
|
104
|
+
|
105
|
+
@provider_state_meta&.each_pair do |provider_state, meta|
|
106
|
+
if meta.present?
|
107
|
+
meta.each_pair do |k, v|
|
108
|
+
if v.nil? || (v.respond_to?(:empty?) && v.empty?)
|
109
|
+
PactFfi.given(interaction, provider_state)
|
110
|
+
else
|
111
|
+
PactFfi.given_with_param(interaction, provider_state, k.to_s, v.to_s)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
else
|
115
|
+
PactFfi.given(interaction, provider_state)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
result = PactFfi.with_body(interaction, 0, @interaction_content_type, interaction_json)
|
120
|
+
if CREATE_INTERACTION_ERRORS[result].present?
|
121
|
+
error = CREATE_INTERACTION_ERRORS[result]
|
122
|
+
raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status])
|
123
|
+
end
|
124
|
+
|
125
|
+
mock_server = MockServer.create_for_transport!(pact: pact_handle, transport: @transport, host: @pact_config.mock_host, port: @pact_config.mock_port)
|
126
|
+
|
127
|
+
yield(pact_handle, mock_server)
|
128
|
+
|
129
|
+
ensure
|
130
|
+
if mock_server.matched?
|
131
|
+
mock_server.write_pacts!(@pact_config.pact_dir)
|
132
|
+
else
|
133
|
+
msg = mismatches_error_msg(mock_server)
|
134
|
+
raise InteractionMismatchesError.new(msg)
|
135
|
+
end
|
136
|
+
@used = true
|
137
|
+
mock_server&.cleanup
|
138
|
+
PactFfi::PluginConsumer.cleanup_plugins(pact_handle) if pact_handle
|
139
|
+
PactFfi.free_pact_handle(pact_handle) if pact_handle
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def mismatches_error_msg(mock_server)
|
145
|
+
rspec_example_desc = RSpec.current_example&.description
|
146
|
+
return "interaction for has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank?
|
147
|
+
|
148
|
+
"#{rspec_example_desc} has mismatches: #{mock_server.mismatches}"
|
149
|
+
end
|
150
|
+
|
151
|
+
def init_pact
|
152
|
+
handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name)
|
153
|
+
PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
|
154
|
+
PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version)
|
155
|
+
|
156
|
+
Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level)
|
157
|
+
|
158
|
+
handle
|
159
|
+
end
|
160
|
+
|
161
|
+
def init_plugin!(pact_handle)
|
162
|
+
result = PactFfi::PluginConsumer.using_plugin(pact_handle, @plugin_name, @plugin_version)
|
163
|
+
return result if INIT_PLUGIN_ERRORS[result].blank?
|
164
|
+
|
165
|
+
error = INIT_PLUGIN_ERRORS[result]
|
166
|
+
raise PluginInitError.new("There was an error while trying to initialize plugin #{@plugin_name}/#{@plugin_version}", error[:reason], error[:status])
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/http_consumer"
|
4
|
+
require "pact/ffi/plugin_consumer"
|
5
|
+
require "pact/ffi/logger"
|
6
|
+
|
7
|
+
module Pact
|
8
|
+
module V2
|
9
|
+
module Consumer
|
10
|
+
class PluginHttpInteractionBuilder
|
11
|
+
|
12
|
+
class PluginInitError < Pact::V2::FfiError; end
|
13
|
+
|
14
|
+
# https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html
|
15
|
+
INIT_PLUGIN_ERRORS = {
|
16
|
+
1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
|
17
|
+
2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"},
|
18
|
+
3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"}
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html
|
22
|
+
CREATE_INTERACTION_ERRORS = {
|
23
|
+
1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
|
24
|
+
2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"},
|
25
|
+
3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"},
|
26
|
+
4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"},
|
27
|
+
5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"},
|
28
|
+
6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"}
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
class CreateInteractionError < Pact::V2::FfiError; end
|
32
|
+
|
33
|
+
class InteractionMismatchesError < Pact::V2::Error; end
|
34
|
+
|
35
|
+
class InteractionBuilderError < Pact::V2::Error; end
|
36
|
+
|
37
|
+
def initialize(pact_config, description: nil)
|
38
|
+
@pact_config = pact_config
|
39
|
+
@description = description || ""
|
40
|
+
@contents = nil
|
41
|
+
@provider_state_meta = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def with_plugin(plugin_name, plugin_version)
|
45
|
+
raise InteractionBuilderError.new("plugin_name is required") if plugin_name.blank?
|
46
|
+
raise InteractionBuilderError.new("plugin_version is required") if plugin_version.blank?
|
47
|
+
|
48
|
+
@plugin_name = plugin_name
|
49
|
+
@plugin_version = plugin_version
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def given(provider_state, metadata = {})
|
54
|
+
@provider_state_meta = {provider_state => metadata}
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def upon_receiving(description)
|
59
|
+
@description = description
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def with_request(method: nil, path: nil, query: {}, headers: {}, body: nil)
|
64
|
+
@request = {
|
65
|
+
method: method,
|
66
|
+
path: path,
|
67
|
+
query: query,
|
68
|
+
headers: headers,
|
69
|
+
body: body
|
70
|
+
}
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def will_respond_with(status: nil, headers: {}, body: nil)
|
75
|
+
@response = {
|
76
|
+
status: status,
|
77
|
+
headers: headers,
|
78
|
+
body: body
|
79
|
+
}
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def with_content_type(content_type)
|
84
|
+
@content_type = content_type
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def with_plugin_metadata(meta_hash)
|
90
|
+
@plugin_metadata = meta_hash
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def with_transport(transport)
|
95
|
+
@transport = transport
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
def interaction_json
|
100
|
+
result = {
|
101
|
+
request: @request,
|
102
|
+
response: @response
|
103
|
+
}
|
104
|
+
result.merge!(@plugin_metadata) if @plugin_metadata.is_a?(Hash)
|
105
|
+
JSON.dump(result)
|
106
|
+
end
|
107
|
+
|
108
|
+
def validate!
|
109
|
+
raise InteractionBuilderError.new("invalid request format, should be a hash") unless @request.is_a?(Hash)
|
110
|
+
raise InteractionBuilderError.new("invalid response format, should be a hash") unless @response.is_a?(Hash)
|
111
|
+
end
|
112
|
+
|
113
|
+
def execute(&block)
|
114
|
+
raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used)
|
115
|
+
|
116
|
+
validate!
|
117
|
+
|
118
|
+
pact_handle = init_pact
|
119
|
+
init_plugin!(pact_handle)
|
120
|
+
|
121
|
+
interaction = PactFfi.new_interaction(pact_handle, @description)
|
122
|
+
@provider_state_meta&.each_pair do |provider_state, meta|
|
123
|
+
if meta.present?
|
124
|
+
meta.each_pair do |k, v|
|
125
|
+
if v.nil? || (v.respond_to?(:empty?) && v.empty?)
|
126
|
+
PactFfi.given(interaction, provider_state)
|
127
|
+
else
|
128
|
+
PactFfi.given_with_param(interaction, provider_state, k.to_s, v.to_s)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
else
|
132
|
+
PactFfi.given(interaction, provider_state)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
PactFfi::HttpConsumer.with_request(interaction, @request[:method], @request[:path])
|
136
|
+
|
137
|
+
result = PactFfi::PluginConsumer.interaction_contents(interaction, 0, @request[:headers]["content-type"], format_value(@request[:body]))
|
138
|
+
if CREATE_INTERACTION_ERRORS[result].present?
|
139
|
+
error = CREATE_INTERACTION_ERRORS[result]
|
140
|
+
raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status])
|
141
|
+
end
|
142
|
+
result = PactFfi::PluginConsumer.interaction_contents(interaction, 1, @response[:headers]["content-type"], format_value(@response[:body]))
|
143
|
+
if CREATE_INTERACTION_ERRORS[result].present?
|
144
|
+
error = CREATE_INTERACTION_ERRORS[result]
|
145
|
+
raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status])
|
146
|
+
end
|
147
|
+
mock_server = MockServer.create_for_transport!(pact: pact_handle, transport: @transport || 'http', host: @pact_config.mock_host, port: @pact_config.mock_port)
|
148
|
+
|
149
|
+
yield(mock_server)
|
150
|
+
|
151
|
+
ensure
|
152
|
+
if mock_server.matched?
|
153
|
+
mock_server.write_pacts!(@pact_config.pact_dir)
|
154
|
+
else
|
155
|
+
msg = mismatches_error_msg(mock_server)
|
156
|
+
raise InteractionMismatchesError.new(msg)
|
157
|
+
end
|
158
|
+
@used = true
|
159
|
+
mock_server&.cleanup
|
160
|
+
PactFfi::PluginConsumer.cleanup_plugins(pact_handle) if pact_handle
|
161
|
+
PactFfi.free_pact_handle(pact_handle) if pact_handle
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def mismatches_error_msg(mock_server)
|
167
|
+
rspec_example_desc = RSpec.current_example&.description
|
168
|
+
return "interaction for has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank?
|
169
|
+
|
170
|
+
"#{rspec_example_desc} has mismatches: #{mock_server.mismatches}"
|
171
|
+
end
|
172
|
+
|
173
|
+
def init_pact
|
174
|
+
handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name)
|
175
|
+
PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
|
176
|
+
PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version)
|
177
|
+
|
178
|
+
Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level)
|
179
|
+
|
180
|
+
handle
|
181
|
+
end
|
182
|
+
|
183
|
+
def init_plugin!(pact_handle)
|
184
|
+
result = PactFfi::PluginConsumer.using_plugin(pact_handle, @plugin_name, @plugin_version)
|
185
|
+
return result if INIT_PLUGIN_ERRORS[result].blank?
|
186
|
+
|
187
|
+
error = INIT_PLUGIN_ERRORS[result]
|
188
|
+
raise PluginInitError.new("There was an error while trying to initialize plugin #{@plugin_name}/#{@plugin_version}", error[:reason], error[:status])
|
189
|
+
end
|
190
|
+
|
191
|
+
def format_value(obj)
|
192
|
+
return obj if obj.is_a?(String)
|
193
|
+
|
194
|
+
return JSON.dump({value: obj}) if obj.is_a?(Array)
|
195
|
+
|
196
|
+
JSON.dump(obj)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|