pact 1.66.2 → 1.67.1

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/lib/pact/v2/configuration.rb +23 -0
  4. data/lib/pact/v2/consumer/grpc_interaction_builder.rb +194 -0
  5. data/lib/pact/v2/consumer/http_interaction_builder.rb +162 -0
  6. data/lib/pact/v2/consumer/interaction_contents.rb +62 -0
  7. data/lib/pact/v2/consumer/message_interaction_builder.rb +287 -0
  8. data/lib/pact/v2/consumer/mock_server.rb +100 -0
  9. data/lib/pact/v2/consumer/pact_config/base.rb +24 -0
  10. data/lib/pact/v2/consumer/pact_config/grpc.rb +26 -0
  11. data/lib/pact/v2/consumer/pact_config/http.rb +55 -0
  12. data/lib/pact/v2/consumer/pact_config/message.rb +17 -0
  13. data/lib/pact/v2/consumer/pact_config/plugin_async_message.rb +26 -0
  14. data/lib/pact/v2/consumer/pact_config/plugin_http.rb +26 -0
  15. data/lib/pact/v2/consumer/pact_config/plugin_sync_message.rb +26 -0
  16. data/lib/pact/v2/consumer/pact_config.rb +30 -0
  17. data/lib/pact/v2/consumer/plugin_async_message_interaction_builder.rb +171 -0
  18. data/lib/pact/v2/consumer/plugin_http_interaction_builder.rb +201 -0
  19. data/lib/pact/v2/consumer/plugin_sync_message_interaction_builder.rb +180 -0
  20. data/lib/pact/v2/consumer.rb +8 -0
  21. data/lib/pact/v2/generators/base.rb +287 -0
  22. data/lib/pact/v2/generators.rb +49 -0
  23. data/lib/pact/v2/matchers/base.rb +74 -0
  24. data/lib/pact/v2/matchers/v1/equality.rb +19 -0
  25. data/lib/pact/v2/matchers/v2/regex.rb +19 -0
  26. data/lib/pact/v2/matchers/v2/type.rb +17 -0
  27. data/lib/pact/v2/matchers/v3/boolean.rb +17 -0
  28. data/lib/pact/v2/matchers/v3/content_type.rb +32 -0
  29. data/lib/pact/v2/matchers/v3/date.rb +18 -0
  30. data/lib/pact/v2/matchers/v3/date_time.rb +18 -0
  31. data/lib/pact/v2/matchers/v3/decimal.rb +17 -0
  32. data/lib/pact/v2/matchers/v3/each.rb +42 -0
  33. data/lib/pact/v2/matchers/v3/include.rb +17 -0
  34. data/lib/pact/v2/matchers/v3/integer.rb +17 -0
  35. data/lib/pact/v2/matchers/v3/null.rb +16 -0
  36. data/lib/pact/v2/matchers/v3/number.rb +17 -0
  37. data/lib/pact/v2/matchers/v3/semver.rb +23 -0
  38. data/lib/pact/v2/matchers/v3/time.rb +18 -0
  39. data/lib/pact/v2/matchers/v3/values.rb +16 -0
  40. data/lib/pact/v2/matchers/v4/each_key.rb +26 -0
  41. data/lib/pact/v2/matchers/v4/each_key_value.rb +32 -0
  42. data/lib/pact/v2/matchers/v4/each_value.rb +33 -0
  43. data/lib/pact/v2/matchers/v4/not_empty.rb +24 -0
  44. data/lib/pact/v2/matchers/v4/status_code.rb +17 -0
  45. data/lib/pact/v2/matchers.rb +110 -0
  46. data/lib/pact/v2/native/blocking_verifier.rb +17 -0
  47. data/lib/pact/v2/native/logger.rb +25 -0
  48. data/lib/pact/v2/provider/async_message_verifier.rb +28 -0
  49. data/lib/pact/v2/provider/base_verifier.rb +242 -0
  50. data/lib/pact/v2/provider/grpc_verifier.rb +38 -0
  51. data/lib/pact/v2/provider/gruf_server.rb +75 -0
  52. data/lib/pact/v2/provider/http_server.rb +79 -0
  53. data/lib/pact/v2/provider/http_verifier.rb +43 -0
  54. data/lib/pact/v2/provider/message_provider_servlet.rb +79 -0
  55. data/lib/pact/v2/provider/mixed_verifier.rb +22 -0
  56. data/lib/pact/v2/provider/pact_broker_proxy.rb +66 -0
  57. data/lib/pact/v2/provider/pact_broker_proxy_runner.rb +77 -0
  58. data/lib/pact/v2/provider/pact_config/async.rb +29 -0
  59. data/lib/pact/v2/provider/pact_config/base.rb +101 -0
  60. data/lib/pact/v2/provider/pact_config/grpc.rb +26 -0
  61. data/lib/pact/v2/provider/pact_config/http.rb +27 -0
  62. data/lib/pact/v2/provider/pact_config/mixed.rb +39 -0
  63. data/lib/pact/v2/provider/pact_config.rb +26 -0
  64. data/lib/pact/v2/provider/provider_server_runner.rb +89 -0
  65. data/lib/pact/v2/provider/provider_state_configuration.rb +32 -0
  66. data/lib/pact/v2/provider/provider_state_servlet.rb +86 -0
  67. data/lib/pact/v2/provider.rb +8 -0
  68. data/lib/pact/v2/railtie.rb +13 -0
  69. data/lib/pact/v2/rspec/support/pact_consumer_helpers.rb +93 -0
  70. data/lib/pact/v2/rspec/support/pact_message_helpers.rb +42 -0
  71. data/lib/pact/v2/rspec/support/pact_provider_helpers.rb +129 -0
  72. data/lib/pact/v2/rspec/support/waterdrop/pact_waterdrop_client.rb +27 -0
  73. data/lib/pact/v2/rspec/support/webmock/webmock_helpers.rb +30 -0
  74. data/lib/pact/v2/rspec.rb +17 -0
  75. data/lib/pact/v2/tasks/pact.rake +13 -0
  76. data/lib/pact/v2.rb +71 -0
  77. data/lib/pact/version.rb +1 -1
  78. data/lib/pact.rb +6 -0
  79. data/pact.gemspec +54 -7
  80. metadata +405 -65
@@ -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
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/sync_message_consumer"
4
+ require "pact/ffi/plugin_consumer"
5
+ require "pact/ffi/logger"
6
+
7
+ module Pact
8
+ module V2
9
+ module Consumer
10
+ class PluginSyncMessageInteractionBuilder
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
+ @request = nil
41
+ @response = nil
42
+ @response_meta = nil
43
+ @provider_state_meta = nil
44
+ end
45
+
46
+ def with_plugin(plugin_name, plugin_version)
47
+ raise InteractionBuilderError.new("plugin_name is required") if plugin_name.blank?
48
+ raise InteractionBuilderError.new("plugin_version is required") if plugin_version.blank?
49
+
50
+ @plugin_name = plugin_name
51
+ @plugin_version = plugin_version
52
+ self
53
+ end
54
+
55
+ def given(provider_state, metadata = {})
56
+ @provider_state_meta = {provider_state => metadata}
57
+ self
58
+ end
59
+
60
+ def upon_receiving(description)
61
+ @description = description
62
+ self
63
+ end
64
+
65
+ def with_request(req_hash)
66
+ @request = InteractionContents.plugin(req_hash)
67
+ self
68
+ end
69
+
70
+ def with_content_type(content_type)
71
+ @content_type = content_type
72
+ self
73
+ end
74
+
75
+ def will_respond_with(resp_hash)
76
+ @response = InteractionContents.plugin(resp_hash)
77
+ self
78
+ end
79
+
80
+ def will_respond_with_meta(meta_hash)
81
+ @response_meta = InteractionContents.plugin(meta_hash)
82
+ self
83
+ end
84
+
85
+ def with_plugin_metadata(meta_hash)
86
+ @plugin_metadata = meta_hash
87
+ self
88
+ end
89
+
90
+ def with_transport(transport)
91
+ @transport = transport
92
+ self
93
+ end
94
+
95
+ def interaction_json
96
+ result = {
97
+ request: @request
98
+ }
99
+ result.merge!(@plugin_metadata) if @plugin_metadata.is_a?(Hash)
100
+
101
+ result[:response] = @response if @response.is_a?(Hash)
102
+ result[:responseMetadata] = @response_meta if @response_meta.is_a?(Hash)
103
+
104
+ JSON.dump(result)
105
+ end
106
+
107
+ def validate!
108
+ raise InteractionBuilderError.new("invalid request format, should be a hash") unless @request.is_a?(Hash)
109
+ raise InteractionBuilderError.new("invalid response format, should be a hash") unless @response.is_a?(Hash) || @response_meta.is_a?(Hash)
110
+ end
111
+
112
+ def execute(&block)
113
+ raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used)
114
+
115
+ validate!
116
+
117
+ pact_handle = init_pact
118
+ init_plugin!(pact_handle)
119
+
120
+ message_pact = PactFfi::SyncMessageConsumer.new_interaction(pact_handle, @description)
121
+ @provider_state_meta&.each_pair do |provider_state, meta|
122
+ if meta.present?
123
+ meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) }
124
+ else
125
+ PactFfi.given(message_pact, provider_state)
126
+ end
127
+ end
128
+ result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, @content_type, interaction_json)
129
+ if CREATE_INTERACTION_ERRORS[result].present?
130
+ error = CREATE_INTERACTION_ERRORS[result]
131
+ raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status])
132
+ end
133
+
134
+ mock_server = MockServer.create_for_transport!(pact: pact_handle, transport: @transport, host: @pact_config.mock_host, port: @pact_config.mock_port)
135
+
136
+ yield(message_pact, mock_server)
137
+
138
+ ensure
139
+ if mock_server.matched?
140
+ mock_server.write_pacts!(@pact_config.pact_dir)
141
+ else
142
+ msg = mismatches_error_msg(mock_server)
143
+ raise InteractionMismatchesError.new(msg)
144
+ end
145
+ @used = true
146
+ mock_server&.cleanup
147
+ PactFfi::PluginConsumer.cleanup_plugins(pact_handle)
148
+ PactFfi.free_pact_handle(pact_handle)
149
+ end
150
+
151
+ private
152
+
153
+ def mismatches_error_msg(mock_server)
154
+ rspec_example_desc = RSpec.current_example&.description
155
+ return "interaction for has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank?
156
+
157
+ "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}"
158
+ end
159
+
160
+ def init_pact
161
+ handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name)
162
+ PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
163
+ PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version)
164
+
165
+ Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level)
166
+
167
+ handle
168
+ end
169
+
170
+ def init_plugin!(pact_handle)
171
+ result = PactFfi::PluginConsumer.using_plugin(pact_handle, @plugin_name, @plugin_version)
172
+ return result if INIT_PLUGIN_ERRORS[result].blank?
173
+
174
+ error = INIT_PLUGIN_ERRORS[result]
175
+ raise PluginInitError.new("There was an error while trying to initialize plugin #{@plugin_name}/#{@plugin_version}", error[:reason], error[:status])
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Consumer
6
+ end
7
+ end
8
+ end