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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/lib/pact/provider/request.rb +14 -1
  4. data/lib/pact/v2/consumer/grpc_interaction_builder.rb +10 -3
  5. data/lib/pact/v2/consumer/http_interaction_builder.rb +1 -2
  6. data/lib/pact/v2/consumer/interaction_contents.rb +8 -0
  7. data/lib/pact/v2/consumer/message_interaction_builder.rb +9 -2
  8. data/lib/pact/v2/consumer/mock_server.rb +4 -3
  9. data/lib/pact/v2/consumer/pact_config/plugin_async_message.rb +26 -0
  10. data/lib/pact/v2/consumer/pact_config/plugin_http.rb +26 -0
  11. data/lib/pact/v2/consumer/pact_config/plugin_sync_message.rb +26 -0
  12. data/lib/pact/v2/consumer/pact_config.rb +6 -0
  13. data/lib/pact/v2/consumer/plugin_async_message_interaction_builder.rb +171 -0
  14. data/lib/pact/v2/consumer/plugin_http_interaction_builder.rb +201 -0
  15. data/lib/pact/v2/consumer/plugin_sync_message_interaction_builder.rb +180 -0
  16. data/lib/pact/v2/generators/base.rb +287 -0
  17. data/lib/pact/v2/generators.rb +49 -0
  18. data/lib/pact/v2/matchers/base.rb +13 -6
  19. data/lib/pact/v2/matchers/v3/content_type.rb +32 -0
  20. data/lib/pact/v2/matchers/v3/null.rb +16 -0
  21. data/lib/pact/v2/matchers/v3/semver.rb +23 -0
  22. data/lib/pact/v2/matchers/v3/values.rb +16 -0
  23. data/lib/pact/v2/matchers/v4/not_empty.rb +10 -3
  24. data/lib/pact/v2/matchers/v4/status_code.rb +17 -0
  25. data/lib/pact/v2/matchers.rb +16 -0
  26. data/lib/pact/v2/provider/pact_broker_proxy.rb +0 -5
  27. data/lib/pact/v2/rspec/support/pact_consumer_helpers.rb +14 -1
  28. data/lib/pact/v2/version.rb +2 -2
  29. data/lib/pact/version.rb +1 -1
  30. data/lib/pact.rb +7 -13
  31. data/pact.gemspec +57 -67
  32. metadata +203 -204
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23f610401e860f40dadf011d0609bb01b240d6002f07800df3815b35435df4c1
4
- data.tar.gz: 26235296c89f61bcbcc4e7b849793bfeeb286decef097a9168a9fdcb74a9c6ff
3
+ metadata.gz: 0f4ee103d30ead3e3bb7c57842add4eb6e0aea19da1ab945c4007631d49ffa94
4
+ data.tar.gz: 514b47642e7634b2b3232a353869277be5ca9f536a687a19f7eca366215dd9ee
5
5
  SHA512:
6
- metadata.gz: 71746aac031e4164e75e3f315f9cd17e45b299d615b92aaf069c3d43dd392be708eaa9b83a11dd5026337ac1dc28314867bb9191214e0736768ebc9359299737
7
- data.tar.gz: d3e77fe2177f4b1e7a549e4196c2cbb101f478d0ce04ab754c3d95802a9ba7ba8a0cce74391cbf84bb0f728777ec1e3902ab5f621aae0a95da96db907162ecf3
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
- Pact::Generators.apply_generators(expected_request, "body", reified_body, @state_params)
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