sbmt-pact 0.12.0

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