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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/lib/pact/v2/configuration.rb +23 -0
- data/lib/pact/v2/consumer/grpc_interaction_builder.rb +194 -0
- data/lib/pact/v2/consumer/http_interaction_builder.rb +162 -0
- data/lib/pact/v2/consumer/interaction_contents.rb +62 -0
- data/lib/pact/v2/consumer/message_interaction_builder.rb +287 -0
- data/lib/pact/v2/consumer/mock_server.rb +100 -0
- data/lib/pact/v2/consumer/pact_config/base.rb +24 -0
- data/lib/pact/v2/consumer/pact_config/grpc.rb +26 -0
- data/lib/pact/v2/consumer/pact_config/http.rb +55 -0
- data/lib/pact/v2/consumer/pact_config/message.rb +17 -0
- 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 +30 -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/consumer.rb +8 -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 +74 -0
- data/lib/pact/v2/matchers/v1/equality.rb +19 -0
- data/lib/pact/v2/matchers/v2/regex.rb +19 -0
- data/lib/pact/v2/matchers/v2/type.rb +17 -0
- data/lib/pact/v2/matchers/v3/boolean.rb +17 -0
- data/lib/pact/v2/matchers/v3/content_type.rb +32 -0
- data/lib/pact/v2/matchers/v3/date.rb +18 -0
- data/lib/pact/v2/matchers/v3/date_time.rb +18 -0
- data/lib/pact/v2/matchers/v3/decimal.rb +17 -0
- data/lib/pact/v2/matchers/v3/each.rb +42 -0
- data/lib/pact/v2/matchers/v3/include.rb +17 -0
- data/lib/pact/v2/matchers/v3/integer.rb +17 -0
- data/lib/pact/v2/matchers/v3/null.rb +16 -0
- data/lib/pact/v2/matchers/v3/number.rb +17 -0
- data/lib/pact/v2/matchers/v3/semver.rb +23 -0
- data/lib/pact/v2/matchers/v3/time.rb +18 -0
- data/lib/pact/v2/matchers/v3/values.rb +16 -0
- data/lib/pact/v2/matchers/v4/each_key.rb +26 -0
- data/lib/pact/v2/matchers/v4/each_key_value.rb +32 -0
- data/lib/pact/v2/matchers/v4/each_value.rb +33 -0
- data/lib/pact/v2/matchers/v4/not_empty.rb +24 -0
- data/lib/pact/v2/matchers/v4/status_code.rb +17 -0
- data/lib/pact/v2/matchers.rb +110 -0
- data/lib/pact/v2/native/blocking_verifier.rb +17 -0
- data/lib/pact/v2/native/logger.rb +25 -0
- data/lib/pact/v2/provider/async_message_verifier.rb +28 -0
- data/lib/pact/v2/provider/base_verifier.rb +242 -0
- data/lib/pact/v2/provider/grpc_verifier.rb +38 -0
- data/lib/pact/v2/provider/gruf_server.rb +75 -0
- data/lib/pact/v2/provider/http_server.rb +79 -0
- data/lib/pact/v2/provider/http_verifier.rb +43 -0
- data/lib/pact/v2/provider/message_provider_servlet.rb +79 -0
- data/lib/pact/v2/provider/mixed_verifier.rb +22 -0
- data/lib/pact/v2/provider/pact_broker_proxy.rb +66 -0
- data/lib/pact/v2/provider/pact_broker_proxy_runner.rb +77 -0
- data/lib/pact/v2/provider/pact_config/async.rb +29 -0
- data/lib/pact/v2/provider/pact_config/base.rb +101 -0
- data/lib/pact/v2/provider/pact_config/grpc.rb +26 -0
- data/lib/pact/v2/provider/pact_config/http.rb +27 -0
- data/lib/pact/v2/provider/pact_config/mixed.rb +39 -0
- data/lib/pact/v2/provider/pact_config.rb +26 -0
- data/lib/pact/v2/provider/provider_server_runner.rb +89 -0
- data/lib/pact/v2/provider/provider_state_configuration.rb +32 -0
- data/lib/pact/v2/provider/provider_state_servlet.rb +86 -0
- data/lib/pact/v2/provider.rb +8 -0
- data/lib/pact/v2/railtie.rb +13 -0
- data/lib/pact/v2/rspec/support/pact_consumer_helpers.rb +93 -0
- data/lib/pact/v2/rspec/support/pact_message_helpers.rb +42 -0
- data/lib/pact/v2/rspec/support/pact_provider_helpers.rb +129 -0
- data/lib/pact/v2/rspec/support/waterdrop/pact_waterdrop_client.rb +27 -0
- data/lib/pact/v2/rspec/support/webmock/webmock_helpers.rb +30 -0
- data/lib/pact/v2/rspec.rb +17 -0
- data/lib/pact/v2/tasks/pact.rake +13 -0
- data/lib/pact/v2.rb +71 -0
- data/lib/pact/version.rb +1 -1
- data/lib/pact.rb +6 -0
- data/pact.gemspec +54 -7
- 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
|