sbmt-pact 0.12.0
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/.rspec +3 -0
- data/.rubocop.yml +62 -0
- data/Appraisals +23 -0
- data/CHANGELOG.md +96 -0
- data/Dockerfile +14 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +212 -0
- data/Rakefile +12 -0
- data/dip.yml +86 -0
- data/docker-compose.yml +40 -0
- data/docs/sbmt-pact-arch.png +0 -0
- data/lefthook-local.dip_example.yml +4 -0
- data/lefthook.yml +6 -0
- data/lib/sbmt/pact/configuration.rb +23 -0
- data/lib/sbmt/pact/consumer/grpc_interaction_builder.rb +193 -0
- data/lib/sbmt/pact/consumer/http_interaction_builder.rb +149 -0
- data/lib/sbmt/pact/consumer/interaction_contents.rb +47 -0
- data/lib/sbmt/pact/consumer/message_interaction_builder.rb +285 -0
- data/lib/sbmt/pact/consumer/mock_server.rb +92 -0
- data/lib/sbmt/pact/consumer/pact_config/base.rb +24 -0
- data/lib/sbmt/pact/consumer/pact_config/grpc.rb +26 -0
- data/lib/sbmt/pact/consumer/pact_config/http.rb +26 -0
- data/lib/sbmt/pact/consumer/pact_config/message.rb +17 -0
- data/lib/sbmt/pact/consumer/pact_config.rb +24 -0
- data/lib/sbmt/pact/consumer.rb +8 -0
- data/lib/sbmt/pact/matchers/base.rb +67 -0
- data/lib/sbmt/pact/matchers/v1/equality.rb +19 -0
- data/lib/sbmt/pact/matchers/v2/regex.rb +19 -0
- data/lib/sbmt/pact/matchers/v2/type.rb +17 -0
- data/lib/sbmt/pact/matchers/v3/boolean.rb +17 -0
- data/lib/sbmt/pact/matchers/v3/date.rb +18 -0
- data/lib/sbmt/pact/matchers/v3/date_time.rb +18 -0
- data/lib/sbmt/pact/matchers/v3/decimal.rb +17 -0
- data/lib/sbmt/pact/matchers/v3/each.rb +42 -0
- data/lib/sbmt/pact/matchers/v3/include.rb +17 -0
- data/lib/sbmt/pact/matchers/v3/integer.rb +17 -0
- data/lib/sbmt/pact/matchers/v3/number.rb +17 -0
- data/lib/sbmt/pact/matchers/v3/time.rb +18 -0
- data/lib/sbmt/pact/matchers/v4/each_key.rb +26 -0
- data/lib/sbmt/pact/matchers/v4/each_key_value.rb +32 -0
- data/lib/sbmt/pact/matchers/v4/each_value.rb +33 -0
- data/lib/sbmt/pact/matchers/v4/not_empty.rb +17 -0
- data/lib/sbmt/pact/matchers.rb +94 -0
- data/lib/sbmt/pact/native/blocking_verifier.rb +17 -0
- data/lib/sbmt/pact/native/logger.rb +25 -0
- data/lib/sbmt/pact/provider/async_message_verifier.rb +32 -0
- data/lib/sbmt/pact/provider/base_verifier.rb +158 -0
- data/lib/sbmt/pact/provider/grpc_verifier.rb +42 -0
- data/lib/sbmt/pact/provider/gruf_server.rb +75 -0
- data/lib/sbmt/pact/provider/http_server.rb +66 -0
- data/lib/sbmt/pact/provider/http_verifier.rb +46 -0
- data/lib/sbmt/pact/provider/message_provider_servlet.rb +80 -0
- data/lib/sbmt/pact/provider/pact_broker_proxy.rb +85 -0
- data/lib/sbmt/pact/provider/pact_broker_proxy_runner.rb +71 -0
- data/lib/sbmt/pact/provider/pact_config/async.rb +25 -0
- data/lib/sbmt/pact/provider/pact_config/base.rb +92 -0
- data/lib/sbmt/pact/provider/pact_config/grpc.rb +30 -0
- data/lib/sbmt/pact/provider/pact_config/http.rb +25 -0
- data/lib/sbmt/pact/provider/pact_config.rb +24 -0
- data/lib/sbmt/pact/provider/provider_server_runner.rb +89 -0
- data/lib/sbmt/pact/provider/provider_state_configuration.rb +32 -0
- data/lib/sbmt/pact/provider/provider_state_servlet.rb +84 -0
- data/lib/sbmt/pact/provider.rb +8 -0
- data/lib/sbmt/pact/railtie.rb +13 -0
- data/lib/sbmt/pact/rspec/support/pact_consumer_helpers.rb +46 -0
- data/lib/sbmt/pact/rspec/support/pact_message_helpers.rb +42 -0
- data/lib/sbmt/pact/rspec/support/pact_provider_helpers.rb +87 -0
- data/lib/sbmt/pact/rspec/support/waterdrop/pact_waterdrop_client.rb +27 -0
- data/lib/sbmt/pact/rspec/support/webmock/webmock_helpers.rb +30 -0
- data/lib/sbmt/pact/rspec.rb +17 -0
- data/lib/sbmt/pact/tasks/pact.rake +13 -0
- data/lib/sbmt/pact/version.rb +7 -0
- data/lib/sbmt/pact.rb +48 -0
- data/sbmt-pact.gemspec +59 -0
- metadata +462 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Pact
|
5
|
+
module Matchers
|
6
|
+
PACT_SPEC_V1 = 1
|
7
|
+
PACT_SPEC_V2 = 2
|
8
|
+
PACT_SPEC_V3 = 3
|
9
|
+
PACT_SPEC_V4 = 4
|
10
|
+
|
11
|
+
ANY_STRING_REGEX = /.*/
|
12
|
+
UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
|
13
|
+
|
14
|
+
# simplified
|
15
|
+
ISO8601_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)*(.\d{2}:\d{2})*/i
|
16
|
+
|
17
|
+
def match_exactly(arg)
|
18
|
+
V1::Equality.new(arg)
|
19
|
+
end
|
20
|
+
|
21
|
+
def match_type_of(arg)
|
22
|
+
V2::Type.new(arg)
|
23
|
+
end
|
24
|
+
|
25
|
+
def match_include(arg)
|
26
|
+
V3::Include.new(arg)
|
27
|
+
end
|
28
|
+
|
29
|
+
def match_any_string(sample = "any")
|
30
|
+
V2::Regex.new(ANY_STRING_REGEX, sample)
|
31
|
+
end
|
32
|
+
|
33
|
+
def match_any_integer(sample = 10)
|
34
|
+
V3::Integer.new(sample)
|
35
|
+
end
|
36
|
+
|
37
|
+
def match_any_decimal(sample = 10.0)
|
38
|
+
V3::Decimal.new(sample)
|
39
|
+
end
|
40
|
+
|
41
|
+
def match_any_number(sample = 10.0)
|
42
|
+
V3::Number.new(sample)
|
43
|
+
end
|
44
|
+
|
45
|
+
def match_any_boolean(sample = true)
|
46
|
+
V3::Boolean.new(sample)
|
47
|
+
end
|
48
|
+
|
49
|
+
def match_uuid(sample = "e1d01e04-3a2b-4eed-a4fb-54f5cd257338")
|
50
|
+
V2::Regex.new(UUID_REGEX, sample)
|
51
|
+
end
|
52
|
+
|
53
|
+
def match_regex(regex, sample)
|
54
|
+
V2::Regex.new(regex, sample)
|
55
|
+
end
|
56
|
+
|
57
|
+
def match_datetime(format, sample)
|
58
|
+
V3::DateTime.new(format, sample)
|
59
|
+
end
|
60
|
+
|
61
|
+
def match_iso8601(sample = "2024-08-12T12:25:00.243118+03:00")
|
62
|
+
V2::Regex.new(ISO8601_REGEX, sample)
|
63
|
+
end
|
64
|
+
|
65
|
+
def match_date(format, sample)
|
66
|
+
V3::Date.new(format, sample)
|
67
|
+
end
|
68
|
+
|
69
|
+
def match_time(format, sample)
|
70
|
+
V3::Time.new(format, sample)
|
71
|
+
end
|
72
|
+
|
73
|
+
def match_each(template, min = nil)
|
74
|
+
V3::Each.new(template, min)
|
75
|
+
end
|
76
|
+
|
77
|
+
def match_each_regex(regex, sample)
|
78
|
+
match_each_value(sample, match_regex(regex, sample))
|
79
|
+
end
|
80
|
+
|
81
|
+
def match_each_key(template, key_matchers)
|
82
|
+
V4::EachKey.new(key_matchers.is_a?(Array) ? key_matchers : [key_matchers], template)
|
83
|
+
end
|
84
|
+
|
85
|
+
def match_each_value(template, value_matchers = V2::Type.new(""))
|
86
|
+
V4::EachValue.new(value_matchers.is_a?(Array) ? value_matchers : [value_matchers], template)
|
87
|
+
end
|
88
|
+
|
89
|
+
def match_each_kv(template, key_matchers)
|
90
|
+
V4::EachKeyValue.new(key_matchers.is_a?(Array) ? key_matchers : [key_matchers], template)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ffi"
|
4
|
+
require "pact/ffi/verifier"
|
5
|
+
|
6
|
+
module Sbmt
|
7
|
+
module Pact
|
8
|
+
module Native
|
9
|
+
module BlockingVerifier
|
10
|
+
extend FFI::Library
|
11
|
+
ffi_lib DetectOS.get_bin_path
|
12
|
+
|
13
|
+
attach_function :execute, :pactffi_verifier_execute, %i[pointer], :int32, blocking: true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/logger"
|
4
|
+
|
5
|
+
module Sbmt
|
6
|
+
module Pact
|
7
|
+
module Native
|
8
|
+
module Logger
|
9
|
+
LOG_LEVELS = {
|
10
|
+
off: PactFfi::FfiLogLevelFilter["LOG_LEVEL_OFF"],
|
11
|
+
error: PactFfi::FfiLogLevelFilter["LOG_LEVEL_ERROR"],
|
12
|
+
warn: PactFfi::FfiLogLevelFilter["LOG_LEVEL_WARN"],
|
13
|
+
info: PactFfi::FfiLogLevelFilter["LOG_LEVEL_INFO"],
|
14
|
+
debug: PactFfi::FfiLogLevelFilter["LOG_LEVEL_DEBUG"],
|
15
|
+
trace: PactFfi::FfiLogLevelFilter["LOG_LEVEL_TRACE"]
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def self.log_to_stdout(log_level)
|
19
|
+
raise "invalid log level for PactFfi::FfiLogLevelFilter" unless LOG_LEVELS.key?(log_level)
|
20
|
+
PactFfi::Logger.log_to_stdout(LOG_LEVELS[log_level])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/verifier"
|
4
|
+
require "sbmt/pact/native/logger"
|
5
|
+
|
6
|
+
module Sbmt
|
7
|
+
module Pact
|
8
|
+
module Provider
|
9
|
+
class AsyncMessageVerifier < BaseVerifier
|
10
|
+
PROVIDER_TRANSPORT_TYPE = "message"
|
11
|
+
INTERACTION_FILTER_REGEX = "^async:.+"
|
12
|
+
|
13
|
+
def initialize(pact_config)
|
14
|
+
super
|
15
|
+
|
16
|
+
raise ArgumentError, "pact_config must be an instance of Sbmt::Pact::Provider::PactConfig::Message" unless pact_config.is_a?(::Sbmt::Pact::Provider::PactConfig::Async)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def add_provider_transport(pact_handle)
|
22
|
+
setup_uri = URI(@pact_config.message_setup_url)
|
23
|
+
PactFfi::Verifier.add_provider_transport(pact_handle, PROVIDER_TRANSPORT_TYPE, setup_uri.port, setup_uri.path, "")
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_filter_info(pact_handle)
|
27
|
+
PactFfi::Verifier.set_filter_info(pact_handle, INTERACTION_FILTER_REGEX, nil, 0)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/verifier"
|
4
|
+
require "sbmt/pact/native/logger"
|
5
|
+
require "sbmt/pact/native/blocking_verifier"
|
6
|
+
|
7
|
+
module Sbmt
|
8
|
+
module Pact
|
9
|
+
module Provider
|
10
|
+
class BaseVerifier
|
11
|
+
PROVIDER_TRANSPORT_TYPE = nil
|
12
|
+
attr_reader :logger
|
13
|
+
|
14
|
+
class VerificationError < Sbmt::Pact::FfiError; end
|
15
|
+
|
16
|
+
class VerifierError < Sbmt::Pact::Error; end
|
17
|
+
|
18
|
+
DEFAULT_CONSUMER_SELECTORS = {"deployed" => true, "environment" => "production"}.freeze
|
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)
|
31
|
+
raise ArgumentError, "pact_config must be a subclass of Sbmt::Pact::Provider::PactConfig::Base" unless pact_config.is_a?(::Sbmt::Pact::Provider::PactConfig::Base)
|
32
|
+
@pact_config = pact_config
|
33
|
+
@logger = Logger.new($stdout)
|
34
|
+
end
|
35
|
+
|
36
|
+
def verify!
|
37
|
+
raise VerifierError.new("interaction is designed to be used one-time only") if defined?(@used)
|
38
|
+
|
39
|
+
if consumer_selectors.blank?
|
40
|
+
logger.info("[verifier] does not need to verify consumer #{@pact_config.consumer_name}")
|
41
|
+
return
|
42
|
+
end
|
43
|
+
|
44
|
+
exception = nil
|
45
|
+
pact_handle = init_pact
|
46
|
+
|
47
|
+
start_servers!
|
48
|
+
|
49
|
+
logger.info("[verifier] starting provider verification")
|
50
|
+
|
51
|
+
result = Sbmt::Pact::Native::BlockingVerifier.execute(pact_handle)
|
52
|
+
if VERIFICATION_ERRORS[result].present?
|
53
|
+
error = VERIFICATION_ERRORS[result]
|
54
|
+
exception = VerificationError.new("There was an error while trying to verify provider \"#{@pact_config.provider_name}\"", error[:reason], error[:status])
|
55
|
+
end
|
56
|
+
ensure
|
57
|
+
@used = true
|
58
|
+
PactFfi::Verifier.shutdown(pact_handle) if pact_handle
|
59
|
+
stop_servers
|
60
|
+
|
61
|
+
raise exception if exception
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def init_pact
|
67
|
+
handle = PactFfi::Verifier.new_for_application("sbmt-pact", PactFfi.version)
|
68
|
+
set_provider_info(handle)
|
69
|
+
PactFfi::Verifier.set_provider_state(handle, @pact_config.provider_setup_url, 1, 1)
|
70
|
+
PactFfi::Verifier.set_verification_options(handle, 0, 10000)
|
71
|
+
PactFfi::Verifier.set_publish_options(handle, @pact_config.provider_version, "", nil, 0, "")
|
72
|
+
|
73
|
+
configure_verification_source(handle)
|
74
|
+
|
75
|
+
PactFfi::Verifier.set_no_pacts_is_error(handle, 1)
|
76
|
+
|
77
|
+
add_provider_transport(handle)
|
78
|
+
set_filter_info(handle)
|
79
|
+
|
80
|
+
Sbmt::Pact::Native::Logger.log_to_stdout(@pact_config.log_level)
|
81
|
+
|
82
|
+
logger.info("[verifier] verification initialized for provider #{@pact_config.provider_name}, version #{@pact_config.provider_version}, transport #{self.class::PROVIDER_TRANSPORT_TYPE}")
|
83
|
+
|
84
|
+
handle
|
85
|
+
end
|
86
|
+
|
87
|
+
def set_provider_info(pact_handle)
|
88
|
+
PactFfi::Verifier.set_provider_info(pact_handle, @pact_config.provider_name, "", "", 0, "")
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_provider_transport(pact_handle)
|
92
|
+
raise Sbmt::Pact::ImplementationRequired, "Implement #add_provider_transport in a subclass"
|
93
|
+
end
|
94
|
+
|
95
|
+
def set_filter_info(pact_handle)
|
96
|
+
# e.g. PactFfi::Verifier.set_filter_info(pact_handle, "^grpc:.+", nil, 0)
|
97
|
+
raise Sbmt::Pact::ImplementationRequired, "Implement #set_filter_info in a subclass"
|
98
|
+
end
|
99
|
+
|
100
|
+
def start_servers!
|
101
|
+
logger.info("[verifier] starting services")
|
102
|
+
|
103
|
+
@servers_started = true
|
104
|
+
@pact_config.start_servers
|
105
|
+
end
|
106
|
+
|
107
|
+
def stop_servers
|
108
|
+
return unless @servers_started
|
109
|
+
|
110
|
+
logger.info("[verifier] stopping services")
|
111
|
+
|
112
|
+
@pact_config.stop_servers
|
113
|
+
end
|
114
|
+
|
115
|
+
def configure_verification_source(handle)
|
116
|
+
if @pact_config.pact_broker_proxy_url.blank?
|
117
|
+
path = Rails.root.join("pacts").to_s
|
118
|
+
logger.info("[verifier] pact broker url is not set, using #{path} as a verification source")
|
119
|
+
return PactFfi::Verifier.add_directory_source(handle, path)
|
120
|
+
end
|
121
|
+
|
122
|
+
logger.info("[verifier] using pact broker url #{@pact_config.broker_url} with consumer selectors: #{JSON.dump(consumer_selectors)} as a verification source")
|
123
|
+
|
124
|
+
filters = consumer_selectors.map do |selector|
|
125
|
+
FFI::MemoryPointer.from_string(JSON.dump(selector).to_s)
|
126
|
+
end
|
127
|
+
filters_ptr = FFI::MemoryPointer.new(:pointer, filters.size + 1)
|
128
|
+
filters_ptr.write_array_of_pointer(filters)
|
129
|
+
PactFfi::Verifier.broker_source_with_selectors(handle, @pact_config.pact_broker_proxy_url, @pact_config.broker_username, @pact_config.broker_password, nil, 0, nil, nil, 0, nil, filters_ptr, filters.size, nil, 0)
|
130
|
+
end
|
131
|
+
|
132
|
+
def consumer_selectors
|
133
|
+
@consumer_selectors ||= build_consumer_selectors(@pact_config.verify_only, @pact_config.consumer_name, @pact_config.consumer_branch)
|
134
|
+
end
|
135
|
+
|
136
|
+
def build_consumer_selectors(verify_only, consumer_name, consumer_branch)
|
137
|
+
# if verify_only and consumer_name are defined - select only needed consumer
|
138
|
+
if verify_only.present?
|
139
|
+
# select proper consumer branch if defined
|
140
|
+
if consumer_name.present?
|
141
|
+
return [] unless verify_only.include?(consumer_name)
|
142
|
+
return [{"branch" => consumer_branch, "consumer" => consumer_name}] if consumer_branch.present?
|
143
|
+
return [DEFAULT_CONSUMER_SELECTORS.merge("consumer" => consumer_name)]
|
144
|
+
end
|
145
|
+
# or default selectors
|
146
|
+
return verify_only.map { |name| DEFAULT_CONSUMER_SELECTORS.merge("consumer" => name) }
|
147
|
+
end
|
148
|
+
|
149
|
+
# select provided consumer_name
|
150
|
+
return [{"branch" => consumer_branch, "consumer" => consumer_name}] if consumer_name.present? && consumer_branch.present?
|
151
|
+
return [DEFAULT_CONSUMER_SELECTORS.merge("consumer" => consumer_name)] if consumer_name.present?
|
152
|
+
|
153
|
+
[DEFAULT_CONSUMER_SELECTORS]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/verifier"
|
4
|
+
require "sbmt/pact/native/logger"
|
5
|
+
|
6
|
+
module Sbmt
|
7
|
+
module Pact
|
8
|
+
module Provider
|
9
|
+
class GrpcVerifier < BaseVerifier
|
10
|
+
PROVIDER_TRANSPORT_TYPE = "grpc"
|
11
|
+
INTERACTION_FILTER_REGEX = "^grpc:.+"
|
12
|
+
|
13
|
+
def initialize(pact_config)
|
14
|
+
super
|
15
|
+
|
16
|
+
raise ArgumentError, "pact_config must be an instance of Sbmt::Pact::Provider::PactConfig::Grpc" unless pact_config.is_a?(::Sbmt::Pact::Provider::PactConfig::Grpc)
|
17
|
+
@grpc_server = GrufServer.new(host: "127.0.0.1:#{@pact_config.grpc_port}", services: @pact_config.grpc_services)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def add_provider_transport(pact_handle)
|
23
|
+
PactFfi::Verifier.add_provider_transport(pact_handle, PROVIDER_TRANSPORT_TYPE, @pact_config.grpc_port, "", "")
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_filter_info(pact_handle)
|
27
|
+
PactFfi::Verifier.set_filter_info(pact_handle, INTERACTION_FILTER_REGEX, nil, 0)
|
28
|
+
end
|
29
|
+
|
30
|
+
def start_servers!
|
31
|
+
super
|
32
|
+
@grpc_server.start
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop_servers
|
36
|
+
super
|
37
|
+
@grpc_server.stop
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Pact
|
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,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Pact
|
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
|
+
end
|
18
|
+
|
19
|
+
def start
|
20
|
+
raise "server already running, stop server before starting new one" if @thread
|
21
|
+
|
22
|
+
@logger.info("[webrick] starting server with options: #{@options}")
|
23
|
+
|
24
|
+
@thread = Thread.new do
|
25
|
+
@logger.debug "[webrick] starting http server"
|
26
|
+
|
27
|
+
::Rack::Handler::WEBrick.run(Rails.application,
|
28
|
+
Host: @options[:host],
|
29
|
+
Port: @options[:port],
|
30
|
+
Logger: @logger,
|
31
|
+
StartCallback: -> { @started = true }) do |server|
|
32
|
+
@server = server
|
33
|
+
end
|
34
|
+
end
|
35
|
+
sleep 0.001 until @started
|
36
|
+
|
37
|
+
@logger.info("[webrick] server started")
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop
|
41
|
+
@logger.info("[webrick] stopping server")
|
42
|
+
|
43
|
+
@server&.shutdown
|
44
|
+
@thread&.join(SERVER_STOP_TIMEOUT_SEC)
|
45
|
+
@thread&.kill
|
46
|
+
|
47
|
+
@logger.info("[webrick] server stopped")
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Run the server
|
52
|
+
#
|
53
|
+
def run
|
54
|
+
start
|
55
|
+
|
56
|
+
yield
|
57
|
+
rescue => e
|
58
|
+
@logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}")
|
59
|
+
raise
|
60
|
+
ensure
|
61
|
+
stop
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/verifier"
|
4
|
+
require "sbmt/pact/native/logger"
|
5
|
+
|
6
|
+
module Sbmt
|
7
|
+
module Pact
|
8
|
+
module Provider
|
9
|
+
class HttpVerifier < BaseVerifier
|
10
|
+
PROVIDER_TRANSPORT_TYPE = "http"
|
11
|
+
|
12
|
+
def initialize(pact_config)
|
13
|
+
super
|
14
|
+
|
15
|
+
raise ArgumentError, "pact_config must be an instance of Sbmt::Pact::Provider::PactConfig::Http" unless pact_config.is_a?(::Sbmt::Pact::Provider::PactConfig::Http)
|
16
|
+
@http_server = HttpServer.new(host: "127.0.0.1", port: @pact_config.http_port)
|
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
|
+
def set_filter_info(pact_handle)
|
31
|
+
PactFfi::Verifier.set_filter_info(pact_handle, "^http:.+", nil, 0)
|
32
|
+
end
|
33
|
+
|
34
|
+
def start_servers!
|
35
|
+
super
|
36
|
+
@http_server.start
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop_servers
|
40
|
+
super
|
41
|
+
@http_server.stop
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webrick"
|
4
|
+
|
5
|
+
module Sbmt
|
6
|
+
module Pact
|
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
|
+
desc_no_prefix = description.sub(/^(async|sync): /, "")
|
75
|
+
@message_handlers[description] || @message_handlers[desc_no_prefix]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|