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,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pact_message_helpers"
4
+ require_relative "webmock/webmock_helpers"
5
+
6
+ module SbmtPactProducerDsl
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 _pact_provider(transport_type, provider, opts: {})
23
+ raise "#{transport_type}_pact_provider is designed to be used with RSpec" unless defined?(::RSpec)
24
+ raise "#{transport_type}_pact_provider has to be declared at the top level of a suite" unless top_level?
25
+ raise "*_pact_provider is designed to be run once per provider so cannot be declared more than once" if defined?(@_pact_config)
26
+
27
+ pact_config_instance = Sbmt::Pact::Provider::PactConfig.new(transport_type, provider_name: provider, opts: opts)
28
+ instance_variable_set(:@_pact_config, pact_config_instance)
29
+
30
+ # rubocop:disable RSpec/BeforeAfterAll
31
+ before(:context) do
32
+ # rspec allows only context ivars in specs and ignores the rest
33
+ # so we use block-as-a-closure feature to save pact_config ivar reference and make it available for descendants
34
+ @_pact_config = pact_config_instance
35
+ end
36
+ # rubocop:enable RSpec/BeforeAfterAll
37
+
38
+ it "verifies interactions with provider #{provider}" do
39
+ pact_config.new_verifier.verify!
40
+ end
41
+ end
42
+
43
+ def before_state_setup(&block)
44
+ raise PACT_PROVIDER_NOT_DECLARED_MESSAGE unless pact_config
45
+ pact_config.before_setup(&block)
46
+ end
47
+
48
+ def after_state_teardown(&block)
49
+ raise PACT_PROVIDER_NOT_DECLARED_MESSAGE unless pact_config
50
+ pact_config.after_teardown(&block)
51
+ end
52
+
53
+ def provider_state(name, opts: {}, &block)
54
+ raise PACT_PROVIDER_NOT_DECLARED_MESSAGE unless pact_config
55
+ pact_config.new_provider_state(name, opts: opts, &block)
56
+ end
57
+
58
+ def handle_message(name, opts: {}, &block)
59
+ raise "message_pact_provider should be declared first" unless pact_config
60
+ raise "message_pact_provider should be declared first" unless pact_config.is_a?(Sbmt::Pact::Provider::PactConfig::Async)
61
+ pact_config.new_message_handler(name, opts: opts, &block)
62
+ end
63
+
64
+ def pact_config
65
+ instance_variable_get(:@_pact_config)
66
+ end
67
+ end
68
+
69
+ def pact_config
70
+ instance_variable_get(:@_pact_config)
71
+ end
72
+ end
73
+
74
+ RSpec.configure do |config|
75
+ config.include SbmtPactProducerDsl, pact_entity: :provider
76
+ config.extend SbmtPactProducerDsl::ClassMethods, pact_entity: :provider
77
+
78
+ config.around pact_entity: :provider do |example|
79
+ WebmockHelpers.turned_off do
80
+ if defined?(::VCR)
81
+ VCR.turned_off { example.run }
82
+ else
83
+ example.run
84
+ end
85
+ end
86
+ end
87
+ 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
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).tap do |task|
6
+ task.pattern = "spec/pact/consumers/**/*_spec.rb"
7
+ task.rspec_opts = "--require rails_helper --tag pact"
8
+ end
9
+
10
+ namespace :pact do
11
+ desc "Verifies the pact files"
12
+ task verify: :pact
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Pact
5
+ VERSION = "0.12.0"
6
+ end
7
+ end
data/lib/sbmt/pact.rb ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "pact/ffi"
5
+
6
+ require "sbmt/pact/railtie" if defined?(Rails::Railtie)
7
+
8
+ module Sbmt
9
+ module Pact
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 ||= Sbmt::Pact::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 = "sbmt-pact"
42
+
43
+ loader.ignore("#{__dir__}/pact/version.rb")
44
+ loader.ignore("#{__dir__}/pact/rspec.rb")
45
+ loader.ignore("#{__dir__}/pact/rspec")
46
+
47
+ loader.setup
48
+ loader.eager_load
data/sbmt-pact.gemspec ADDED
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/sbmt/pact/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "sbmt-pact"
7
+ spec.license = "MIT"
8
+ spec.version = Sbmt::Pact::VERSION
9
+ spec.authors = ["Kuper Ruby Platform Team"]
10
+
11
+ spec.summary = "Ruby gem for simplified Pact testing between microservices, supporting the latest Pact specifications and multiple transport protocols"
12
+ spec.description = "It is a powerful Ruby gem designed to streamline Pact testing in microservice architectures. It supports the latest Pact specifications and offers capabilities beyond the current pact-ruby gem, including support for non-HTTP transports like gRPC and async messaging systems like Kafka."
13
+ spec.homepage = "https://github.com/Kuper-Tech/sbmt-pact"
14
+ spec.required_ruby_version = ">= 2.7.0"
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = spec.homepage
20
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
21
+ spec.metadata["rubygems_mfa_required"] = "false" # rubocop:disable Gemspec/RequireMFA
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.add_dependency "zeitwerk", "~> 2.3"
35
+ spec.add_dependency "pact-ffi", "~> 0.4.22"
36
+ spec.add_dependency "rack", "~> 2.0"
37
+ spec.add_dependency "webrick"
38
+ spec.add_dependency "rack-proxy"
39
+
40
+ spec.add_development_dependency "appraisal", ">= 2.4"
41
+ spec.add_development_dependency "bundler", ">= 2.3"
42
+ spec.add_development_dependency "combustion", ">= 1.3"
43
+ spec.add_development_dependency "gruf", ">= 2.18"
44
+ spec.add_development_dependency "rake", ">= 13.0"
45
+ spec.add_development_dependency "sbmt-kafka_consumer", ">= 2.0.1"
46
+ spec.add_development_dependency "sbmt-kafka_producer", ">= 1.0"
47
+ spec.add_development_dependency "rspec"
48
+ spec.add_development_dependency "rspec-rails"
49
+ spec.add_development_dependency "rspec_junit_formatter"
50
+ spec.add_development_dependency "rubocop"
51
+ spec.add_development_dependency "rubocop-rails"
52
+ spec.add_development_dependency "rubocop-rspec"
53
+ spec.add_development_dependency "rubocop-performance"
54
+ spec.add_development_dependency "standard", ">= 1.35.1"
55
+ spec.add_development_dependency "vcr", ">= 6.0"
56
+ spec.add_development_dependency "faraday", "> 1.0"
57
+ spec.add_development_dependency "webmock", ">= 3.0"
58
+ spec.add_development_dependency "gruf-rspec", ">= 0.6.0"
59
+ end