pact 1.66.1 → 1.67.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/lib/pact/provider/request.rb +14 -1
  4. data/lib/pact/v2/configuration.rb +23 -0
  5. data/lib/pact/v2/consumer/grpc_interaction_builder.rb +194 -0
  6. data/lib/pact/v2/consumer/http_interaction_builder.rb +162 -0
  7. data/lib/pact/v2/consumer/interaction_contents.rb +62 -0
  8. data/lib/pact/v2/consumer/message_interaction_builder.rb +287 -0
  9. data/lib/pact/v2/consumer/mock_server.rb +100 -0
  10. data/lib/pact/v2/consumer/pact_config/base.rb +24 -0
  11. data/lib/pact/v2/consumer/pact_config/grpc.rb +26 -0
  12. data/lib/pact/v2/consumer/pact_config/http.rb +55 -0
  13. data/lib/pact/v2/consumer/pact_config/message.rb +17 -0
  14. data/lib/pact/v2/consumer/pact_config/plugin_async_message.rb +26 -0
  15. data/lib/pact/v2/consumer/pact_config/plugin_http.rb +26 -0
  16. data/lib/pact/v2/consumer/pact_config/plugin_sync_message.rb +26 -0
  17. data/lib/pact/v2/consumer/pact_config.rb +30 -0
  18. data/lib/pact/v2/consumer/plugin_async_message_interaction_builder.rb +171 -0
  19. data/lib/pact/v2/consumer/plugin_http_interaction_builder.rb +201 -0
  20. data/lib/pact/v2/consumer/plugin_sync_message_interaction_builder.rb +180 -0
  21. data/lib/pact/v2/consumer.rb +8 -0
  22. data/lib/pact/v2/generators/base.rb +287 -0
  23. data/lib/pact/v2/generators.rb +49 -0
  24. data/lib/pact/v2/matchers/base.rb +74 -0
  25. data/lib/pact/v2/matchers/v1/equality.rb +19 -0
  26. data/lib/pact/v2/matchers/v2/regex.rb +19 -0
  27. data/lib/pact/v2/matchers/v2/type.rb +17 -0
  28. data/lib/pact/v2/matchers/v3/boolean.rb +17 -0
  29. data/lib/pact/v2/matchers/v3/content_type.rb +32 -0
  30. data/lib/pact/v2/matchers/v3/date.rb +18 -0
  31. data/lib/pact/v2/matchers/v3/date_time.rb +18 -0
  32. data/lib/pact/v2/matchers/v3/decimal.rb +17 -0
  33. data/lib/pact/v2/matchers/v3/each.rb +42 -0
  34. data/lib/pact/v2/matchers/v3/include.rb +17 -0
  35. data/lib/pact/v2/matchers/v3/integer.rb +17 -0
  36. data/lib/pact/v2/matchers/v3/null.rb +16 -0
  37. data/lib/pact/v2/matchers/v3/number.rb +17 -0
  38. data/lib/pact/v2/matchers/v3/semver.rb +23 -0
  39. data/lib/pact/v2/matchers/v3/time.rb +18 -0
  40. data/lib/pact/v2/matchers/v3/values.rb +16 -0
  41. data/lib/pact/v2/matchers/v4/each_key.rb +26 -0
  42. data/lib/pact/v2/matchers/v4/each_key_value.rb +32 -0
  43. data/lib/pact/v2/matchers/v4/each_value.rb +33 -0
  44. data/lib/pact/v2/matchers/v4/not_empty.rb +24 -0
  45. data/lib/pact/v2/matchers/v4/status_code.rb +17 -0
  46. data/lib/pact/v2/matchers.rb +110 -0
  47. data/lib/pact/v2/native/blocking_verifier.rb +17 -0
  48. data/lib/pact/v2/native/logger.rb +25 -0
  49. data/lib/pact/v2/provider/async_message_verifier.rb +28 -0
  50. data/lib/pact/v2/provider/base_verifier.rb +242 -0
  51. data/lib/pact/v2/provider/grpc_verifier.rb +38 -0
  52. data/lib/pact/v2/provider/gruf_server.rb +75 -0
  53. data/lib/pact/v2/provider/http_server.rb +79 -0
  54. data/lib/pact/v2/provider/http_verifier.rb +43 -0
  55. data/lib/pact/v2/provider/message_provider_servlet.rb +79 -0
  56. data/lib/pact/v2/provider/mixed_verifier.rb +22 -0
  57. data/lib/pact/v2/provider/pact_broker_proxy.rb +66 -0
  58. data/lib/pact/v2/provider/pact_broker_proxy_runner.rb +77 -0
  59. data/lib/pact/v2/provider/pact_config/async.rb +29 -0
  60. data/lib/pact/v2/provider/pact_config/base.rb +101 -0
  61. data/lib/pact/v2/provider/pact_config/grpc.rb +26 -0
  62. data/lib/pact/v2/provider/pact_config/http.rb +27 -0
  63. data/lib/pact/v2/provider/pact_config/mixed.rb +39 -0
  64. data/lib/pact/v2/provider/pact_config.rb +26 -0
  65. data/lib/pact/v2/provider/provider_server_runner.rb +89 -0
  66. data/lib/pact/v2/provider/provider_state_configuration.rb +32 -0
  67. data/lib/pact/v2/provider/provider_state_servlet.rb +86 -0
  68. data/lib/pact/v2/provider.rb +8 -0
  69. data/lib/pact/v2/railtie.rb +13 -0
  70. data/lib/pact/v2/rspec/support/pact_consumer_helpers.rb +93 -0
  71. data/lib/pact/v2/rspec/support/pact_message_helpers.rb +42 -0
  72. data/lib/pact/v2/rspec/support/pact_provider_helpers.rb +129 -0
  73. data/lib/pact/v2/rspec/support/waterdrop/pact_waterdrop_client.rb +27 -0
  74. data/lib/pact/v2/rspec/support/webmock/webmock_helpers.rb +30 -0
  75. data/lib/pact/v2/rspec.rb +17 -0
  76. data/lib/pact/v2/tasks/pact.rake +13 -0
  77. data/lib/pact/v2.rb +71 -0
  78. data/lib/pact/version.rb +1 -1
  79. data/lib/pact.rb +1 -0
  80. data/pact.gemspec +52 -7
  81. metadata +421 -67
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require_relative "pact_config/grpc"
4
+
5
+ module Pact
6
+ module V2
7
+ module Provider
8
+ module PactConfig
9
+ def self.new(transport_type, provider_name:, opts: {})
10
+ case transport_type
11
+ when :http
12
+ Http.new(provider_name: provider_name, opts: opts)
13
+ when :grpc
14
+ Grpc.new(provider_name: provider_name, opts: opts)
15
+ when :async
16
+ Async.new(provider_name: provider_name, opts: opts)
17
+ when :mixed
18
+ Mixed.new(provider_name: provider_name, opts: opts)
19
+ else
20
+ raise ArgumentError, "unknown transport_type: #{transport_type}"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webrick"
4
+
5
+ module Pact
6
+ module V2
7
+ module Provider
8
+ class ProviderServerRunner
9
+ attr_reader :logger
10
+
11
+ SETUP_PROVIDER_STATE_PATH = "/setup-provider"
12
+ VERIFY_MESSAGE_PATH = "/verify-message"
13
+
14
+ def initialize(port: 9001, host: "127.0.0.1", logger: nil)
15
+ @host = host
16
+ @port = port
17
+ @provider_setup_states = {}
18
+ @provider_teardown_states = {}
19
+ @logger = logger || Logger.new($stdout)
20
+
21
+ @state_servlet = ProviderStateServlet.new(logger: @logger)
22
+ @message_servlet = MessageProviderServlet.new(logger: @logger)
23
+ @thread = nil
24
+ end
25
+
26
+ def state_setup_url
27
+ "http://#{@host}:#{@port}#{SETUP_PROVIDER_STATE_PATH}"
28
+ end
29
+
30
+ def message_setup_url
31
+ "http://#{@host}:#{@port}#{VERIFY_MESSAGE_PATH}"
32
+ end
33
+
34
+ def start
35
+ raise "server already running, stop server before starting new one" if @thread
36
+
37
+ @server = WEBrick::HTTPServer.new({BindAddress: @host, Port: @port}, WEBrick::Config::HTTP)
38
+ @server.mount(SETUP_PROVIDER_STATE_PATH, @state_servlet)
39
+ @server.mount(VERIFY_MESSAGE_PATH, @message_servlet)
40
+
41
+ @thread = Thread.new do
42
+ @logger.debug "starting provider setup server"
43
+ @server.start
44
+ end
45
+ end
46
+
47
+ def stop
48
+ @logger.info("stopping provider setup server")
49
+
50
+ @server&.shutdown
51
+ @thread&.join
52
+
53
+ @logger.info("provider setup server stopped")
54
+ end
55
+
56
+ def run
57
+ start
58
+
59
+ yield
60
+ rescue => e
61
+ logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}")
62
+ raise
63
+ ensure
64
+ stop
65
+ end
66
+
67
+ def add_message_handler(state_name, &block)
68
+ @message_servlet.add_message_handler(state_name, &block)
69
+ end
70
+
71
+ def add_setup_state(state_name, use_before_setup_hook = true, &block)
72
+ @state_servlet.add_setup_state(state_name, use_before_setup_hook, &block)
73
+ end
74
+
75
+ def add_teardown_state(state_name, use_after_teardown_hook = true, &block)
76
+ @state_servlet.add_teardown_state(state_name, use_after_teardown_hook, &block)
77
+ end
78
+
79
+ def set_before_setup_hook(&block)
80
+ @state_servlet.before_setup(&block)
81
+ end
82
+
83
+ def set_after_teardown_hook(&block)
84
+ @state_servlet.after_teardown(&block)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Provider
6
+ class ProviderStateConfiguration
7
+ attr_reader :name, :opts, :setup_proc, :teardown_proc
8
+
9
+ class ProviderStateConfigurationError < ::Pact::V2::Error; end
10
+
11
+ def initialize(name, opts: {})
12
+ @name = name
13
+ @opts = opts
14
+ @setup_proc = nil
15
+ @teardown_proc = nil
16
+ end
17
+
18
+ def set_up(&block)
19
+ @setup_proc = block
20
+ end
21
+
22
+ def tear_down(&block)
23
+ @teardown_proc = block
24
+ end
25
+
26
+ def validate!
27
+ raise ProviderStateConfigurationError.new("no hooks configured for state #{@name}: \"provider_state\" declaration only needed if setup/teardown hooks are used for that state. Please add hooks or remove \"provider_state\" declaration") unless @setup_proc || @teardown_proc
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webrick"
4
+
5
+ module Pact
6
+ module V2
7
+ module Provider
8
+ class ProviderStateServlet < WEBrick::HTTPServlet::ProcHandler
9
+ attr_reader :logger
10
+
11
+ def initialize(logger: Logger.new($stdout))
12
+ super(build_proc)
13
+
14
+ @logger = logger
15
+
16
+ @provider_setup_states = {}
17
+ @provider_teardown_states = {}
18
+
19
+ @before_setup_hook_proc = nil
20
+ @after_teardown_hook_proc = nil
21
+
22
+ @global_setup_hook = ::Pact::V2.configuration.before_provider_state_proc
23
+ @global_teardown_hook = ::Pact::V2.configuration.after_provider_state_proc
24
+ end
25
+
26
+ def add_setup_state(name, use_before_setup_hook, &block)
27
+ raise "provider state #{name} already configured" if @provider_setup_states[name].present?
28
+
29
+ @provider_setup_states[name] = {proc: block, use_hooks: use_before_setup_hook}
30
+ end
31
+
32
+ def add_teardown_state(name, use_after_teardown_hook, &block)
33
+ raise "provider state #{name} already configured" if @provider_teardown_states[name].present?
34
+
35
+ @provider_teardown_states[name] = {proc: block, use_hooks: use_after_teardown_hook}
36
+ end
37
+
38
+ def before_setup(&block)
39
+ @before_setup_hook_proc = block
40
+ end
41
+
42
+ def after_teardown(&block)
43
+ @after_teardown_hook_proc = block
44
+ end
45
+
46
+ private
47
+
48
+ def call_setup(state_name, state_data)
49
+ logger.debug "call_setup #{state_name} with #{state_data}"
50
+ @global_setup_hook&.call
51
+ @before_setup_hook_proc&.call(state_name, state_data) if @provider_setup_states.dig(state_name, :use_hooks)
52
+ @provider_setup_states.dig(state_name, :proc)&.call(state_data)
53
+ end
54
+
55
+ def call_teardown(state_name, state_data)
56
+ logger.debug "call_teardown #{state_name} with #{state_data}"
57
+ @provider_teardown_states.dig(state_name, :proc)&.call(state_data)
58
+ @after_teardown_hook_proc&.call(state_name, state_data) if @provider_setup_states.dig(state_name, :use_hooks)
59
+ @global_teardown_hook&.call
60
+ end
61
+
62
+ def build_proc
63
+ proc do |request, response|
64
+ # {"action" => "setup", "params" => {"order_uuid" => "mxfcpcsfUOHO"},"state" => "order exists and can be saved"}
65
+ # {"action"=> "teardown", "params" => {"order_uuid" => "mxfcpcsfUOHO"}, "state" => "order exists and can be saved"}
66
+ data = JSON.parse(request.body)
67
+
68
+ action = data["action"]
69
+ state_name = data["state"]
70
+ state_data = data["params"]
71
+
72
+ logger.warn("unknown callback state action: #{action}") if action.blank?
73
+
74
+ call_setup(state_name, state_data) if action == "setup"
75
+ call_teardown(state_name, state_data) if action == "teardown"
76
+
77
+ response.status = 200
78
+ rescue JSON::ParserError => ex
79
+ logger.error("cannot parse request: #{ex.message}")
80
+ response.status = 500
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Provider
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Pact
6
+ module V2
7
+ class Railtie < Rails::Railtie
8
+ rake_tasks do
9
+ load "pact/v2/tasks/pact.rake"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pact_message_helpers"
4
+ require "json"
5
+
6
+ module PactV2ConsumerDsl
7
+ include Pact::V2::Matchers
8
+ include Pact::V2::Generators
9
+
10
+ module ClassMethods
11
+ def has_http_pact_between(consumer, provider, opts: {})
12
+ _has_pact_between(:http, consumer, provider, opts: opts)
13
+ end
14
+
15
+ def has_grpc_pact_between(consumer, provider, opts: {})
16
+ _has_pact_between(:grpc, consumer, provider, opts: opts)
17
+ end
18
+
19
+ def has_message_pact_between(consumer, provider, opts: {})
20
+ _has_pact_between(:message, consumer, provider, opts: opts)
21
+ end
22
+
23
+ def has_plugin_http_pact_between(consumer, provider, opts: {})
24
+ _has_pact_between(:plugin_http, consumer, provider, opts: opts)
25
+ end
26
+
27
+ def has_plugin_sync_message_pact_between(consumer, provider, opts: {})
28
+ _has_pact_between(:plugin_sync_message, consumer, provider, opts: opts)
29
+ end
30
+
31
+ def has_plugin_async_message_pact_between(consumer, provider, opts: {})
32
+ _has_pact_between(:plugin_async_message, consumer, provider, opts: opts)
33
+ end
34
+
35
+ def _has_pact_between(transport_type, consumer, provider, opts: {})
36
+ raise "has_#{transport_type}_pact_between is designed to be used with RSpec 3+" unless defined?(::RSpec)
37
+ raise "has_#{transport_type}_pact_between has to be declared at the top level of a suite" unless top_level?
38
+ raise "has_*_pact_between cannot be declared more than once per suite" if defined?(@_pact_config)
39
+
40
+ # rubocop:disable RSpec/BeforeAfterAll
41
+ before(:context) do
42
+ @_pact_config = Pact::V2::Consumer::PactConfig.new(transport_type, consumer_name: consumer, provider_name: provider, opts: opts)
43
+ end
44
+ # rubocop:enable RSpec/BeforeAfterAll
45
+ end
46
+ end
47
+
48
+ def new_interaction(description = nil)
49
+ pact_config.new_interaction(description)
50
+ end
51
+
52
+ def reset_pact # rubocop:disable Rails/Delegate
53
+ pact_config.reset_pact
54
+ end
55
+
56
+ def pact_config
57
+ instance_variable_get(:@_pact_config)
58
+ end
59
+
60
+ def execute_http_pact
61
+ raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used)
62
+ mock_server = Pact::V2::Consumer::MockServer.create_for_http!(
63
+ pact: pact_config.pact_handle, host: pact_config.mock_host, port: pact_config.mock_port
64
+ )
65
+
66
+ yield(mock_server)
67
+
68
+ ensure
69
+ if mock_server.matched?
70
+ mock_server.write_pacts!(pact_config.pact_dir)
71
+ else
72
+ msg = mismatches_error_msg(mock_server)
73
+ raise Pact::V2::Consumer::HttpInteractionBuilder::InteractionMismatchesError.new(msg)
74
+ end
75
+ @used = true
76
+ mock_server&.cleanup
77
+ reset_pact
78
+ end
79
+
80
+
81
+ def mismatches_error_msg(mock_server)
82
+ rspec_example_desc = RSpec.current_example&.description
83
+ mismatches = JSON.pretty_generate(JSON.parse(mock_server.mismatches))
84
+ mismatches_with_colored_keys = mismatches.gsub(/"([^"]+)":/) { |match| "\e[34m#{match}\e[0m" } # Blue keys / white values
85
+
86
+ "#{rspec_example_desc} has mismatches: #{mismatches_with_colored_keys}"
87
+ end
88
+ end
89
+
90
+ RSpec.configure do |config|
91
+ config.include PactV2ConsumerDsl, pact_entity: :consumer
92
+ config.extend PactV2ConsumerDsl::ClassMethods, pact_entity: :consumer
93
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "waterdrop/pact_waterdrop_client"
4
+
5
+ module PactMessageHelpers
6
+ module ProviderHelpers
7
+ def with_pact_producer
8
+ client = PactWaterdropClient.new
9
+ yield(client)
10
+ client.to_pact
11
+ end
12
+
13
+ def produce_outbox_item(item)
14
+ raise "Please require sbmt/kafka_producer to use helper" unless defined?(::Sbmt::KafkaProducer)
15
+
16
+ with_pact_producer do |client|
17
+ Sbmt::KafkaProducer::OutboxProducer.new(
18
+ client: client, topic: item.transports.first.topic
19
+ ).call(item, item.payload)
20
+ end
21
+ end
22
+ end
23
+
24
+ module ConsumerHelpers
25
+ def outbox_headers
26
+ raise "Please require sbmt/outbox to use helper" unless defined?(::Sbmt::Outbox)
27
+
28
+ {
29
+ Sbmt::Outbox::OutboxItem::OUTBOX_HEADER_NAME => match_regex(/(.+?_)*outbox_item/, "order_outbox_item"),
30
+ Sbmt::Outbox::OutboxItem::IDEMPOTENCY_HEADER_NAME => match_uuid,
31
+ Sbmt::Outbox::OutboxItem::SEQUENCE_HEADER_NAME => match_regex(/\d+/, "68"),
32
+ Sbmt::Outbox::OutboxItem::EVENT_TIME_HEADER_NAME => match_iso8601,
33
+ Sbmt::Outbox::OutboxItem::DISPATCH_TIME_HEADER_NAME => match_iso8601
34
+ }
35
+ end
36
+ end
37
+ end
38
+
39
+ RSpec.configure do |config|
40
+ config.extend PactMessageHelpers::ProviderHelpers, pact_entity: :provider
41
+ config.include PactMessageHelpers::ConsumerHelpers, pact_entity: :consumer
42
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pact_message_helpers"
4
+ require_relative "webmock/webmock_helpers"
5
+
6
+ module PactV2ProducerDsl
7
+ module ClassMethods
8
+ PACT_PROVIDER_NOT_DECLARED_MESSAGE = "http_pact_provider or grpc_pact_provider should be declared first"
9
+
10
+ def http_pact_provider(provider, opts: {})
11
+ _pact_provider(:http, provider, opts: opts)
12
+ end
13
+
14
+ def grpc_pact_provider(provider, opts: {})
15
+ _pact_provider(:grpc, provider, opts: opts)
16
+ end
17
+
18
+ def message_pact_provider(provider, opts: {})
19
+ _pact_provider(:async, provider, opts: opts)
20
+ end
21
+
22
+ def mixed_pact_provider(provider, opts: {})
23
+ execute_mixed_pact_provider(:mixed, provider, opts: opts)
24
+ end
25
+
26
+ def execute_mixed_pact_provider(transport_type, provider, opts: {})
27
+ raise "#{transport_type}_pact_provider is designed to be used with RSpec" unless defined?(::RSpec)
28
+ raise "#{transport_type}_pact_provider has to be declared at the top level of a suite" unless top_level?
29
+ raise "mixed_pact_provider is designed to be run once per provider so cannot be declared more than once" if defined?(@_pact_config)
30
+
31
+ pact_config_instance = Pact::V2::Provider::PactConfig.new(transport_type, provider_name: provider, opts: opts)
32
+ instance_variable_set(:@_pact_config, pact_config_instance)
33
+
34
+ # rubocop:disable RSpec/BeforeAfterAll
35
+ before(:context) do
36
+ # rspec allows only context ivars in specs and ignores the rest
37
+ # so we use block-as-a-closure feature to save pact_config ivar reference and make it available for descendants
38
+ @_pact_config = pact_config_instance
39
+ end
40
+ # rubocop:enable RSpec/BeforeAfterAll
41
+
42
+ it "verifies mixed interactions with provider #{provider}" do
43
+ pact_config.start_servers
44
+ # todo: call any available verifier, or exit if none specified
45
+ pact_config.http_config.new_verifier(@_pact_config).verify!
46
+ end
47
+ end
48
+
49
+ def _pact_provider(transport_type, provider, opts: {})
50
+ raise "#{transport_type}_pact_provider is designed to be used with RSpec" unless defined?(::RSpec)
51
+ raise "#{transport_type}_pact_provider has to be declared at the top level of a suite" unless top_level?
52
+ raise "*_pact_provider is designed to be run once per provider so cannot be declared more than once" if defined?(@_pact_config)
53
+
54
+ pact_config_instance = Pact::V2::Provider::PactConfig.new(transport_type, provider_name: provider, opts: opts)
55
+ instance_variable_set(:@_pact_config, pact_config_instance)
56
+
57
+ # rubocop:disable RSpec/BeforeAfterAll
58
+ before(:context) do
59
+ # rspec allows only context ivars in specs and ignores the rest
60
+ # so we use block-as-a-closure feature to save pact_config ivar reference and make it available for descendants
61
+ @_pact_config = pact_config_instance
62
+ end
63
+ # rubocop:enable RSpec/BeforeAfterAll
64
+
65
+ it "verifies interactions with provider #{provider}" do
66
+ pact_config.new_verifier.verify!
67
+ end
68
+ end
69
+
70
+ def before_state_setup(&block)
71
+ raise PACT_PROVIDER_NOT_DECLARED_MESSAGE unless pact_config
72
+ pact_config.before_setup(&block)
73
+ end
74
+
75
+ def after_state_teardown(&block)
76
+ raise PACT_PROVIDER_NOT_DECLARED_MESSAGE unless pact_config
77
+ pact_config.after_teardown(&block)
78
+ end
79
+
80
+ def provider_state(name, opts: {}, &block)
81
+ raise PACT_PROVIDER_NOT_DECLARED_MESSAGE unless pact_config
82
+ pact_config.new_provider_state(name, opts: opts, &block)
83
+ end
84
+
85
+ def handle_message(name, opts: {}, &block)
86
+ async_klass = Pact::V2::Provider::PactConfig::Async
87
+ if defined?(@_pact_config) &&
88
+ @_pact_config.respond_to?(:async_config) &&
89
+ @_pact_config.async_config.is_a?(async_klass)
90
+ @_pact_config.async_config.new_message_handler(name, opts: opts, &block)
91
+ elsif pact_config &&
92
+ pact_config.respond_to?(:async_config) &&
93
+ pact_config.async_config.is_a?(async_klass)
94
+ pact_config.async_config.new_message_handler(name, opts: opts, &block)
95
+ elsif defined?(@_pact_config) &&
96
+ @_pact_config.is_a?(async_klass)
97
+ @_pact_config.new_message_handler(name, opts: opts, &block)
98
+ elsif pact_config.is_a?(async_klass)
99
+ pact_config.new_message_handler(name, opts: opts, &block)
100
+
101
+ else
102
+ raise "handle_message can only be used with message_pact_provider or mixed_pact_provider with an async block"
103
+ end
104
+ end
105
+
106
+ def pact_config
107
+ instance_variable_get(:@_pact_config)
108
+ end
109
+ end
110
+
111
+ def pact_config
112
+ instance_variable_get(:@_pact_config)
113
+ end
114
+ end
115
+
116
+ RSpec.configure do |config|
117
+ config.include PactV2ProducerDsl, pact_entity: :provider
118
+ config.extend PactV2ProducerDsl::ClassMethods, pact_entity: :provider
119
+
120
+ config.around pact_entity: :provider do |example|
121
+ WebmockHelpers.turned_off do
122
+ if defined?(::VCR)
123
+ VCR.turned_off { example.run }
124
+ else
125
+ example.run
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PactWaterdropClient
4
+ attr_reader :message
5
+
6
+ Report = Struct.new(:partition, :offset, :topic_name, keyword_init: true)
7
+
8
+ def produce_async(message)
9
+ @message = message
10
+ end
11
+
12
+ def produce_sync(message)
13
+ @message = message
14
+ Report.new(partition: 0, offset: 0, topic_name: message[:topic])
15
+ end
16
+
17
+ def to_pact(content_type: nil)
18
+ payload = message[:payload]
19
+ metadata = {
20
+ key: message[:key],
21
+ topic: message[:topic],
22
+ content_type: content_type
23
+ }.merge(message[:headers] || {})
24
+
25
+ [payload, metadata]
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebmockHelpers
4
+ def self.turned_off
5
+ yield unless defined?(::WebMock)
6
+
7
+ allow_net_connect = WebMock::Config.instance.allow_net_connect
8
+ allow_localhost = WebMock::Config.instance.allow_localhost
9
+ allow_hosts = WebMock::Config.instance.allow
10
+ net_http_connect_on_start = WebMock::Config.instance.net_http_connect_on_start
11
+
12
+ return yield if allow_net_connect
13
+
14
+ WebMock.allow_net_connect!
15
+
16
+ result = yield
17
+
18
+ # disable_net_connect! resets previous config settings
19
+ # so we need to specify them explicitly
20
+ WebMock.disable_net_connect!(
21
+ {
22
+ allow_localhost: allow_localhost,
23
+ allow: allow_hosts,
24
+ net_http_connect_on_start: net_http_connect_on_start
25
+ }
26
+ )
27
+
28
+ result
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec"
4
+ require_relative "rspec/support/pact_consumer_helpers"
5
+ require_relative "rspec/support/pact_provider_helpers"
6
+
7
+ RSpec.configure do |config|
8
+ config.define_derived_metadata(file_path: %r{spec/pact/}) { |metadata| metadata[:pact] = true }
9
+
10
+ # it's not an error: consumer tests contain `providers` subdirectory (because we're testing against different providers)
11
+ config.define_derived_metadata(file_path: %r{spec/pact/providers/}) { |metadata| metadata[:pact_entity] = :consumer }
12
+ # for provider tests it's the same thing: we're running tests which test consumers
13
+ config.define_derived_metadata(file_path: %r{spec/pact/consumers/}) { |metadata| metadata[:pact_entity] = :provider }
14
+
15
+ # exclude pact specs from generic rspec pipeline
16
+ config.filter_run_excluding :pact_v2
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:pact_v2).tap do |task|
6
+ task.pattern = "spec/pact/consumers/**/*_spec.rb"
7
+ task.rspec_opts = "--require rails_helper_v2 --tag pact_v2"
8
+ end
9
+
10
+ namespace :pact_v2 do
11
+ desc "Verifies the pact files"
12
+ task verify: :pact_v2
13
+ end
data/lib/pact/v2.rb ADDED
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "pact/ffi"
5
+
6
+ require "pact/v2/railtie" if defined?(Rails::Railtie)
7
+
8
+ module Pact
9
+ module V2
10
+ class Error < StandardError; end
11
+
12
+ class ImplementationRequired < Error; end
13
+
14
+ class FfiError < Error
15
+ def initialize(msg, reason, status)
16
+ super(msg)
17
+
18
+ @msg = msg
19
+ @reason = reason
20
+ @status = status
21
+ end
22
+
23
+ def message
24
+ "FFI error: reason: #{@reason}, status: #{@status}, message: #{@msg}"
25
+ end
26
+ end
27
+
28
+ def self.configure
29
+ yield configuration if block_given?
30
+ end
31
+
32
+ def self.configuration
33
+ @configuration ||= Pact::V2::Configuration.new
34
+ end
35
+ end
36
+ end
37
+
38
+ loader = Zeitwerk::Loader.new
39
+ loader.push_dir(File.join(__dir__, ".."))
40
+
41
+ loader.tag = "pact-v2"
42
+
43
+ # existing pact-ruby ignores
44
+ # loader.ignore("#{__dir__}/../pact") # ignore the pact dir at the root of the repo
45
+ # loader.ignore("#{__dir__}/../pact/v2",false) # ignore the pact dir at the root of the repo
46
+ # loader.push_dir(File.join(__dir__))
47
+
48
+
49
+ loader.ignore("#{__dir__}/../pact/version.rb")
50
+ loader.ignore("#{__dir__}/../pact/cli")
51
+ loader.ignore("#{__dir__}/../pact/cli.rb")
52
+ loader.ignore("#{__dir__}/../pact/consumer")
53
+ loader.ignore("#{__dir__}/../pact/consumer.rb")
54
+ loader.ignore("#{__dir__}/../pact/doc")
55
+ loader.ignore("#{__dir__}/../pact/hal")
56
+ loader.ignore("#{__dir__}/../pact/hash_refinements.rb")
57
+ loader.ignore("#{__dir__}/../pact/pact_broker")
58
+ loader.ignore("#{__dir__}/../pact/pact_broker.rb")
59
+ loader.ignore("#{__dir__}/../pact/project_root.rb")
60
+ loader.ignore("#{__dir__}/../pact/provider")
61
+ loader.ignore("#{__dir__}/../pact/provider.rb")
62
+ loader.ignore("#{__dir__}/../pact/retry.rb")
63
+ loader.ignore("#{__dir__}/../pact/tasks")
64
+ loader.ignore("#{__dir__}/../pact/tasks.rb")
65
+ loader.ignore("#{__dir__}/../pact/templates")
66
+ loader.ignore("#{__dir__}/../pact/utils")
67
+ loader.ignore("#{__dir__}/../pact/v2/rspec.rb")
68
+ loader.ignore("#{__dir__}/../pact/v2/rspec")
69
+ loader.ignore("#{__dir__}/../pact/v2/railtie.rb") unless defined?(Rails::Railtie)
70
+ loader.setup
71
+ loader.eager_load