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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +62 -0
  4. data/Appraisals +23 -0
  5. data/CHANGELOG.md +96 -0
  6. data/Dockerfile +14 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE +21 -0
  9. data/README.md +212 -0
  10. data/Rakefile +12 -0
  11. data/dip.yml +86 -0
  12. data/docker-compose.yml +40 -0
  13. data/docs/sbmt-pact-arch.png +0 -0
  14. data/lefthook-local.dip_example.yml +4 -0
  15. data/lefthook.yml +6 -0
  16. data/lib/sbmt/pact/configuration.rb +23 -0
  17. data/lib/sbmt/pact/consumer/grpc_interaction_builder.rb +193 -0
  18. data/lib/sbmt/pact/consumer/http_interaction_builder.rb +149 -0
  19. data/lib/sbmt/pact/consumer/interaction_contents.rb +47 -0
  20. data/lib/sbmt/pact/consumer/message_interaction_builder.rb +285 -0
  21. data/lib/sbmt/pact/consumer/mock_server.rb +92 -0
  22. data/lib/sbmt/pact/consumer/pact_config/base.rb +24 -0
  23. data/lib/sbmt/pact/consumer/pact_config/grpc.rb +26 -0
  24. data/lib/sbmt/pact/consumer/pact_config/http.rb +26 -0
  25. data/lib/sbmt/pact/consumer/pact_config/message.rb +17 -0
  26. data/lib/sbmt/pact/consumer/pact_config.rb +24 -0
  27. data/lib/sbmt/pact/consumer.rb +8 -0
  28. data/lib/sbmt/pact/matchers/base.rb +67 -0
  29. data/lib/sbmt/pact/matchers/v1/equality.rb +19 -0
  30. data/lib/sbmt/pact/matchers/v2/regex.rb +19 -0
  31. data/lib/sbmt/pact/matchers/v2/type.rb +17 -0
  32. data/lib/sbmt/pact/matchers/v3/boolean.rb +17 -0
  33. data/lib/sbmt/pact/matchers/v3/date.rb +18 -0
  34. data/lib/sbmt/pact/matchers/v3/date_time.rb +18 -0
  35. data/lib/sbmt/pact/matchers/v3/decimal.rb +17 -0
  36. data/lib/sbmt/pact/matchers/v3/each.rb +42 -0
  37. data/lib/sbmt/pact/matchers/v3/include.rb +17 -0
  38. data/lib/sbmt/pact/matchers/v3/integer.rb +17 -0
  39. data/lib/sbmt/pact/matchers/v3/number.rb +17 -0
  40. data/lib/sbmt/pact/matchers/v3/time.rb +18 -0
  41. data/lib/sbmt/pact/matchers/v4/each_key.rb +26 -0
  42. data/lib/sbmt/pact/matchers/v4/each_key_value.rb +32 -0
  43. data/lib/sbmt/pact/matchers/v4/each_value.rb +33 -0
  44. data/lib/sbmt/pact/matchers/v4/not_empty.rb +17 -0
  45. data/lib/sbmt/pact/matchers.rb +94 -0
  46. data/lib/sbmt/pact/native/blocking_verifier.rb +17 -0
  47. data/lib/sbmt/pact/native/logger.rb +25 -0
  48. data/lib/sbmt/pact/provider/async_message_verifier.rb +32 -0
  49. data/lib/sbmt/pact/provider/base_verifier.rb +158 -0
  50. data/lib/sbmt/pact/provider/grpc_verifier.rb +42 -0
  51. data/lib/sbmt/pact/provider/gruf_server.rb +75 -0
  52. data/lib/sbmt/pact/provider/http_server.rb +66 -0
  53. data/lib/sbmt/pact/provider/http_verifier.rb +46 -0
  54. data/lib/sbmt/pact/provider/message_provider_servlet.rb +80 -0
  55. data/lib/sbmt/pact/provider/pact_broker_proxy.rb +85 -0
  56. data/lib/sbmt/pact/provider/pact_broker_proxy_runner.rb +71 -0
  57. data/lib/sbmt/pact/provider/pact_config/async.rb +25 -0
  58. data/lib/sbmt/pact/provider/pact_config/base.rb +92 -0
  59. data/lib/sbmt/pact/provider/pact_config/grpc.rb +30 -0
  60. data/lib/sbmt/pact/provider/pact_config/http.rb +25 -0
  61. data/lib/sbmt/pact/provider/pact_config.rb +24 -0
  62. data/lib/sbmt/pact/provider/provider_server_runner.rb +89 -0
  63. data/lib/sbmt/pact/provider/provider_state_configuration.rb +32 -0
  64. data/lib/sbmt/pact/provider/provider_state_servlet.rb +84 -0
  65. data/lib/sbmt/pact/provider.rb +8 -0
  66. data/lib/sbmt/pact/railtie.rb +13 -0
  67. data/lib/sbmt/pact/rspec/support/pact_consumer_helpers.rb +46 -0
  68. data/lib/sbmt/pact/rspec/support/pact_message_helpers.rb +42 -0
  69. data/lib/sbmt/pact/rspec/support/pact_provider_helpers.rb +87 -0
  70. data/lib/sbmt/pact/rspec/support/waterdrop/pact_waterdrop_client.rb +27 -0
  71. data/lib/sbmt/pact/rspec/support/webmock/webmock_helpers.rb +30 -0
  72. data/lib/sbmt/pact/rspec.rb +17 -0
  73. data/lib/sbmt/pact/tasks/pact.rake +13 -0
  74. data/lib/sbmt/pact/version.rb +7 -0
  75. data/lib/sbmt/pact.rb +48 -0
  76. data/sbmt-pact.gemspec +59 -0
  77. 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