sbmt-pact 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|