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,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/verifier"
4
+ require "pact/v2/native/logger"
5
+ require "pact/v2/native/blocking_verifier"
6
+
7
+ module Pact
8
+ module V2
9
+ module Provider
10
+ class BaseVerifier
11
+ PROVIDER_TRANSPORT_TYPE = nil
12
+ attr_reader :logger
13
+
14
+ class VerificationError < Pact::V2::FfiError; end
15
+
16
+ class VerifierError < Pact::V2::Error; end
17
+
18
+ DEFAULT_CONSUMER_SELECTORS = {}
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, mixed_config = nil)
31
+ raise ArgumentError, "pact_config must be a subclass of Pact::V2::Provider::PactConfig::Base" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Base)
32
+ @pact_config = pact_config
33
+ @mixed_config = mixed_config
34
+ @logger = Logger.new($stdout)
35
+ end
36
+
37
+ def verify!
38
+ raise VerifierError.new("interaction is designed to be used one-time only") if defined?(@used)
39
+
40
+ # if consumer_selectors.blank?
41
+ # logger.info("[verifier] does not need to verify consumer #{@pact_config.consumer_name}")
42
+ # return
43
+ # end
44
+
45
+ exception = nil
46
+ pact_handle = init_pact
47
+
48
+ start_servers!
49
+
50
+ logger.info("[verifier] starting provider verification")
51
+
52
+ result = Pact::V2::Native::BlockingVerifier.execute(pact_handle)
53
+ if VERIFICATION_ERRORS[result].present?
54
+ error = VERIFICATION_ERRORS[result]
55
+ exception = VerificationError.new("There was an error while trying to verify provider \"#{@pact_config.provider_name}\"", error[:reason], error[:status])
56
+ end
57
+ ensure
58
+ @used = true
59
+ PactFfi::Verifier.shutdown(pact_handle) if pact_handle
60
+ stop_servers
61
+ @grpc_server.stop if @grpc_server
62
+ raise exception if exception
63
+ end
64
+
65
+ private
66
+
67
+ def create_c_pointer_array_from_string_array(string_array)
68
+ pointers = string_array.map { |str| FFI::MemoryPointer.from_string(str) }
69
+ array_pointer = FFI::MemoryPointer.new(:pointer, pointers.size)
70
+ pointers.each_with_index do |ptr, index|
71
+ array_pointer[index].put_pointer(0, ptr)
72
+ end
73
+ array_pointer
74
+ end
75
+
76
+ def bool_to_int(value)
77
+ value ? 1 : 0
78
+ end
79
+
80
+ def init_pact
81
+ handle = PactFfi::Verifier.new_for_application("pact-ruby-v2", PactFfi.version)
82
+ set_provider_info(handle)
83
+
84
+ if defined?(@mixed_config.grpc_config) && @mixed_config.grpc_config
85
+ @grpc_server = GrufServer.new(host: "127.0.0.1:#{@mixed_config.grpc_config.grpc_port}", services: @mixed_config.grpc_config.grpc_services)
86
+ @grpc_server.start
87
+ PactFfi::Verifier.add_provider_transport(handle, "grpc", @mixed_config.grpc_config.grpc_port, "", "")
88
+ end
89
+
90
+ if defined?(@mixed_config.async_config) && @mixed_config.async_config
91
+ setup_uri = URI(@mixed_config.async_config.message_setup_url)
92
+ PactFfi::Verifier.add_provider_transport(handle, "message", setup_uri.port, setup_uri.path, "")
93
+ end
94
+
95
+ # todo: add http transport?
96
+
97
+ PactFfi::Verifier.set_provider_state(handle, @pact_config.provider_setup_url, 1, 1)
98
+ PactFfi::Verifier.set_verification_options(handle, 0, 10000)
99
+ # pactffi_verifier_set_publish_options(
100
+ # handle: *mut VerifierHandle,
101
+ # provider_version: *const c_char,
102
+ # build_url: *const c_char,
103
+ # provider_tags: *const *const c_char,
104
+ # provider_tags_len: c_ushort,
105
+ # provider_branch: *const c_char,
106
+ # )
107
+ c_provider_version_tags = create_c_pointer_array_from_string_array(@pact_config.provider_version_tags)
108
+ c_provider_version_tags_size = @pact_config.provider_version_tags.size
109
+ c_consumer_version_tags = create_c_pointer_array_from_string_array(@pact_config.consumer_version_tags)
110
+ c_consumer_version_tags_size = @pact_config.consumer_version_tags.size
111
+
112
+ if @pact_config.provider_build_uri.present?
113
+ begin
114
+ URI.parse(@pact_config.provider_build_uri)
115
+ rescue URI::InvalidURIError
116
+ raise VerifierError.new("provider_build_uri is not a valid URI")
117
+ end
118
+ end
119
+
120
+ if @pact_config.publish_verification_results == true
121
+ if @pact_config.provider_version
122
+ PactFfi::Verifier.set_publish_options(handle, @pact_config.provider_version, @pact_config.provider_build_uri, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch)
123
+ else
124
+ logger.warn("[verifier] - unable to publish verification results as provider version is not set")
125
+ end
126
+ end
127
+
128
+ configure_verification_source(handle, c_provider_version_tags, c_provider_version_tags_size, c_consumer_version_tags, c_consumer_version_tags_size)
129
+
130
+ PactFfi::Verifier.set_no_pacts_is_error(handle, bool_to_int(@pact_config.fail_if_no_pacts_found))
131
+
132
+ add_provider_transport(handle)
133
+
134
+ # the core doesnt pick up these env vars, so we need to set them here
135
+ # https://github.com/pact-foundation/pact-reference/issues/451#issuecomment-2338130587
136
+ # PACT_DESCRIPTION
137
+ # Only validate interactions whose descriptions match this filter (regex format)
138
+ # PACT_PROVIDER_STATE
139
+ # Only validate interactions whose provider states match this filter (regex format)
140
+ # PACT_PROVIDER_NO_STATE
141
+ # Only validate interactions that have no defined provider state (true or false)
142
+ PactFfi::Verifier.set_filter_info(
143
+ handle,
144
+ ENV["PACT_DESCRIPTION"] || nil,
145
+ ENV["PACT_PROVIDER_STATE"] || nil,
146
+ bool_to_int(ENV["PACT_PROVIDER_NO_STATE"] || false)
147
+ )
148
+
149
+ Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level)
150
+
151
+ logger.info("[verifier] verification initialized for provider #{@pact_config.provider_name}, version #{@pact_config.provider_version}, transport #{self.class::PROVIDER_TRANSPORT_TYPE}")
152
+
153
+ handle
154
+ end
155
+
156
+ def set_provider_info(pact_handle)
157
+ # pub extern "C" fn pactffi_verifier_set_provider_info(
158
+ # handle: *mut VerifierHandle,
159
+ # name: *const c_char,
160
+ # scheme: *const c_char,
161
+ # host: *const c_char,
162
+ # port: c_ushort,
163
+ # path: *const c_char,
164
+ # ) {
165
+ PactFfi::Verifier.set_provider_info(pact_handle, @pact_config.provider_name, "", "", 0, "")
166
+ end
167
+
168
+ def add_provider_transport(pact_handle)
169
+ raise Pact::V2::ImplementationRequired, "Implement #add_provider_transport in a subclass"
170
+ end
171
+
172
+ def start_servers!
173
+ logger.info("[verifier] starting services")
174
+
175
+ @servers_started = true
176
+ @pact_config.start_servers
177
+ end
178
+
179
+ def stop_servers
180
+ return unless @servers_started
181
+
182
+ logger.info("[verifier] stopping services")
183
+
184
+ @pact_config.stop_servers
185
+ end
186
+
187
+ def configure_verification_source(handle, c_provider_version_tags, c_provider_version_tags_size, c_consumer_version_tags, c_consumer_version_tags_size)
188
+ logger.info("[verifier] configuring verification source")
189
+ if @pact_config.pact_broker_proxy_url.blank? && @pact_config.pact_uri.blank?
190
+ # todo support non rail apps
191
+ path = @pact_config.pact_dir || (defined?(Rails) ? Rails.root.join("pacts").to_s : "pacts")
192
+ logger.info("[verifier] pact broker url or pact uri is not set, using directory #{path} as a verification source")
193
+ return PactFfi::Verifier.add_directory_source(handle, path)
194
+ end
195
+
196
+ if @pact_config.pact_uri.present?
197
+ if @pact_config.pact_uri.start_with?("http")
198
+ logger.info("[verifier] using pact uri #{@pact_config.pact_uri} as a verification source")
199
+ PactFfi::Verifier.url_source(handle, @pact_config.pact_uri, @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token)
200
+ else
201
+ logger.info("[verifier] using pact file #{@pact_config.pact_uri} as a verification source")
202
+ PactFfi::Verifier.add_file_source(handle, @pact_config.pact_uri)
203
+ end
204
+ else
205
+ logger.info("[verifier] using pact broker url #{@pact_config.broker_url} with consumer selectors: #{JSON.dump(consumer_selectors)} as a verification source")
206
+ consumer_selectors = [] if consumer_selectors.nil?
207
+ filters = consumer_selectors.map do |selector|
208
+ FFI::MemoryPointer.from_string(JSON.dump(selector).to_s)
209
+ end
210
+ filters_ptr = FFI::MemoryPointer.new(:pointer, filters.size + 1)
211
+ filters_ptr.write_array_of_pointer(filters)
212
+ PactFfi::Verifier.broker_source_with_selectors(handle, @pact_config.pact_broker_proxy_url, @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token, bool_to_int(@pact_config.enable_pending), @pact_config.include_wip_pacts_since, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch, filters_ptr, consumer_selectors.size, c_consumer_version_tags, c_consumer_version_tags_size)
213
+ end
214
+ end
215
+
216
+ def consumer_selectors
217
+ (!@pact_config.consumer_version_selectors.empty? && @pact_config.consumer_version_selectors) || @consumer_selectors if @pact_config.consumer_version_selectors
218
+ end
219
+
220
+ def build_consumer_selectors(verify_only, consumer_name, consumer_branch)
221
+ # if verify_only and consumer_name are defined - select only needed consumer
222
+ if verify_only.present?
223
+ # select proper consumer branch if defined
224
+ if consumer_name.present?
225
+ return [] unless verify_only.include?(consumer_name)
226
+ return [{"branch" => consumer_branch, "consumer" => consumer_name}] if consumer_branch.present?
227
+ return [DEFAULT_CONSUMER_SELECTORS.merge("consumer" => consumer_name)]
228
+ end
229
+ # or default selectors
230
+ return verify_only.map { |name| DEFAULT_CONSUMER_SELECTORS.merge("consumer" => name) }
231
+ end
232
+
233
+ # select provided consumer_name
234
+ return [{"branch" => consumer_branch, "consumer" => consumer_name}] if consumer_name.present? && consumer_branch.present?
235
+ return [DEFAULT_CONSUMER_SELECTORS.merge("consumer" => consumer_name)] if consumer_name.present?
236
+
237
+ [DEFAULT_CONSUMER_SELECTORS]
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/verifier"
4
+ require "pact/v2/native/logger"
5
+
6
+ module Pact
7
+ module V2
8
+ module Provider
9
+ class GrpcVerifier < BaseVerifier
10
+ PROVIDER_TRANSPORT_TYPE = "grpc"
11
+
12
+ def initialize(pact_config, mixed_config = nil)
13
+ super
14
+
15
+ raise ArgumentError, "pact_config must be an instance of Pact::V2::Provider::PactConfig::Grpc" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Grpc)
16
+ @grpc_server = GrufServer.new(host: "127.0.0.1:#{@pact_config.grpc_port}", services: @pact_config.grpc_services)
17
+ end
18
+
19
+ private
20
+
21
+ def add_provider_transport(pact_handle)
22
+ PactFfi::Verifier.add_provider_transport(pact_handle, PROVIDER_TRANSPORT_TYPE, @pact_config.grpc_port, "", "")
23
+ end
24
+
25
+
26
+ def start_servers!
27
+ super
28
+ @grpc_server.start
29
+ end
30
+
31
+ def stop_servers
32
+ super
33
+ @grpc_server.stop
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pact
4
+ module V2
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
+ # allow any rack based app to be passed in, otherwise
18
+ # we will load a Rails.application
19
+ # allows for backwards compat with pact-ruby v1
20
+ @app = @options[:app] || nil
21
+ end
22
+
23
+ def start
24
+ raise "server already running, stop server before starting new one" if @thread
25
+
26
+ @logger.info("[webrick] starting server with options: #{@options}")
27
+
28
+ @thread = Thread.new do
29
+ @logger.debug "[webrick] starting http server"
30
+
31
+ # TODO: load from config.ru, if not rails and no app provided?
32
+ # Rack 2/3 compatibility
33
+ begin
34
+ require 'rack/handler/webrick'
35
+ handler = ::Rack::Handler::WEBrick
36
+ rescue LoadError
37
+ require 'rackup/handler/webrick'
38
+ handler = Class.new(Rackup::Handler::WEBrick)
39
+ end
40
+ handler.run(@app || (defined?(Rails) ? Rails.application : nil),
41
+ Host: @options[:host],
42
+ Port: @options[:port],
43
+ Logger: @logger,
44
+ StartCallback: -> { @started = true }) do |server|
45
+ @server = server
46
+ end
47
+ end
48
+ sleep 0.001 until @started
49
+
50
+ @logger.info("[webrick] server started")
51
+ end
52
+
53
+ def stop
54
+ @logger.info("[webrick] stopping server")
55
+
56
+ @server&.shutdown
57
+ @thread&.join(SERVER_STOP_TIMEOUT_SEC)
58
+ @thread&.kill
59
+
60
+ @logger.info("[webrick] server stopped")
61
+ end
62
+
63
+ ##
64
+ # Run the server
65
+ #
66
+ def run
67
+ start
68
+
69
+ yield
70
+ rescue => e
71
+ @logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}")
72
+ raise
73
+ ensure
74
+ stop
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pact/ffi/verifier"
4
+ require "pact/v2/native/logger"
5
+
6
+ module Pact
7
+ module V2
8
+ module Provider
9
+ class HttpVerifier < BaseVerifier
10
+ PROVIDER_TRANSPORT_TYPE = "http"
11
+
12
+ def initialize(pact_config, mixed_config = nil)
13
+ super
14
+
15
+ raise ArgumentError, "pact_config must be an instance of Pact::V2::Provider::PactConfig::Http" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Http)
16
+ @http_server = HttpServer.new(host: "127.0.0.1", port: @pact_config.http_port, app: @pact_config.app)
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
+
31
+ def start_servers!
32
+ super
33
+ @http_server.start
34
+ end
35
+
36
+ def stop_servers
37
+ super
38
+ @http_server.stop
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webrick"
4
+
5
+ module Pact
6
+ module V2
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
+ @message_handlers[description]
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,22 @@
1
+ # # frozen_string_literal: true
2
+ module Pact
3
+ module V2
4
+ module Provider
5
+ # MixedVerifier coordinates verification for all present configs (async, grpc, http)
6
+ class MixedVerifier
7
+ attr_reader :mixed_config, :verifiers
8
+
9
+ def initialize(mixed_config)
10
+ unless mixed_config.is_a?(::Pact::V2::Provider::PactConfig::Mixed)
11
+ raise ArgumentError, "mixed_config must be a PactConfig::Mixed"
12
+ end
13
+ @mixed_config = mixed_config
14
+ @verifiers = []
15
+ @verifiers << AsyncMessageVerifier.new(mixed_config.async_config) if mixed_config.async_config
16
+ @verifiers << GrpcVerifier.new(mixed_config.grpc_config) if mixed_config.grpc_config
17
+ @verifiers << HttpVerifier.new(mixed_config.http_config) if mixed_config.http_config
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack-proxy"
4
+
5
+ module Pact
6
+ module V2
7
+ module Provider
8
+ class PactBrokerProxy < Rack::Proxy
9
+ attr_reader :backend_uri, :path, :logger
10
+
11
+ # e.g. /pacts/provider/paas-stand-seeker/consumer/paas-stand-placer/pact-version/2967a9343bd8fdd28a286c4b8322380020618892/metadata/c1tdW2VdPXByb2R1Y3Rpb24mc1tdW2N2XT03MzIy
12
+ PACT_FILE_REQUEST_PATH_REGEX = %r{/pacts/provider/.+?/consumer/.+?/pact-version/.+}.freeze
13
+
14
+ def initialize(app = nil, opts = {})
15
+ super
16
+ @backend_uri = URI(opts[:backend])
17
+ @path = nil
18
+ @logger = opts[:logger] || Logger.new($stdout)
19
+ end
20
+
21
+ def perform_request(env)
22
+ request = Rack::Request.new(env)
23
+ env["rack.timeout"] ||= ENV.fetch("PACT_BROKER_REQUEST_TIMEOUT", 5).to_i
24
+ @path = request.path
25
+
26
+ super
27
+ end
28
+
29
+ def rewrite_env(env)
30
+ env["HTTP_HOST"] = backend_uri.host
31
+ env
32
+ end
33
+
34
+ def rewrite_response(triplet)
35
+ status, headers, body = triplet
36
+
37
+ if status == "200" && PACT_FILE_REQUEST_PATH_REGEX.match?(path)
38
+ patched_body = patch_response(body.first)
39
+
40
+ # we need to recalculate content length
41
+ headers[Rack::CONTENT_LENGTH] = patched_body.bytesize.to_s
42
+
43
+ return [status, headers, [patched_body]]
44
+ end
45
+
46
+ triplet
47
+ end
48
+
49
+ private
50
+
51
+ def patch_response(raw_body)
52
+ parsed_body = JSON.parse(raw_body)
53
+
54
+ return body if parsed_body["consumer"].blank? || parsed_body["provider"].blank?
55
+ return body if parsed_body["interactions"].blank?
56
+
57
+
58
+ JSON.generate(parsed_body)
59
+ rescue JSON::ParserError => ex
60
+ logger.error("cannot parse broker response: #{ex.message}")
61
+ end
62
+
63
+
64
+ def set_description_prefix(interaction, prefix)
65
+ orig_description = interaction["description"]
66
+ interaction["description"] = "#{prefix} #{orig_description}" unless orig_description.start_with?(prefix)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end