pact-v2 2.0.0.pre.preview1

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 (164) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1321 -0
  3. data/LICENSE.txt +23 -0
  4. data/bin/pact +4 -0
  5. data/lib/pact/cli/generate_pact_docs.rb +4 -0
  6. data/lib/pact/cli/run_pact_verification.rb +99 -0
  7. data/lib/pact/cli/spec_criteria.rb +26 -0
  8. data/lib/pact/cli.rb +45 -0
  9. data/lib/pact/consumer/configuration/configuration_extensions.rb +90 -0
  10. data/lib/pact/consumer/configuration/dsl.rb +11 -0
  11. data/lib/pact/consumer/configuration/mock_service.rb +112 -0
  12. data/lib/pact/consumer/configuration/service_consumer.rb +51 -0
  13. data/lib/pact/consumer/configuration/service_provider.rb +40 -0
  14. data/lib/pact/consumer/configuration.rb +10 -0
  15. data/lib/pact/consumer/consumer_contract_builder.rb +82 -0
  16. data/lib/pact/consumer/consumer_contract_builders.rb +10 -0
  17. data/lib/pact/consumer/interaction_builder.rb +45 -0
  18. data/lib/pact/consumer/rspec.rb +35 -0
  19. data/lib/pact/consumer/spec_hooks.rb +40 -0
  20. data/lib/pact/consumer/world.rb +37 -0
  21. data/lib/pact/consumer.rb +7 -0
  22. data/lib/pact/doc/README.md +13 -0
  23. data/lib/pact/doc/doc_file.rb +40 -0
  24. data/lib/pact/doc/generate.rb +11 -0
  25. data/lib/pact/doc/generator.rb +82 -0
  26. data/lib/pact/doc/interaction_view_model.rb +124 -0
  27. data/lib/pact/doc/markdown/consumer_contract_renderer.rb +68 -0
  28. data/lib/pact/doc/markdown/generator.rb +26 -0
  29. data/lib/pact/doc/markdown/index_renderer.rb +43 -0
  30. data/lib/pact/doc/markdown/interaction.erb +14 -0
  31. data/lib/pact/doc/markdown/interaction_renderer.rb +43 -0
  32. data/lib/pact/doc/sort_interactions.rb +16 -0
  33. data/lib/pact/hal/authorization_header_redactor.rb +32 -0
  34. data/lib/pact/hal/entity.rb +110 -0
  35. data/lib/pact/hal/http_client.rb +128 -0
  36. data/lib/pact/hal/link.rb +112 -0
  37. data/lib/pact/hal/non_json_entity.rb +28 -0
  38. data/lib/pact/hash_refinements.rb +17 -0
  39. data/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb +112 -0
  40. data/lib/pact/pact_broker/fetch_pacts.rb +103 -0
  41. data/lib/pact/pact_broker/notices.rb +34 -0
  42. data/lib/pact/pact_broker/pact_selection_description.rb +66 -0
  43. data/lib/pact/pact_broker.rb +25 -0
  44. data/lib/pact/project_root.rb +7 -0
  45. data/lib/pact/provider/configuration/configuration_extension.rb +69 -0
  46. data/lib/pact/provider/configuration/dsl.rb +18 -0
  47. data/lib/pact/provider/configuration/message_provider_dsl.rb +63 -0
  48. data/lib/pact/provider/configuration/pact_verification.rb +48 -0
  49. data/lib/pact/provider/configuration/pact_verification_from_broker.rb +126 -0
  50. data/lib/pact/provider/configuration/service_provider_config.rb +32 -0
  51. data/lib/pact/provider/configuration/service_provider_dsl.rb +107 -0
  52. data/lib/pact/provider/configuration.rb +7 -0
  53. data/lib/pact/provider/context.rb +0 -0
  54. data/lib/pact/provider/help/console_text.rb +76 -0
  55. data/lib/pact/provider/help/content.rb +38 -0
  56. data/lib/pact/provider/help/pact_diff.rb +43 -0
  57. data/lib/pact/provider/help/prompt_text.rb +49 -0
  58. data/lib/pact/provider/help/write.rb +56 -0
  59. data/lib/pact/provider/matchers/messages.rb +66 -0
  60. data/lib/pact/provider/pact_helper_locator.rb +24 -0
  61. data/lib/pact/provider/pact_source.rb +40 -0
  62. data/lib/pact/provider/pact_spec_runner.rb +188 -0
  63. data/lib/pact/provider/pact_uri.rb +55 -0
  64. data/lib/pact/provider/pact_verification.rb +17 -0
  65. data/lib/pact/provider/print_missing_provider_states.rb +35 -0
  66. data/lib/pact/provider/request.rb +77 -0
  67. data/lib/pact/provider/rspec/backtrace_formatter.rb +43 -0
  68. data/lib/pact/provider/rspec/calculate_exit_code.rb +18 -0
  69. data/lib/pact/provider/rspec/custom_options_file +0 -0
  70. data/lib/pact/provider/rspec/formatter_rspec_2.rb +76 -0
  71. data/lib/pact/provider/rspec/formatter_rspec_3.rb +195 -0
  72. data/lib/pact/provider/rspec/json_formatter.rb +100 -0
  73. data/lib/pact/provider/rspec/matchers.rb +80 -0
  74. data/lib/pact/provider/rspec/pact_broker_formatter.rb +76 -0
  75. data/lib/pact/provider/rspec.rb +234 -0
  76. data/lib/pact/provider/state/provider_state.rb +180 -0
  77. data/lib/pact/provider/state/provider_state_configured_modules.rb +15 -0
  78. data/lib/pact/provider/state/provider_state_manager.rb +42 -0
  79. data/lib/pact/provider/state/provider_state_proxy.rb +39 -0
  80. data/lib/pact/provider/state/set_up.rb +13 -0
  81. data/lib/pact/provider/state/tear_down.rb +13 -0
  82. data/lib/pact/provider/test_methods.rb +77 -0
  83. data/lib/pact/provider/verification_report.rb +36 -0
  84. data/lib/pact/provider/verification_results/create.rb +88 -0
  85. data/lib/pact/provider/verification_results/publish.rb +143 -0
  86. data/lib/pact/provider/verification_results/publish_all.rb +50 -0
  87. data/lib/pact/provider/verification_results/verification_result.rb +40 -0
  88. data/lib/pact/provider/world.rb +50 -0
  89. data/lib/pact/provider.rb +3 -0
  90. data/lib/pact/retry.rb +37 -0
  91. data/lib/pact/tasks/task_helper.rb +62 -0
  92. data/lib/pact/tasks/verification_task.rb +95 -0
  93. data/lib/pact/tasks.rb +2 -0
  94. data/lib/pact/templates/help.erb +22 -0
  95. data/lib/pact/templates/provider_state.erb +14 -0
  96. data/lib/pact/utils/metrics.rb +100 -0
  97. data/lib/pact/utils/string.rb +35 -0
  98. data/lib/pact/v2/configuration.rb +23 -0
  99. data/lib/pact/v2/consumer/grpc_interaction_builder.rb +187 -0
  100. data/lib/pact/v2/consumer/http_interaction_builder.rb +163 -0
  101. data/lib/pact/v2/consumer/interaction_contents.rb +54 -0
  102. data/lib/pact/v2/consumer/message_interaction_builder.rb +280 -0
  103. data/lib/pact/v2/consumer/mock_server.rb +99 -0
  104. data/lib/pact/v2/consumer/pact_config/base.rb +24 -0
  105. data/lib/pact/v2/consumer/pact_config/grpc.rb +26 -0
  106. data/lib/pact/v2/consumer/pact_config/http.rb +55 -0
  107. data/lib/pact/v2/consumer/pact_config/message.rb +17 -0
  108. data/lib/pact/v2/consumer/pact_config.rb +24 -0
  109. data/lib/pact/v2/consumer.rb +8 -0
  110. data/lib/pact/v2/matchers/base.rb +67 -0
  111. data/lib/pact/v2/matchers/v1/equality.rb +19 -0
  112. data/lib/pact/v2/matchers/v2/regex.rb +19 -0
  113. data/lib/pact/v2/matchers/v2/type.rb +17 -0
  114. data/lib/pact/v2/matchers/v3/boolean.rb +17 -0
  115. data/lib/pact/v2/matchers/v3/date.rb +18 -0
  116. data/lib/pact/v2/matchers/v3/date_time.rb +18 -0
  117. data/lib/pact/v2/matchers/v3/decimal.rb +17 -0
  118. data/lib/pact/v2/matchers/v3/each.rb +42 -0
  119. data/lib/pact/v2/matchers/v3/include.rb +17 -0
  120. data/lib/pact/v2/matchers/v3/integer.rb +17 -0
  121. data/lib/pact/v2/matchers/v3/number.rb +17 -0
  122. data/lib/pact/v2/matchers/v3/time.rb +18 -0
  123. data/lib/pact/v2/matchers/v4/each_key.rb +26 -0
  124. data/lib/pact/v2/matchers/v4/each_key_value.rb +32 -0
  125. data/lib/pact/v2/matchers/v4/each_value.rb +33 -0
  126. data/lib/pact/v2/matchers/v4/not_empty.rb +17 -0
  127. data/lib/pact/v2/matchers.rb +94 -0
  128. data/lib/pact/v2/native/blocking_verifier.rb +17 -0
  129. data/lib/pact/v2/native/logger.rb +25 -0
  130. data/lib/pact/v2/provider/async_message_verifier.rb +28 -0
  131. data/lib/pact/v2/provider/base_verifier.rb +242 -0
  132. data/lib/pact/v2/provider/grpc_verifier.rb +38 -0
  133. data/lib/pact/v2/provider/gruf_server.rb +75 -0
  134. data/lib/pact/v2/provider/http_server.rb +79 -0
  135. data/lib/pact/v2/provider/http_verifier.rb +43 -0
  136. data/lib/pact/v2/provider/message_provider_servlet.rb +79 -0
  137. data/lib/pact/v2/provider/mixed_verifier.rb +22 -0
  138. data/lib/pact/v2/provider/pact_broker_proxy.rb +71 -0
  139. data/lib/pact/v2/provider/pact_broker_proxy_runner.rb +77 -0
  140. data/lib/pact/v2/provider/pact_config/async.rb +29 -0
  141. data/lib/pact/v2/provider/pact_config/base.rb +101 -0
  142. data/lib/pact/v2/provider/pact_config/grpc.rb +26 -0
  143. data/lib/pact/v2/provider/pact_config/http.rb +27 -0
  144. data/lib/pact/v2/provider/pact_config/mixed.rb +39 -0
  145. data/lib/pact/v2/provider/pact_config.rb +26 -0
  146. data/lib/pact/v2/provider/provider_server_runner.rb +89 -0
  147. data/lib/pact/v2/provider/provider_state_configuration.rb +32 -0
  148. data/lib/pact/v2/provider/provider_state_servlet.rb +86 -0
  149. data/lib/pact/v2/provider.rb +8 -0
  150. data/lib/pact/v2/railtie.rb +13 -0
  151. data/lib/pact/v2/rspec/support/pact_consumer_helpers.rb +80 -0
  152. data/lib/pact/v2/rspec/support/pact_message_helpers.rb +42 -0
  153. data/lib/pact/v2/rspec/support/pact_provider_helpers.rb +129 -0
  154. data/lib/pact/v2/rspec/support/waterdrop/pact_waterdrop_client.rb +27 -0
  155. data/lib/pact/v2/rspec/support/webmock/webmock_helpers.rb +30 -0
  156. data/lib/pact/v2/rspec.rb +17 -0
  157. data/lib/pact/v2/tasks/pact.rake +13 -0
  158. data/lib/pact/v2/version.rb +8 -0
  159. data/lib/pact/v2.rb +71 -0
  160. data/lib/pact/version.rb +4 -0
  161. data/lib/pact.rb +13 -0
  162. data/lib/tasks/pact.rake +34 -0
  163. data/pact.gemspec +106 -0
  164. metadata +529 -0
@@ -0,0 +1,100 @@
1
+ require 'securerandom'
2
+ require 'digest'
3
+ require 'socket'
4
+ require 'pact/version'
5
+ require 'net/http'
6
+
7
+ module Pact
8
+ module Utils
9
+ class Metrics
10
+
11
+ def self.report_metric(event, category, action, value = 1)
12
+ do_once_per_thread(:pact_metrics_message_shown) do
13
+ if track_events?
14
+ Pact.configuration.output_stream.puts "pact WARN: Please note: we are tracking events anonymously to gather important usage statistics like Pact-Ruby version
15
+ and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment
16
+ variable to 'true'."
17
+ end
18
+ end
19
+
20
+ in_thread do
21
+ begin
22
+ if track_events?
23
+ uri = URI('https://www.google-analytics.com/collect')
24
+ req = Net::HTTP::Post.new(uri)
25
+ req.set_form_data(create_tracking_event(event, category, action, value))
26
+
27
+ Net::HTTP.start(uri.hostname, uri.port, read_timeout:2, open_timeout:2, :use_ssl => true ) do |http|
28
+ http.request(req)
29
+ end
30
+ end
31
+ rescue StandardError => e
32
+ handle_error(e)
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def self.handle_error e
40
+ if ENV['PACT_METRICS_DEBUG'] == 'true'
41
+ Pact.configuration.output_stream.puts("DEBUG: #{e.inspect}\n" + e.backtrace.join("\n"))
42
+ end
43
+ end
44
+
45
+ def self.in_thread
46
+ Thread.new do
47
+ yield
48
+ end
49
+ end
50
+
51
+ # not super safe to use the thread, but it's good enough for this usecase
52
+ def self.do_once_per_thread(key)
53
+ result = nil
54
+ if !Thread.current[key]
55
+ result = yield
56
+ end
57
+ Thread.current[key] = true
58
+ result
59
+ end
60
+
61
+ def self.create_tracking_event(event, category, action, value)
62
+ {
63
+ "v" => 1,
64
+ "t" => "event",
65
+ "tid" => "UA-117778936-1",
66
+ "cid" => calculate_cid,
67
+ "an" => "Pact Ruby",
68
+ "av" => Pact::VERSION,
69
+ "aid" => "pact-ruby",
70
+ "aip" => 1,
71
+ "ds" => ENV['PACT_EXECUTING_LANGUAGE'] ? "client" : "cli",
72
+ "cd2" => ENV['CI'] == "true" ? "CI" : "unknown",
73
+ "cd3" => RUBY_PLATFORM,
74
+ "cd6" => ENV['PACT_EXECUTING_LANGUAGE'] || "unknown",
75
+ "cd7" => ENV['PACT_EXECUTING_LANGUAGE_VERSION'],
76
+ "el" => event,
77
+ "ec" => category,
78
+ "ea" => action,
79
+ "ev" => value
80
+ }
81
+ end
82
+
83
+ def self.track_events?
84
+ ENV['PACT_DO_NOT_TRACK'] != 'true'
85
+ end
86
+
87
+ def self.calculate_cid
88
+ if RUBY_PLATFORM.include? "windows"
89
+ hostname = ENV['COMPUTERNAME']
90
+ else
91
+ hostname = ENV['HOSTNAME']
92
+ end
93
+ if !hostname
94
+ hostname = Socket.gethostname
95
+ end
96
+ Digest::MD5.hexdigest hostname || SecureRandom.urlsafe_base64(5)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,35 @@
1
+ # Can't use refinements because of Travelling Ruby
2
+
3
+ module Pact
4
+ module Utils
5
+ module String
6
+
7
+ extend self
8
+
9
+ # ripped from rubyworks/facets, thank you
10
+ def camelcase(string, *separators)
11
+ case separators.first
12
+ when Symbol, TrueClass, FalseClass, NilClass
13
+ first_letter = separators.shift
14
+ end
15
+
16
+ separators = ['_', '\s'] if separators.empty?
17
+
18
+ str = string.dup
19
+
20
+ separators.each do |s|
21
+ str = str.gsub(/(?:#{s}+)([a-z])/){ $1.upcase }
22
+ end
23
+
24
+ case first_letter
25
+ when :upper, true
26
+ str = str.gsub(/(\A|\s)([a-z])/){ $1 + $2.upcase }
27
+ when :lower, false
28
+ str = str.gsub(/(\A|\s)([A-Z])/){ $1 + $2.downcase }
29
+ end
30
+
31
+ str
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ class Configuration
6
+ attr_reader :before_provider_state_proc, :after_provider_state_proc
7
+
8
+ class GlobalProviderConfigurationError < ::Pact::V2::Error; end
9
+
10
+ def before_provider_state_setup(&block)
11
+ raise GlobalProviderConfigurationError, "no block given" unless block
12
+
13
+ @before_provider_state_proc = block
14
+ end
15
+
16
+ def after_provider_state_teardown(&block)
17
+ raise GlobalProviderConfigurationError, "no block given" unless block
18
+
19
+ @after_provider_state_proc = block
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/sync_message_consumer"
4
+ require "pact/ffi/plugin_consumer"
5
+ require "pact/ffi/logger"
6
+
7
+ module Pact
8
+ module V2
9
+ module Consumer
10
+ class GrpcInteractionBuilder
11
+ CONTENT_TYPE = "application/protobuf"
12
+ GRPC_CONTENT_TYPE = "application/grpc"
13
+ PROTOBUF_PLUGIN_NAME = "protobuf"
14
+ PROTOBUF_PLUGIN_VERSION = "0.6.5"
15
+
16
+ class PluginInitError < Pact::V2::FfiError; end
17
+
18
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html
19
+ INIT_PLUGIN_ERRORS = {
20
+ 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
21
+ 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"},
22
+ 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"}
23
+ }.freeze
24
+
25
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html
26
+ CREATE_INTERACTION_ERRORS = {
27
+ 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
28
+ 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"},
29
+ 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"},
30
+ 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"},
31
+ 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"},
32
+ 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"}
33
+ }.freeze
34
+
35
+ class CreateInteractionError < Pact::V2::FfiError; end
36
+
37
+ class InteractionMismatchesError < Pact::V2::Error; end
38
+
39
+ class InteractionBuilderError < Pact::V2::Error; end
40
+
41
+ def initialize(pact_config, description: nil)
42
+ @pact_config = pact_config
43
+ @description = description || ""
44
+ @proto_path = nil
45
+ @proto_include_dirs = []
46
+ @service_name = nil
47
+ @method_name = nil
48
+ @request = nil
49
+ @response = nil
50
+ @response_meta = nil
51
+ @provider_state_meta = nil
52
+ end
53
+
54
+ def with_service(proto_path, method, include_dirs = [])
55
+ raise InteractionBuilderError.new("invalid grpc method: cannot be blank") if method.blank?
56
+
57
+ service_name, method_name = method.split("/") || []
58
+ raise InteractionBuilderError.new("invalid grpc method: #{method}, should be like service/SomeMethod") if service_name.blank? || method_name.blank?
59
+
60
+ absolute_path = File.expand_path(proto_path)
61
+ raise InteractionBuilderError.new("proto file #{proto_path} does not exist") unless File.exist?(absolute_path)
62
+
63
+ @proto_path = absolute_path
64
+ @service_name = service_name
65
+ @method_name = method_name
66
+ @proto_include_dirs = include_dirs.map { |dir| File.expand_path(dir) }
67
+
68
+ self
69
+ end
70
+
71
+ def given(provider_state, metadata = {})
72
+ @provider_state_meta = {provider_state => metadata}
73
+ self
74
+ end
75
+
76
+ def upon_receiving(description)
77
+ @description = description
78
+ self
79
+ end
80
+
81
+ def with_request(req_hash)
82
+ @request = InteractionContents.plugin(req_hash)
83
+ self
84
+ end
85
+
86
+ def will_respond_with(resp_hash)
87
+ @response = InteractionContents.plugin(resp_hash)
88
+ self
89
+ end
90
+
91
+ def will_respond_with_meta(meta_hash)
92
+ @response_meta = InteractionContents.plugin(meta_hash)
93
+ self
94
+ end
95
+
96
+ def interaction_json
97
+ result = {
98
+ "pact:proto": @proto_path,
99
+ "pact:proto-service": "#{@service_name}/#{@method_name}",
100
+ "pact:content-type": CONTENT_TYPE,
101
+ request: @request
102
+ }
103
+
104
+ result["pact:protobuf-config"] = {additionalIncludes: @proto_include_dirs} if @proto_include_dirs.present?
105
+
106
+ result[:response] = @response if @response.is_a?(Hash)
107
+ result[:responseMetadata] = @response_meta if @response_meta.is_a?(Hash)
108
+
109
+ JSON.dump(result)
110
+ end
111
+
112
+ def validate!
113
+ raise InteractionBuilderError.new("uninitialized service params, use #with_service to configure") if @proto_path.blank? || @service_name.blank? || @method_name.blank?
114
+ raise InteractionBuilderError.new("invalid request format, should be a hash") unless @request.is_a?(Hash)
115
+ raise InteractionBuilderError.new("invalid response format, should be a hash") unless @response.is_a?(Hash) || @response_meta.is_a?(Hash)
116
+ end
117
+
118
+ def execute(&block)
119
+ raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used)
120
+
121
+ validate!
122
+
123
+ pact_handle = init_pact
124
+ init_plugin!(pact_handle)
125
+
126
+ message_pact = PactFfi::SyncMessageConsumer.new_interaction(pact_handle, @description)
127
+ @provider_state_meta&.each_pair do |provider_state, meta|
128
+ if meta.present?
129
+ meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) }
130
+ else
131
+ PactFfi.given(message_pact, provider_state)
132
+ end
133
+ end
134
+
135
+ result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, GRPC_CONTENT_TYPE, interaction_json)
136
+ if CREATE_INTERACTION_ERRORS[result].present?
137
+ error = CREATE_INTERACTION_ERRORS[result]
138
+ raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status])
139
+ end
140
+
141
+ mock_server = MockServer.create_for_grpc!(pact: pact_handle, host: @pact_config.mock_host, port: @pact_config.mock_port)
142
+
143
+ yield(message_pact, mock_server)
144
+
145
+ if mock_server.matched?
146
+ mock_server.write_pacts!(@pact_config.pact_dir)
147
+ else
148
+ msg = mismatches_error_msg(mock_server)
149
+ raise InteractionMismatchesError.new(msg)
150
+ end
151
+ ensure
152
+ @used = true
153
+ mock_server&.cleanup
154
+ PactFfi::PluginConsumer.cleanup_plugins(pact_handle)
155
+ PactFfi.free_pact_handle(pact_handle)
156
+ end
157
+
158
+ private
159
+
160
+ def mismatches_error_msg(mock_server)
161
+ rspec_example_desc = RSpec.current_example&.description
162
+ return "interaction for #{@service_name}/#{@method_name} has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank?
163
+
164
+ "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}"
165
+ end
166
+
167
+ def init_pact
168
+ handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name)
169
+ PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"])
170
+ PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version)
171
+
172
+ Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level)
173
+
174
+ handle
175
+ end
176
+
177
+ def init_plugin!(pact_handle)
178
+ result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, PROTOBUF_PLUGIN_VERSION)
179
+ return result if INIT_PLUGIN_ERRORS[result].blank?
180
+
181
+ error = INIT_PLUGIN_ERRORS[result]
182
+ raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status])
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/sync_message_consumer"
4
+ require "pact/ffi/plugin_consumer"
5
+ require "pact/ffi/logger"
6
+ require "json"
7
+
8
+ module Pact
9
+ module V2
10
+ module Consumer
11
+ class HttpInteractionBuilder
12
+ DESCRIPTION_PREFIX = "http: "
13
+
14
+ # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html
15
+ CREATE_INTERACTION_ERRORS = {
16
+ 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
17
+ 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"},
18
+ 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"},
19
+ 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"},
20
+ 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"},
21
+ 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"}
22
+ }.freeze
23
+
24
+ class CreateInteractionError < Pact::V2::FfiError; end
25
+
26
+ class InteractionMismatchesError < Pact::V2::Error; end
27
+
28
+ class InteractionBuilderError < Pact::V2::Error; end
29
+
30
+ class << self
31
+ def create_finalizer(pact_handle)
32
+ proc { PactFfi.free_pact_handle(pact_handle) }
33
+ end
34
+ end
35
+
36
+ def initialize(pact_config, description: nil)
37
+ @pact_config = pact_config
38
+ @description = description || ""
39
+
40
+ @pact_handle = pact_config.pact_handle ||= init_pact
41
+ @pact_interaction = PactFfi.new_interaction(pact_handle, @description)
42
+
43
+ ObjectSpace.define_finalizer(self, self.class.create_finalizer(pact_interaction))
44
+ end
45
+
46
+ def given(provider_state, metadata = {})
47
+ if metadata.present?
48
+ PactFfi.given_with_params(pact_interaction, provider_state, JSON.dump(metadata))
49
+ else
50
+ PactFfi.given(pact_interaction, provider_state)
51
+ end
52
+
53
+ self
54
+ end
55
+
56
+ def upon_receiving(description)
57
+ @description = description
58
+ PactFfi.upon_receiving(pact_interaction, @description)
59
+ self
60
+ end
61
+
62
+ def with_request(method: nil, path: nil, query: {}, headers: {}, body: nil)
63
+ interaction_part = PactFfi::FfiInteractionPart["INTERACTION_PART_REQUEST"]
64
+ PactFfi.with_request(pact_interaction, method.to_s, format_value(path))
65
+
66
+ # Processing as an array of hashes, allows us to consider duplicate keys
67
+ # which should be passed to the core, at a non 0 index
68
+ if query.is_a?(Array)
69
+ key_index = Hash.new(0)
70
+ query.each do |query_item|
71
+ InteractionContents.basic(query_item).each_pair do |key, value_item|
72
+ PactFfi.with_query_parameter_v2(pact_interaction, key.to_s, key_index[key], format_value(value_item))
73
+ key_index[key] += 1
74
+ end
75
+ end
76
+ else
77
+ InteractionContents.basic(query).each_pair do |key, value_item|
78
+ PactFfi.with_query_parameter_v2(pact_interaction, key.to_s, 0, format_value(value_item))
79
+ end
80
+ end
81
+
82
+ InteractionContents.basic(headers).each_pair do |key, value_item|
83
+ PactFfi.with_header_v2(pact_interaction, interaction_part, key.to_s, 0, format_value(value_item))
84
+ end
85
+
86
+ if body
87
+ PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body)))
88
+ end
89
+
90
+ self
91
+ end
92
+
93
+ def will_respond_with(status: nil, headers: {}, body: nil)
94
+ interaction_part = PactFfi::FfiInteractionPart["INTERACTION_PART_RESPONSE"]
95
+ PactFfi.response_status(pact_interaction, status)
96
+
97
+ InteractionContents.basic(headers).each_pair do |key, value_item|
98
+ PactFfi.with_header_v2(pact_interaction, interaction_part, key.to_s, 0, format_value(value_item))
99
+ end
100
+
101
+ if body
102
+ PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body)))
103
+ end
104
+
105
+ self
106
+ end
107
+
108
+ def execute(&block)
109
+ raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used)
110
+
111
+ mock_server = MockServer.create_for_http!(
112
+ pact: pact_handle, host: pact_config.mock_host, port: pact_config.mock_port
113
+ )
114
+
115
+ yield(mock_server)
116
+
117
+ if mock_server.matched?
118
+ mock_server.write_pacts!(pact_config.pact_dir)
119
+ else
120
+ msg = mismatches_error_msg(mock_server)
121
+ raise InteractionMismatchesError.new(msg)
122
+ end
123
+ ensure
124
+ @used = true
125
+ mock_server&.cleanup
126
+ # Reset the pact handle to allow for a new interaction to be built
127
+ # without previous interactions being included
128
+ @pact_config.reset_pact
129
+ end
130
+
131
+ private
132
+
133
+ attr_reader :pact_handle, :pact_interaction, :pact_config
134
+
135
+ def mismatches_error_msg(mock_server)
136
+ rspec_example_desc = RSpec.current_example&.description
137
+ mismatches = JSON.pretty_generate(JSON.parse(mock_server.mismatches))
138
+ mismatches_with_colored_keys = mismatches.gsub(/"([^"]+)":/) { |match| "\e[34m#{match}\e[0m" } # Blue keys / white values
139
+
140
+ "#{rspec_example_desc} has mismatches: #{mismatches_with_colored_keys}"
141
+ end
142
+
143
+ def init_pact
144
+ handle = PactFfi.new_pact(pact_config.consumer_name, pact_config.provider_name)
145
+ PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_#{pact_config.pact_specification}"])
146
+ PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version)
147
+
148
+ Pact::V2::Native::Logger.log_to_stdout(pact_config.log_level)
149
+
150
+ handle
151
+ end
152
+
153
+ def format_value(obj)
154
+ return obj if obj.is_a?(String)
155
+
156
+ return JSON.dump({value: obj}) if obj.is_a?(Array)
157
+
158
+ JSON.dump(obj)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
5
+ module Consumer
6
+ class InteractionContents < Hash
7
+ BASIC_FORMAT = :basic
8
+ PLUGIN_FORMAT = :plugin
9
+
10
+ attr_reader :format
11
+
12
+ def self.basic(contents_hash)
13
+ new(contents_hash, BASIC_FORMAT)
14
+ end
15
+
16
+ def self.plugin(contents_hash)
17
+ new(contents_hash, PLUGIN_FORMAT)
18
+ end
19
+
20
+ def initialize(contents_hash, format)
21
+ init_hash(contents_hash, format).each_pair { |k, v| self[k] = v }
22
+ @format = format
23
+ end
24
+
25
+ private
26
+
27
+ def serialize(hash, format)
28
+ # serialize recursively
29
+ if hash.is_a?(String)
30
+ return hash
31
+ end
32
+ if hash.is_a?(Pact::V2::Matchers::Base)
33
+ return hash.as_basic if format == :basic
34
+ return hash.as_plugin if format == :plugin
35
+ end
36
+ hash.each_pair do |key, value|
37
+ next serialize(value, format) if value.is_a?(Hash)
38
+ next hash[key] = value.map { |v| serialize(v, format) } if value.is_a?(Array)
39
+ if value.is_a?(Pact::V2::Matchers::Base)
40
+ hash[key] = value.as_basic if format == :basic
41
+ hash[key] = value.as_plugin if format == :plugin
42
+ end
43
+ end
44
+
45
+ hash
46
+ end
47
+
48
+ def init_hash(hash, format)
49
+ serialize(hash.deep_dup, format)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end