pact-v2 2.0.0.pre.preview1
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 +7 -0
- data/CHANGELOG.md +1321 -0
- data/LICENSE.txt +23 -0
- data/bin/pact +4 -0
- data/lib/pact/cli/generate_pact_docs.rb +4 -0
- data/lib/pact/cli/run_pact_verification.rb +99 -0
- data/lib/pact/cli/spec_criteria.rb +26 -0
- data/lib/pact/cli.rb +45 -0
- data/lib/pact/consumer/configuration/configuration_extensions.rb +90 -0
- data/lib/pact/consumer/configuration/dsl.rb +11 -0
- data/lib/pact/consumer/configuration/mock_service.rb +112 -0
- data/lib/pact/consumer/configuration/service_consumer.rb +51 -0
- data/lib/pact/consumer/configuration/service_provider.rb +40 -0
- data/lib/pact/consumer/configuration.rb +10 -0
- data/lib/pact/consumer/consumer_contract_builder.rb +82 -0
- data/lib/pact/consumer/consumer_contract_builders.rb +10 -0
- data/lib/pact/consumer/interaction_builder.rb +45 -0
- data/lib/pact/consumer/rspec.rb +35 -0
- data/lib/pact/consumer/spec_hooks.rb +40 -0
- data/lib/pact/consumer/world.rb +37 -0
- data/lib/pact/consumer.rb +7 -0
- data/lib/pact/doc/README.md +13 -0
- data/lib/pact/doc/doc_file.rb +40 -0
- data/lib/pact/doc/generate.rb +11 -0
- data/lib/pact/doc/generator.rb +82 -0
- data/lib/pact/doc/interaction_view_model.rb +124 -0
- data/lib/pact/doc/markdown/consumer_contract_renderer.rb +68 -0
- data/lib/pact/doc/markdown/generator.rb +26 -0
- data/lib/pact/doc/markdown/index_renderer.rb +43 -0
- data/lib/pact/doc/markdown/interaction.erb +14 -0
- data/lib/pact/doc/markdown/interaction_renderer.rb +43 -0
- data/lib/pact/doc/sort_interactions.rb +16 -0
- data/lib/pact/hal/authorization_header_redactor.rb +32 -0
- data/lib/pact/hal/entity.rb +110 -0
- data/lib/pact/hal/http_client.rb +128 -0
- data/lib/pact/hal/link.rb +112 -0
- data/lib/pact/hal/non_json_entity.rb +28 -0
- data/lib/pact/hash_refinements.rb +17 -0
- data/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb +112 -0
- data/lib/pact/pact_broker/fetch_pacts.rb +103 -0
- data/lib/pact/pact_broker/notices.rb +34 -0
- data/lib/pact/pact_broker/pact_selection_description.rb +66 -0
- data/lib/pact/pact_broker.rb +25 -0
- data/lib/pact/project_root.rb +7 -0
- data/lib/pact/provider/configuration/configuration_extension.rb +69 -0
- data/lib/pact/provider/configuration/dsl.rb +18 -0
- data/lib/pact/provider/configuration/message_provider_dsl.rb +63 -0
- data/lib/pact/provider/configuration/pact_verification.rb +48 -0
- data/lib/pact/provider/configuration/pact_verification_from_broker.rb +126 -0
- data/lib/pact/provider/configuration/service_provider_config.rb +32 -0
- data/lib/pact/provider/configuration/service_provider_dsl.rb +107 -0
- data/lib/pact/provider/configuration.rb +7 -0
- data/lib/pact/provider/context.rb +0 -0
- data/lib/pact/provider/help/console_text.rb +76 -0
- data/lib/pact/provider/help/content.rb +38 -0
- data/lib/pact/provider/help/pact_diff.rb +43 -0
- data/lib/pact/provider/help/prompt_text.rb +49 -0
- data/lib/pact/provider/help/write.rb +56 -0
- data/lib/pact/provider/matchers/messages.rb +66 -0
- data/lib/pact/provider/pact_helper_locator.rb +24 -0
- data/lib/pact/provider/pact_source.rb +40 -0
- data/lib/pact/provider/pact_spec_runner.rb +188 -0
- data/lib/pact/provider/pact_uri.rb +55 -0
- data/lib/pact/provider/pact_verification.rb +17 -0
- data/lib/pact/provider/print_missing_provider_states.rb +35 -0
- data/lib/pact/provider/request.rb +77 -0
- data/lib/pact/provider/rspec/backtrace_formatter.rb +43 -0
- data/lib/pact/provider/rspec/calculate_exit_code.rb +18 -0
- data/lib/pact/provider/rspec/custom_options_file +0 -0
- data/lib/pact/provider/rspec/formatter_rspec_2.rb +76 -0
- data/lib/pact/provider/rspec/formatter_rspec_3.rb +195 -0
- data/lib/pact/provider/rspec/json_formatter.rb +100 -0
- data/lib/pact/provider/rspec/matchers.rb +80 -0
- data/lib/pact/provider/rspec/pact_broker_formatter.rb +76 -0
- data/lib/pact/provider/rspec.rb +234 -0
- data/lib/pact/provider/state/provider_state.rb +180 -0
- data/lib/pact/provider/state/provider_state_configured_modules.rb +15 -0
- data/lib/pact/provider/state/provider_state_manager.rb +42 -0
- data/lib/pact/provider/state/provider_state_proxy.rb +39 -0
- data/lib/pact/provider/state/set_up.rb +13 -0
- data/lib/pact/provider/state/tear_down.rb +13 -0
- data/lib/pact/provider/test_methods.rb +77 -0
- data/lib/pact/provider/verification_report.rb +36 -0
- data/lib/pact/provider/verification_results/create.rb +88 -0
- data/lib/pact/provider/verification_results/publish.rb +143 -0
- data/lib/pact/provider/verification_results/publish_all.rb +50 -0
- data/lib/pact/provider/verification_results/verification_result.rb +40 -0
- data/lib/pact/provider/world.rb +50 -0
- data/lib/pact/provider.rb +3 -0
- data/lib/pact/retry.rb +37 -0
- data/lib/pact/tasks/task_helper.rb +62 -0
- data/lib/pact/tasks/verification_task.rb +95 -0
- data/lib/pact/tasks.rb +2 -0
- data/lib/pact/templates/help.erb +22 -0
- data/lib/pact/templates/provider_state.erb +14 -0
- data/lib/pact/utils/metrics.rb +100 -0
- data/lib/pact/utils/string.rb +35 -0
- data/lib/pact/v2/configuration.rb +23 -0
- data/lib/pact/v2/consumer/grpc_interaction_builder.rb +187 -0
- data/lib/pact/v2/consumer/http_interaction_builder.rb +163 -0
- data/lib/pact/v2/consumer/interaction_contents.rb +54 -0
- data/lib/pact/v2/consumer/message_interaction_builder.rb +280 -0
- data/lib/pact/v2/consumer/mock_server.rb +99 -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.rb +24 -0
- data/lib/pact/v2/consumer.rb +8 -0
- data/lib/pact/v2/matchers/base.rb +67 -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/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/number.rb +17 -0
- data/lib/pact/v2/matchers/v3/time.rb +18 -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 +17 -0
- data/lib/pact/v2/matchers.rb +94 -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 +71 -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 +80 -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/version.rb +8 -0
- data/lib/pact/v2.rb +71 -0
- data/lib/pact/version.rb +4 -0
- data/lib/pact.rb +13 -0
- data/lib/tasks/pact.rake +34 -0
- data/pact.gemspec +106 -0
- metadata +529 -0
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/verifier"
|
4
|
+
require "pact/v2/native/logger"
|
5
|
+
require "pact/v2/native/blocking_verifier"
|
6
|
+
|
7
|
+
module Pact
|
8
|
+
module V2
|
9
|
+
module Provider
|
10
|
+
class BaseVerifier
|
11
|
+
PROVIDER_TRANSPORT_TYPE = nil
|
12
|
+
attr_reader :logger
|
13
|
+
|
14
|
+
class VerificationError < Pact::V2::FfiError; end
|
15
|
+
|
16
|
+
class VerifierError < Pact::V2::Error; end
|
17
|
+
|
18
|
+
DEFAULT_CONSUMER_SELECTORS = {}
|
19
|
+
|
20
|
+
# https://docs.rs/pact_ffi/0.4.17/pact_ffi/verifier/fn.pactffi_verify.html#errors
|
21
|
+
VERIFICATION_ERRORS = {
|
22
|
+
1 => {reason: :verification_failed, status: 1, description: "The verification process failed, see output for errors"},
|
23
|
+
2 => {reason: :null_pointer, status: 2, description: "A null pointer was received"},
|
24
|
+
3 => {reason: :internal_error, status: 3, description: "The method panicked"},
|
25
|
+
4 => {reason: :invalid_arguments, status: 4, description: "Invalid arguments were provided to the verification process"}
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
# env below are set up by pipeline-builder
|
29
|
+
# see paas/cicd/images/pact/pipeline-builder/-/blob/master/internal/commands/consumers-pipeline/ruby.go
|
30
|
+
def initialize(pact_config, mixed_config = nil)
|
31
|
+
raise ArgumentError, "pact_config must be a subclass of Pact::V2::Provider::PactConfig::Base" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Base)
|
32
|
+
@pact_config = pact_config
|
33
|
+
@mixed_config = mixed_config
|
34
|
+
@logger = Logger.new($stdout)
|
35
|
+
end
|
36
|
+
|
37
|
+
def verify!
|
38
|
+
raise VerifierError.new("interaction is designed to be used one-time only") if defined?(@used)
|
39
|
+
|
40
|
+
# if consumer_selectors.blank?
|
41
|
+
# logger.info("[verifier] does not need to verify consumer #{@pact_config.consumer_name}")
|
42
|
+
# return
|
43
|
+
# end
|
44
|
+
|
45
|
+
exception = nil
|
46
|
+
pact_handle = init_pact
|
47
|
+
|
48
|
+
start_servers!
|
49
|
+
|
50
|
+
logger.info("[verifier] starting provider verification")
|
51
|
+
|
52
|
+
result = Pact::V2::Native::BlockingVerifier.execute(pact_handle)
|
53
|
+
if VERIFICATION_ERRORS[result].present?
|
54
|
+
error = VERIFICATION_ERRORS[result]
|
55
|
+
exception = VerificationError.new("There was an error while trying to verify provider \"#{@pact_config.provider_name}\"", error[:reason], error[:status])
|
56
|
+
end
|
57
|
+
ensure
|
58
|
+
@used = true
|
59
|
+
PactFfi::Verifier.shutdown(pact_handle) if pact_handle
|
60
|
+
stop_servers
|
61
|
+
@grpc_server.stop if @grpc_server
|
62
|
+
raise exception if exception
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def create_c_pointer_array_from_string_array(string_array)
|
68
|
+
pointers = string_array.map { |str| FFI::MemoryPointer.from_string(str) }
|
69
|
+
array_pointer = FFI::MemoryPointer.new(:pointer, pointers.size)
|
70
|
+
pointers.each_with_index do |ptr, index|
|
71
|
+
array_pointer[index].put_pointer(0, ptr)
|
72
|
+
end
|
73
|
+
array_pointer
|
74
|
+
end
|
75
|
+
|
76
|
+
def bool_to_int(value)
|
77
|
+
value ? 1 : 0
|
78
|
+
end
|
79
|
+
|
80
|
+
def init_pact
|
81
|
+
handle = PactFfi::Verifier.new_for_application("pact-ruby-v2", PactFfi.version)
|
82
|
+
set_provider_info(handle)
|
83
|
+
|
84
|
+
if defined?(@mixed_config.grpc_config) && @mixed_config.grpc_config
|
85
|
+
@grpc_server = GrufServer.new(host: "127.0.0.1:#{@mixed_config.grpc_config.grpc_port}", services: @mixed_config.grpc_config.grpc_services)
|
86
|
+
@grpc_server.start
|
87
|
+
PactFfi::Verifier.add_provider_transport(handle, "grpc", @mixed_config.grpc_config.grpc_port, "", "")
|
88
|
+
end
|
89
|
+
|
90
|
+
if defined?(@mixed_config.async_config) && @mixed_config.async_config
|
91
|
+
setup_uri = URI(@mixed_config.async_config.message_setup_url)
|
92
|
+
PactFfi::Verifier.add_provider_transport(handle, "message", setup_uri.port, setup_uri.path, "")
|
93
|
+
end
|
94
|
+
|
95
|
+
# todo: add http transport?
|
96
|
+
|
97
|
+
PactFfi::Verifier.set_provider_state(handle, @pact_config.provider_setup_url, 1, 1)
|
98
|
+
PactFfi::Verifier.set_verification_options(handle, 0, 10000)
|
99
|
+
# pactffi_verifier_set_publish_options(
|
100
|
+
# handle: *mut VerifierHandle,
|
101
|
+
# provider_version: *const c_char,
|
102
|
+
# build_url: *const c_char,
|
103
|
+
# provider_tags: *const *const c_char,
|
104
|
+
# provider_tags_len: c_ushort,
|
105
|
+
# provider_branch: *const c_char,
|
106
|
+
# )
|
107
|
+
c_provider_version_tags = create_c_pointer_array_from_string_array(@pact_config.provider_version_tags)
|
108
|
+
c_provider_version_tags_size = @pact_config.provider_version_tags.size
|
109
|
+
c_consumer_version_tags = create_c_pointer_array_from_string_array(@pact_config.consumer_version_tags)
|
110
|
+
c_consumer_version_tags_size = @pact_config.consumer_version_tags.size
|
111
|
+
|
112
|
+
if @pact_config.provider_build_uri.present?
|
113
|
+
begin
|
114
|
+
URI.parse(@pact_config.provider_build_uri)
|
115
|
+
rescue URI::InvalidURIError
|
116
|
+
raise VerifierError.new("provider_build_uri is not a valid URI")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
if @pact_config.publish_verification_results == true
|
121
|
+
if @pact_config.provider_version
|
122
|
+
PactFfi::Verifier.set_publish_options(handle, @pact_config.provider_version, @pact_config.provider_build_uri, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch)
|
123
|
+
else
|
124
|
+
logger.warn("[verifier] - unable to publish verification results as provider version is not set")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
configure_verification_source(handle, c_provider_version_tags, c_provider_version_tags_size, c_consumer_version_tags, c_consumer_version_tags_size)
|
129
|
+
|
130
|
+
PactFfi::Verifier.set_no_pacts_is_error(handle, bool_to_int(@pact_config.fail_if_no_pacts_found))
|
131
|
+
|
132
|
+
add_provider_transport(handle)
|
133
|
+
|
134
|
+
# the core doesnt pick up these env vars, so we need to set them here
|
135
|
+
# https://github.com/pact-foundation/pact-reference/issues/451#issuecomment-2338130587
|
136
|
+
# PACT_DESCRIPTION
|
137
|
+
# Only validate interactions whose descriptions match this filter (regex format)
|
138
|
+
# PACT_PROVIDER_STATE
|
139
|
+
# Only validate interactions whose provider states match this filter (regex format)
|
140
|
+
# PACT_PROVIDER_NO_STATE
|
141
|
+
# Only validate interactions that have no defined provider state (true or false)
|
142
|
+
PactFfi::Verifier.set_filter_info(
|
143
|
+
handle,
|
144
|
+
ENV["PACT_DESCRIPTION"] || nil,
|
145
|
+
ENV["PACT_PROVIDER_STATE"] || nil,
|
146
|
+
bool_to_int(ENV["PACT_PROVIDER_NO_STATE"] || false)
|
147
|
+
)
|
148
|
+
|
149
|
+
Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level)
|
150
|
+
|
151
|
+
logger.info("[verifier] verification initialized for provider #{@pact_config.provider_name}, version #{@pact_config.provider_version}, transport #{self.class::PROVIDER_TRANSPORT_TYPE}")
|
152
|
+
|
153
|
+
handle
|
154
|
+
end
|
155
|
+
|
156
|
+
def set_provider_info(pact_handle)
|
157
|
+
# pub extern "C" fn pactffi_verifier_set_provider_info(
|
158
|
+
# handle: *mut VerifierHandle,
|
159
|
+
# name: *const c_char,
|
160
|
+
# scheme: *const c_char,
|
161
|
+
# host: *const c_char,
|
162
|
+
# port: c_ushort,
|
163
|
+
# path: *const c_char,
|
164
|
+
# ) {
|
165
|
+
PactFfi::Verifier.set_provider_info(pact_handle, @pact_config.provider_name, "", "", 0, "")
|
166
|
+
end
|
167
|
+
|
168
|
+
def add_provider_transport(pact_handle)
|
169
|
+
raise Pact::V2::ImplementationRequired, "Implement #add_provider_transport in a subclass"
|
170
|
+
end
|
171
|
+
|
172
|
+
def start_servers!
|
173
|
+
logger.info("[verifier] starting services")
|
174
|
+
|
175
|
+
@servers_started = true
|
176
|
+
@pact_config.start_servers
|
177
|
+
end
|
178
|
+
|
179
|
+
def stop_servers
|
180
|
+
return unless @servers_started
|
181
|
+
|
182
|
+
logger.info("[verifier] stopping services")
|
183
|
+
|
184
|
+
@pact_config.stop_servers
|
185
|
+
end
|
186
|
+
|
187
|
+
def configure_verification_source(handle, c_provider_version_tags, c_provider_version_tags_size, c_consumer_version_tags, c_consumer_version_tags_size)
|
188
|
+
logger.info("[verifier] configuring verification source")
|
189
|
+
if @pact_config.pact_broker_proxy_url.blank? && @pact_config.pact_uri.blank?
|
190
|
+
# todo support non rail apps
|
191
|
+
path = @pact_config.pact_dir || (defined?(Rails) ? Rails.root.join("pacts").to_s : "pacts")
|
192
|
+
logger.info("[verifier] pact broker url or pact uri is not set, using directory #{path} as a verification source")
|
193
|
+
return PactFfi::Verifier.add_directory_source(handle, path)
|
194
|
+
end
|
195
|
+
|
196
|
+
if @pact_config.pact_uri.present?
|
197
|
+
if @pact_config.pact_uri.start_with?("http")
|
198
|
+
logger.info("[verifier] using pact uri #{@pact_config.pact_uri} as a verification source")
|
199
|
+
PactFfi::Verifier.url_source(handle, @pact_config.pact_uri, @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token)
|
200
|
+
else
|
201
|
+
logger.info("[verifier] using pact file #{@pact_config.pact_uri} as a verification source")
|
202
|
+
PactFfi::Verifier.add_file_source(handle, @pact_config.pact_uri)
|
203
|
+
end
|
204
|
+
else
|
205
|
+
logger.info("[verifier] using pact broker url #{@pact_config.broker_url} with consumer selectors: #{JSON.dump(consumer_selectors)} as a verification source")
|
206
|
+
consumer_selectors = [] if consumer_selectors.nil?
|
207
|
+
filters = consumer_selectors.map do |selector|
|
208
|
+
FFI::MemoryPointer.from_string(JSON.dump(selector).to_s)
|
209
|
+
end
|
210
|
+
filters_ptr = FFI::MemoryPointer.new(:pointer, filters.size + 1)
|
211
|
+
filters_ptr.write_array_of_pointer(filters)
|
212
|
+
PactFfi::Verifier.broker_source_with_selectors(handle, @pact_config.pact_broker_proxy_url, @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token, bool_to_int(@pact_config.enable_pending), @pact_config.include_wip_pacts_since, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch, filters_ptr, consumer_selectors.size, c_consumer_version_tags, c_consumer_version_tags_size)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def consumer_selectors
|
217
|
+
(!@pact_config.consumer_version_selectors.empty? && @pact_config.consumer_version_selectors) || @consumer_selectors if @pact_config.consumer_version_selectors
|
218
|
+
end
|
219
|
+
|
220
|
+
def build_consumer_selectors(verify_only, consumer_name, consumer_branch)
|
221
|
+
# if verify_only and consumer_name are defined - select only needed consumer
|
222
|
+
if verify_only.present?
|
223
|
+
# select proper consumer branch if defined
|
224
|
+
if consumer_name.present?
|
225
|
+
return [] unless verify_only.include?(consumer_name)
|
226
|
+
return [{"branch" => consumer_branch, "consumer" => consumer_name}] if consumer_branch.present?
|
227
|
+
return [DEFAULT_CONSUMER_SELECTORS.merge("consumer" => consumer_name)]
|
228
|
+
end
|
229
|
+
# or default selectors
|
230
|
+
return verify_only.map { |name| DEFAULT_CONSUMER_SELECTORS.merge("consumer" => name) }
|
231
|
+
end
|
232
|
+
|
233
|
+
# select provided consumer_name
|
234
|
+
return [{"branch" => consumer_branch, "consumer" => consumer_name}] if consumer_name.present? && consumer_branch.present?
|
235
|
+
return [DEFAULT_CONSUMER_SELECTORS.merge("consumer" => consumer_name)] if consumer_name.present?
|
236
|
+
|
237
|
+
[DEFAULT_CONSUMER_SELECTORS]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/verifier"
|
4
|
+
require "pact/v2/native/logger"
|
5
|
+
|
6
|
+
module Pact
|
7
|
+
module V2
|
8
|
+
module Provider
|
9
|
+
class GrpcVerifier < BaseVerifier
|
10
|
+
PROVIDER_TRANSPORT_TYPE = "grpc"
|
11
|
+
|
12
|
+
def initialize(pact_config, mixed_config = nil)
|
13
|
+
super
|
14
|
+
|
15
|
+
raise ArgumentError, "pact_config must be an instance of Pact::V2::Provider::PactConfig::Grpc" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Grpc)
|
16
|
+
@grpc_server = GrufServer.new(host: "127.0.0.1:#{@pact_config.grpc_port}", services: @pact_config.grpc_services)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def add_provider_transport(pact_handle)
|
22
|
+
PactFfi::Verifier.add_provider_transport(pact_handle, PROVIDER_TRANSPORT_TYPE, @pact_config.grpc_port, "", "")
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def start_servers!
|
27
|
+
super
|
28
|
+
@grpc_server.start
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop_servers
|
32
|
+
super
|
33
|
+
@grpc_server.stop
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module V2
|
5
|
+
module Provider
|
6
|
+
# inspired by Gruf::Cli::Executor
|
7
|
+
class GrufServer
|
8
|
+
SERVER_STOP_TIMEOUT_SEC = 15
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@options = options
|
12
|
+
|
13
|
+
setup!
|
14
|
+
|
15
|
+
@server_pid = nil
|
16
|
+
|
17
|
+
@services = @options[:services].is_a?(Array) ? @options[:services] : []
|
18
|
+
@logger = @options[:logger] || ::Logger.new($stdout)
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
raise "server already running, stop server before starting new one" if @thread
|
23
|
+
|
24
|
+
@logger.info("[gruf] starting standalone server with options: #{@options}")
|
25
|
+
|
26
|
+
@server = Gruf::Server.new(Gruf.server_options)
|
27
|
+
@services.each { |s| @server.add_service(s) } if @services.any?
|
28
|
+
@thread = Thread.new do
|
29
|
+
@logger.debug "[gruf] starting grpc server"
|
30
|
+
@server.start!
|
31
|
+
end
|
32
|
+
@server.server.wait_till_running(10)
|
33
|
+
|
34
|
+
@logger.info("[gruf] standalone server started")
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop
|
38
|
+
@logger.info("[gruf] stopping standalone server")
|
39
|
+
|
40
|
+
@server&.server&.stop
|
41
|
+
@thread&.join(SERVER_STOP_TIMEOUT_SEC)
|
42
|
+
@thread&.kill
|
43
|
+
|
44
|
+
@logger.info("[gruf] standalone server stopped")
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Run the server
|
49
|
+
#
|
50
|
+
def run
|
51
|
+
start
|
52
|
+
|
53
|
+
yield
|
54
|
+
rescue => e
|
55
|
+
@logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}")
|
56
|
+
raise
|
57
|
+
ensure
|
58
|
+
stop
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def setup!
|
64
|
+
Gruf.server_binding_url = @options[:host] if @options[:host]
|
65
|
+
if @options[:suppress_default_interceptors]
|
66
|
+
Gruf.interceptors.remove(Gruf::Interceptors::ActiveRecord::ConnectionReset)
|
67
|
+
Gruf.interceptors.remove(Gruf::Interceptors::Instrumentation::OutputMetadataTimer)
|
68
|
+
end
|
69
|
+
Gruf.backtrace_on_error = true if @options[:backtrace_on_error]
|
70
|
+
Gruf.health_check_enabled = true if @options[:health_check]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module V2
|
5
|
+
module Provider
|
6
|
+
# inspired by Gruf::Cli::Executor
|
7
|
+
class HttpServer
|
8
|
+
SERVER_STOP_TIMEOUT_SEC = 15
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@options = options
|
12
|
+
|
13
|
+
@server_pid = nil
|
14
|
+
|
15
|
+
@host = @options[:host] || "localhost"
|
16
|
+
@logger = @options[:logger] || ::Logger.new($stdout)
|
17
|
+
# allow any rack based app to be passed in, otherwise
|
18
|
+
# we will load a Rails.application
|
19
|
+
# allows for backwards compat with pact-ruby v1
|
20
|
+
@app = @options[:app] || nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
raise "server already running, stop server before starting new one" if @thread
|
25
|
+
|
26
|
+
@logger.info("[webrick] starting server with options: #{@options}")
|
27
|
+
|
28
|
+
@thread = Thread.new do
|
29
|
+
@logger.debug "[webrick] starting http server"
|
30
|
+
|
31
|
+
# TODO: load from config.ru, if not rails and no app provided?
|
32
|
+
# Rack 2/3 compatibility
|
33
|
+
begin
|
34
|
+
require 'rack/handler/webrick'
|
35
|
+
handler = ::Rack::Handler::WEBrick
|
36
|
+
rescue LoadError
|
37
|
+
require 'rackup/handler/webrick'
|
38
|
+
handler = Class.new(Rackup::Handler::WEBrick)
|
39
|
+
end
|
40
|
+
handler.run(@app || (defined?(Rails) ? Rails.application : nil),
|
41
|
+
Host: @options[:host],
|
42
|
+
Port: @options[:port],
|
43
|
+
Logger: @logger,
|
44
|
+
StartCallback: -> { @started = true }) do |server|
|
45
|
+
@server = server
|
46
|
+
end
|
47
|
+
end
|
48
|
+
sleep 0.001 until @started
|
49
|
+
|
50
|
+
@logger.info("[webrick] server started")
|
51
|
+
end
|
52
|
+
|
53
|
+
def stop
|
54
|
+
@logger.info("[webrick] stopping server")
|
55
|
+
|
56
|
+
@server&.shutdown
|
57
|
+
@thread&.join(SERVER_STOP_TIMEOUT_SEC)
|
58
|
+
@thread&.kill
|
59
|
+
|
60
|
+
@logger.info("[webrick] server stopped")
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Run the server
|
65
|
+
#
|
66
|
+
def run
|
67
|
+
start
|
68
|
+
|
69
|
+
yield
|
70
|
+
rescue => e
|
71
|
+
@logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}")
|
72
|
+
raise
|
73
|
+
ensure
|
74
|
+
stop
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/verifier"
|
4
|
+
require "pact/v2/native/logger"
|
5
|
+
|
6
|
+
module Pact
|
7
|
+
module V2
|
8
|
+
module Provider
|
9
|
+
class HttpVerifier < BaseVerifier
|
10
|
+
PROVIDER_TRANSPORT_TYPE = "http"
|
11
|
+
|
12
|
+
def initialize(pact_config, mixed_config = nil)
|
13
|
+
super
|
14
|
+
|
15
|
+
raise ArgumentError, "pact_config must be an instance of Pact::V2::Provider::PactConfig::Http" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Http)
|
16
|
+
@http_server = HttpServer.new(host: "127.0.0.1", port: @pact_config.http_port, app: @pact_config.app)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def set_provider_info(pact_handle)
|
22
|
+
PactFfi::Verifier.set_provider_info(pact_handle, @pact_config.provider_name, "", "", @pact_config.http_port, "")
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_provider_transport(pact_handle)
|
26
|
+
# The http transport is already added when the `set_provider_info` method is called,
|
27
|
+
# so we don't need to explicitly add the transport here
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def start_servers!
|
32
|
+
super
|
33
|
+
@http_server.start
|
34
|
+
end
|
35
|
+
|
36
|
+
def stop_servers
|
37
|
+
super
|
38
|
+
@http_server.stop
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webrick"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module V2
|
7
|
+
module Provider
|
8
|
+
class MessageProviderServlet < WEBrick::HTTPServlet::ProcHandler
|
9
|
+
attr_reader :logger
|
10
|
+
|
11
|
+
CONTENT_TYPE_JSON = "application/json"
|
12
|
+
CONTENT_TYPE_PROTO = "application/protobuf"
|
13
|
+
METADATA_HEADER = "pact-message-metadata"
|
14
|
+
|
15
|
+
def initialize(logger: Logger.new($stdout))
|
16
|
+
super(build_proc)
|
17
|
+
|
18
|
+
@message_handlers = {}
|
19
|
+
|
20
|
+
@logger = logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_message_handler(name, &block)
|
24
|
+
raise "message handler for #{name} already configured" if @message_handlers[name].present?
|
25
|
+
|
26
|
+
@message_handlers[name] = {proc: block}
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_proc
|
32
|
+
proc do |request, response|
|
33
|
+
# {"description":"message: ","providerStates":[{"name":"pet exists","params":{"pet_id":1}}]}
|
34
|
+
data = JSON.parse(request.body)
|
35
|
+
|
36
|
+
description = data["description"]
|
37
|
+
provider_states = data["providerStates"]
|
38
|
+
|
39
|
+
body, metadata = handle(description, provider_states)
|
40
|
+
|
41
|
+
response.status = 200
|
42
|
+
if body.is_a?(String)
|
43
|
+
# protobuf-serialized body
|
44
|
+
response.body = body
|
45
|
+
response.content_type = metadata[:content_type] || CONTENT_TYPE_PROTO
|
46
|
+
else
|
47
|
+
response.body = body.to_json
|
48
|
+
response.content_type = CONTENT_TYPE_JSON
|
49
|
+
end
|
50
|
+
response[METADATA_HEADER] = Base64.urlsafe_encode64(metadata.to_json)
|
51
|
+
rescue JSON::ParserError => ex
|
52
|
+
logger.error("cannot parse request: #{ex.message}")
|
53
|
+
response.status = 500
|
54
|
+
rescue => ex
|
55
|
+
logger.error("cannot handle message request: #{ex.message}")
|
56
|
+
response.status = 500
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def handle(description, provider_states)
|
61
|
+
handler = find_handler_for(description)
|
62
|
+
return {}, {} unless handler
|
63
|
+
|
64
|
+
body, metadata = handler[:proc].call(provider_states&.first || {})
|
65
|
+
unless metadata[:content_type]
|
66
|
+
# try to find content-type in provider states
|
67
|
+
content_type = provider_states&.filter_map { |state| state.dig("params", "contentType") }&.first
|
68
|
+
metadata[:content_type] = content_type if content_type
|
69
|
+
end
|
70
|
+
[body, metadata]
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_handler_for(description)
|
74
|
+
@message_handlers[description]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# # frozen_string_literal: true
|
2
|
+
module Pact
|
3
|
+
module V2
|
4
|
+
module Provider
|
5
|
+
# MixedVerifier coordinates verification for all present configs (async, grpc, http)
|
6
|
+
class MixedVerifier
|
7
|
+
attr_reader :mixed_config, :verifiers
|
8
|
+
|
9
|
+
def initialize(mixed_config)
|
10
|
+
unless mixed_config.is_a?(::Pact::V2::Provider::PactConfig::Mixed)
|
11
|
+
raise ArgumentError, "mixed_config must be a PactConfig::Mixed"
|
12
|
+
end
|
13
|
+
@mixed_config = mixed_config
|
14
|
+
@verifiers = []
|
15
|
+
@verifiers << AsyncMessageVerifier.new(mixed_config.async_config) if mixed_config.async_config
|
16
|
+
@verifiers << GrpcVerifier.new(mixed_config.grpc_config) if mixed_config.grpc_config
|
17
|
+
@verifiers << HttpVerifier.new(mixed_config.http_config) if mixed_config.http_config
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack-proxy"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module V2
|
7
|
+
module Provider
|
8
|
+
class PactBrokerProxy < Rack::Proxy
|
9
|
+
attr_reader :backend_uri, :path, :logger
|
10
|
+
|
11
|
+
# e.g. /pacts/provider/paas-stand-seeker/consumer/paas-stand-placer/pact-version/2967a9343bd8fdd28a286c4b8322380020618892/metadata/c1tdW2VdPXByb2R1Y3Rpb24mc1tdW2N2XT03MzIy
|
12
|
+
PACT_FILE_REQUEST_PATH_REGEX = %r{/pacts/provider/.+?/consumer/.+?/pact-version/.+}.freeze
|
13
|
+
|
14
|
+
def initialize(app = nil, opts = {})
|
15
|
+
super
|
16
|
+
@backend_uri = URI(opts[:backend])
|
17
|
+
@path = nil
|
18
|
+
@logger = opts[:logger] || Logger.new($stdout)
|
19
|
+
end
|
20
|
+
|
21
|
+
def perform_request(env)
|
22
|
+
request = Rack::Request.new(env)
|
23
|
+
env["rack.timeout"] ||= ENV.fetch("PACT_BROKER_REQUEST_TIMEOUT", 5).to_i
|
24
|
+
@path = request.path
|
25
|
+
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def rewrite_env(env)
|
30
|
+
env["HTTP_HOST"] = backend_uri.host
|
31
|
+
env
|
32
|
+
end
|
33
|
+
|
34
|
+
def rewrite_response(triplet)
|
35
|
+
status, headers, body = triplet
|
36
|
+
|
37
|
+
if status == "200" && PACT_FILE_REQUEST_PATH_REGEX.match?(path)
|
38
|
+
patched_body = patch_response(body.first)
|
39
|
+
|
40
|
+
# we need to recalculate content length
|
41
|
+
headers[Rack::CONTENT_LENGTH] = patched_body.bytesize.to_s
|
42
|
+
|
43
|
+
return [status, headers, [patched_body]]
|
44
|
+
end
|
45
|
+
|
46
|
+
triplet
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def patch_response(raw_body)
|
52
|
+
parsed_body = JSON.parse(raw_body)
|
53
|
+
|
54
|
+
return body if parsed_body["consumer"].blank? || parsed_body["provider"].blank?
|
55
|
+
return body if parsed_body["interactions"].blank?
|
56
|
+
|
57
|
+
|
58
|
+
JSON.generate(parsed_body)
|
59
|
+
rescue JSON::ParserError => ex
|
60
|
+
logger.error("cannot parse broker response: #{ex.message}")
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def set_description_prefix(interaction, prefix)
|
65
|
+
orig_description = interaction["description"]
|
66
|
+
interaction["description"] = "#{prefix} #{orig_description}" unless orig_description.start_with?(prefix)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|