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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1321 -0
- data/LICENSE.txt +23 -0
- data/bin/pact +4 -0
- data/lib/pact/cli/generate_pact_docs.rb +4 -0
- data/lib/pact/cli/run_pact_verification.rb +99 -0
- data/lib/pact/cli/spec_criteria.rb +26 -0
- data/lib/pact/cli.rb +45 -0
- data/lib/pact/consumer/configuration/configuration_extensions.rb +90 -0
- data/lib/pact/consumer/configuration/dsl.rb +11 -0
- data/lib/pact/consumer/configuration/mock_service.rb +112 -0
- data/lib/pact/consumer/configuration/service_consumer.rb +51 -0
- data/lib/pact/consumer/configuration/service_provider.rb +40 -0
- data/lib/pact/consumer/configuration.rb +10 -0
- data/lib/pact/consumer/consumer_contract_builder.rb +82 -0
- data/lib/pact/consumer/consumer_contract_builders.rb +10 -0
- data/lib/pact/consumer/interaction_builder.rb +45 -0
- data/lib/pact/consumer/rspec.rb +35 -0
- data/lib/pact/consumer/spec_hooks.rb +40 -0
- data/lib/pact/consumer/world.rb +37 -0
- data/lib/pact/consumer.rb +7 -0
- data/lib/pact/doc/README.md +13 -0
- data/lib/pact/doc/doc_file.rb +40 -0
- data/lib/pact/doc/generate.rb +11 -0
- data/lib/pact/doc/generator.rb +82 -0
- data/lib/pact/doc/interaction_view_model.rb +124 -0
- data/lib/pact/doc/markdown/consumer_contract_renderer.rb +68 -0
- data/lib/pact/doc/markdown/generator.rb +26 -0
- data/lib/pact/doc/markdown/index_renderer.rb +43 -0
- data/lib/pact/doc/markdown/interaction.erb +14 -0
- data/lib/pact/doc/markdown/interaction_renderer.rb +43 -0
- data/lib/pact/doc/sort_interactions.rb +16 -0
- data/lib/pact/hal/authorization_header_redactor.rb +32 -0
- data/lib/pact/hal/entity.rb +110 -0
- data/lib/pact/hal/http_client.rb +128 -0
- data/lib/pact/hal/link.rb +112 -0
- data/lib/pact/hal/non_json_entity.rb +28 -0
- data/lib/pact/hash_refinements.rb +17 -0
- data/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb +112 -0
- data/lib/pact/pact_broker/fetch_pacts.rb +103 -0
- data/lib/pact/pact_broker/notices.rb +34 -0
- data/lib/pact/pact_broker/pact_selection_description.rb +66 -0
- data/lib/pact/pact_broker.rb +25 -0
- data/lib/pact/project_root.rb +7 -0
- data/lib/pact/provider/configuration/configuration_extension.rb +69 -0
- data/lib/pact/provider/configuration/dsl.rb +18 -0
- data/lib/pact/provider/configuration/message_provider_dsl.rb +63 -0
- data/lib/pact/provider/configuration/pact_verification.rb +48 -0
- data/lib/pact/provider/configuration/pact_verification_from_broker.rb +126 -0
- data/lib/pact/provider/configuration/service_provider_config.rb +32 -0
- data/lib/pact/provider/configuration/service_provider_dsl.rb +107 -0
- data/lib/pact/provider/configuration.rb +7 -0
- data/lib/pact/provider/context.rb +0 -0
- data/lib/pact/provider/help/console_text.rb +76 -0
- data/lib/pact/provider/help/content.rb +38 -0
- data/lib/pact/provider/help/pact_diff.rb +43 -0
- data/lib/pact/provider/help/prompt_text.rb +49 -0
- data/lib/pact/provider/help/write.rb +56 -0
- data/lib/pact/provider/matchers/messages.rb +66 -0
- data/lib/pact/provider/pact_helper_locator.rb +24 -0
- data/lib/pact/provider/pact_source.rb +40 -0
- data/lib/pact/provider/pact_spec_runner.rb +188 -0
- data/lib/pact/provider/pact_uri.rb +55 -0
- data/lib/pact/provider/pact_verification.rb +17 -0
- data/lib/pact/provider/print_missing_provider_states.rb +35 -0
- data/lib/pact/provider/request.rb +77 -0
- data/lib/pact/provider/rspec/backtrace_formatter.rb +43 -0
- data/lib/pact/provider/rspec/calculate_exit_code.rb +18 -0
- data/lib/pact/provider/rspec/custom_options_file +0 -0
- data/lib/pact/provider/rspec/formatter_rspec_2.rb +76 -0
- data/lib/pact/provider/rspec/formatter_rspec_3.rb +195 -0
- data/lib/pact/provider/rspec/json_formatter.rb +100 -0
- data/lib/pact/provider/rspec/matchers.rb +80 -0
- data/lib/pact/provider/rspec/pact_broker_formatter.rb +76 -0
- data/lib/pact/provider/rspec.rb +234 -0
- data/lib/pact/provider/state/provider_state.rb +180 -0
- data/lib/pact/provider/state/provider_state_configured_modules.rb +15 -0
- data/lib/pact/provider/state/provider_state_manager.rb +42 -0
- data/lib/pact/provider/state/provider_state_proxy.rb +39 -0
- data/lib/pact/provider/state/set_up.rb +13 -0
- data/lib/pact/provider/state/tear_down.rb +13 -0
- data/lib/pact/provider/test_methods.rb +77 -0
- data/lib/pact/provider/verification_report.rb +36 -0
- data/lib/pact/provider/verification_results/create.rb +88 -0
- data/lib/pact/provider/verification_results/publish.rb +143 -0
- data/lib/pact/provider/verification_results/publish_all.rb +50 -0
- data/lib/pact/provider/verification_results/verification_result.rb +40 -0
- data/lib/pact/provider/world.rb +50 -0
- data/lib/pact/provider.rb +3 -0
- data/lib/pact/retry.rb +37 -0
- data/lib/pact/tasks/task_helper.rb +62 -0
- data/lib/pact/tasks/verification_task.rb +95 -0
- data/lib/pact/tasks.rb +2 -0
- data/lib/pact/templates/help.erb +22 -0
- data/lib/pact/templates/provider_state.erb +14 -0
- data/lib/pact/utils/metrics.rb +100 -0
- data/lib/pact/utils/string.rb +35 -0
- data/lib/pact/v2/configuration.rb +23 -0
- data/lib/pact/v2/consumer/grpc_interaction_builder.rb +187 -0
- data/lib/pact/v2/consumer/http_interaction_builder.rb +163 -0
- data/lib/pact/v2/consumer/interaction_contents.rb +54 -0
- data/lib/pact/v2/consumer/message_interaction_builder.rb +280 -0
- data/lib/pact/v2/consumer/mock_server.rb +99 -0
- data/lib/pact/v2/consumer/pact_config/base.rb +24 -0
- data/lib/pact/v2/consumer/pact_config/grpc.rb +26 -0
- data/lib/pact/v2/consumer/pact_config/http.rb +55 -0
- data/lib/pact/v2/consumer/pact_config/message.rb +17 -0
- data/lib/pact/v2/consumer/pact_config.rb +24 -0
- data/lib/pact/v2/consumer.rb +8 -0
- data/lib/pact/v2/matchers/base.rb +67 -0
- data/lib/pact/v2/matchers/v1/equality.rb +19 -0
- data/lib/pact/v2/matchers/v2/regex.rb +19 -0
- data/lib/pact/v2/matchers/v2/type.rb +17 -0
- data/lib/pact/v2/matchers/v3/boolean.rb +17 -0
- data/lib/pact/v2/matchers/v3/date.rb +18 -0
- data/lib/pact/v2/matchers/v3/date_time.rb +18 -0
- data/lib/pact/v2/matchers/v3/decimal.rb +17 -0
- data/lib/pact/v2/matchers/v3/each.rb +42 -0
- data/lib/pact/v2/matchers/v3/include.rb +17 -0
- data/lib/pact/v2/matchers/v3/integer.rb +17 -0
- data/lib/pact/v2/matchers/v3/number.rb +17 -0
- data/lib/pact/v2/matchers/v3/time.rb +18 -0
- data/lib/pact/v2/matchers/v4/each_key.rb +26 -0
- data/lib/pact/v2/matchers/v4/each_key_value.rb +32 -0
- data/lib/pact/v2/matchers/v4/each_value.rb +33 -0
- data/lib/pact/v2/matchers/v4/not_empty.rb +17 -0
- data/lib/pact/v2/matchers.rb +94 -0
- data/lib/pact/v2/native/blocking_verifier.rb +17 -0
- data/lib/pact/v2/native/logger.rb +25 -0
- data/lib/pact/v2/provider/async_message_verifier.rb +28 -0
- data/lib/pact/v2/provider/base_verifier.rb +242 -0
- data/lib/pact/v2/provider/grpc_verifier.rb +38 -0
- data/lib/pact/v2/provider/gruf_server.rb +75 -0
- data/lib/pact/v2/provider/http_server.rb +79 -0
- data/lib/pact/v2/provider/http_verifier.rb +43 -0
- data/lib/pact/v2/provider/message_provider_servlet.rb +79 -0
- data/lib/pact/v2/provider/mixed_verifier.rb +22 -0
- data/lib/pact/v2/provider/pact_broker_proxy.rb +71 -0
- data/lib/pact/v2/provider/pact_broker_proxy_runner.rb +77 -0
- data/lib/pact/v2/provider/pact_config/async.rb +29 -0
- data/lib/pact/v2/provider/pact_config/base.rb +101 -0
- data/lib/pact/v2/provider/pact_config/grpc.rb +26 -0
- data/lib/pact/v2/provider/pact_config/http.rb +27 -0
- data/lib/pact/v2/provider/pact_config/mixed.rb +39 -0
- data/lib/pact/v2/provider/pact_config.rb +26 -0
- data/lib/pact/v2/provider/provider_server_runner.rb +89 -0
- data/lib/pact/v2/provider/provider_state_configuration.rb +32 -0
- data/lib/pact/v2/provider/provider_state_servlet.rb +86 -0
- data/lib/pact/v2/provider.rb +8 -0
- data/lib/pact/v2/railtie.rb +13 -0
- data/lib/pact/v2/rspec/support/pact_consumer_helpers.rb +80 -0
- data/lib/pact/v2/rspec/support/pact_message_helpers.rb +42 -0
- data/lib/pact/v2/rspec/support/pact_provider_helpers.rb +129 -0
- data/lib/pact/v2/rspec/support/waterdrop/pact_waterdrop_client.rb +27 -0
- data/lib/pact/v2/rspec/support/webmock/webmock_helpers.rb +30 -0
- data/lib/pact/v2/rspec.rb +17 -0
- data/lib/pact/v2/tasks/pact.rake +13 -0
- data/lib/pact/v2/version.rb +8 -0
- data/lib/pact/v2.rb +71 -0
- data/lib/pact/version.rb +4 -0
- data/lib/pact.rb +13 -0
- data/lib/tasks/pact.rake +34 -0
- data/pact.gemspec +106 -0
- metadata +529 -0
@@ -0,0 +1,280 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/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 MessageInteractionBuilder
|
11
|
+
META_CONTENT_TYPE_HEADER = "contentType"
|
12
|
+
|
13
|
+
JSON_CONTENT_TYPE = "application/json"
|
14
|
+
PROTO_CONTENT_TYPE = "application/protobuf"
|
15
|
+
|
16
|
+
PROTOBUF_PLUGIN_NAME = "protobuf"
|
17
|
+
PROTOBUF_PLUGIN_VERSION = "0.6.5"
|
18
|
+
|
19
|
+
# https://docs.rs/pact_ffi/latest/pact_ffi/mock_server/handles/fn.pactffi_write_message_pact_file.html
|
20
|
+
WRITE_PACT_FILE_ERRORS = {
|
21
|
+
1 => {reason: :file_not_accessible, status: 1, description: "The pact file was not able to be written"},
|
22
|
+
2 => {reason: :internal_error, status: 2, description: "The message pact for the given handle was not found"}
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
class PluginInitError < Pact::V2::FfiError; end
|
26
|
+
|
27
|
+
# https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html
|
28
|
+
INIT_PLUGIN_ERRORS = {
|
29
|
+
1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
|
30
|
+
2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"},
|
31
|
+
3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"}
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
# https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html
|
35
|
+
CREATE_INTERACTION_ERRORS = {
|
36
|
+
1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
|
37
|
+
2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"},
|
38
|
+
3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"},
|
39
|
+
4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"},
|
40
|
+
5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"},
|
41
|
+
6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"}
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
class CreateInteractionError < Pact::V2::FfiError; end
|
45
|
+
|
46
|
+
class InteractionMismatchesError < Pact::V2::Error; end
|
47
|
+
|
48
|
+
class InteractionBuilderError < Pact::V2::Error; end
|
49
|
+
|
50
|
+
def initialize(pact_config, description: nil)
|
51
|
+
@pact_config = pact_config
|
52
|
+
@description = description
|
53
|
+
|
54
|
+
@json_contents = nil
|
55
|
+
@proto_contents = nil
|
56
|
+
@proto_path = nil
|
57
|
+
@proto_message_class = nil
|
58
|
+
@proto_include_dirs = []
|
59
|
+
@meta = {}
|
60
|
+
@headers = {}
|
61
|
+
@provider_state_meta = nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def given(provider_state, metadata = {})
|
65
|
+
@provider_state_meta = {provider_state => metadata}
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def upon_receiving(description)
|
70
|
+
@description = description
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def with_json_contents(contents_hash)
|
75
|
+
@json_contents = InteractionContents.basic(contents_hash)
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
def with_proto_class(proto_path, message_class_name, include_dirs = [])
|
80
|
+
absolute_path = File.expand_path(proto_path)
|
81
|
+
raise InteractionBuilderError.new("proto file #{proto_path} does not exist") unless File.exist?(absolute_path)
|
82
|
+
|
83
|
+
@proto_path = absolute_path
|
84
|
+
@proto_message_class = message_class_name
|
85
|
+
@proto_include_dirs = include_dirs.map { |dir| File.expand_path(dir) }
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def with_proto_contents(contents_hash)
|
90
|
+
@proto_contents = InteractionContents.plugin(contents_hash)
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def with_metadata(meta_hash)
|
95
|
+
@meta = InteractionContents.basic(meta_hash)
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
def with_headers(headers_hash)
|
100
|
+
@headers = InteractionContents.basic(headers_hash)
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def with_header(key, value)
|
105
|
+
@headers[key] = value
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
def validate!
|
110
|
+
if proto_interaction?
|
111
|
+
raise InteractionBuilderError.new("proto_path / proto_message are not defined, please set ones with #with_proto_message") if @proto_contents.blank? || @proto_message_class.blank?
|
112
|
+
raise InteractionBuilderError.new("invalid request format, should be a hash") unless @proto_contents.is_a?(Hash)
|
113
|
+
else
|
114
|
+
raise InteractionBuilderError.new("invalid request format, should be a hash") unless @json_contents.is_a?(Hash)
|
115
|
+
end
|
116
|
+
raise InteractionBuilderError.new("description is required for message interactions, please set one with #upon_receiving") if @description.blank?
|
117
|
+
end
|
118
|
+
|
119
|
+
def execute(&block)
|
120
|
+
raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used)
|
121
|
+
|
122
|
+
validate!
|
123
|
+
pact_handle = init_pact
|
124
|
+
init_plugin!(pact_handle) if proto_interaction?
|
125
|
+
|
126
|
+
message_pact = PactFfi::MessageConsumer.new_message_interaction(pact_handle, @description)
|
127
|
+
|
128
|
+
configure_interaction!(message_pact)
|
129
|
+
|
130
|
+
# strip out matchers and get raw payload/metadata
|
131
|
+
payload, metadata = fetch_reified_message(pact_handle)
|
132
|
+
configure_provider_state(message_pact, metadata)
|
133
|
+
|
134
|
+
yield(payload, metadata)
|
135
|
+
|
136
|
+
write_pacts!(pact_handle, @pact_config.pact_dir)
|
137
|
+
ensure
|
138
|
+
@used = true
|
139
|
+
PactFfi::MessageConsumer.free_handle(message_pact)
|
140
|
+
PactFfi.free_pact_handle(pact_handle)
|
141
|
+
end
|
142
|
+
|
143
|
+
def build_interaction_json
|
144
|
+
return JSON.dump(@json_contents) unless proto_interaction?
|
145
|
+
|
146
|
+
contents = {
|
147
|
+
"pact:proto": @proto_path,
|
148
|
+
"pact:message-type": @proto_message_class,
|
149
|
+
"pact:content-type": PROTO_CONTENT_TYPE
|
150
|
+
}.merge(@proto_contents)
|
151
|
+
|
152
|
+
contents["pact:protobuf-config"] = {additionalIncludes: @proto_include_dirs} if @proto_include_dirs.present?
|
153
|
+
|
154
|
+
JSON.dump(contents)
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def write_pacts!(handle, dir)
|
160
|
+
result = PactFfi.write_message_pact_file(handle, @pact_config.pact_dir, false)
|
161
|
+
return result if WRITE_PACT_FILE_ERRORS[result].blank?
|
162
|
+
|
163
|
+
error = WRITE_PACT_FILE_ERRORS[result]
|
164
|
+
raise WritePactsError.new("There was an error while trying to write pact file to #{dir}", error[:reason], error[:status])
|
165
|
+
end
|
166
|
+
|
167
|
+
def init_pact
|
168
|
+
handle = PactFfi::MessageConsumer.new_message_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 fetch_reified_message(pact_handle)
|
178
|
+
iterator = PactFfi::MessageConsumer.pact_handle_get_message_iter(pact_handle)
|
179
|
+
raise InteractionBuilderError.new("cannot get message iterator: internal error") if iterator.blank?
|
180
|
+
|
181
|
+
message_handle = PactFfi.pact_message_iter_next(iterator)
|
182
|
+
raise InteractionBuilderError.new("cannot get message from iterator: no messages") if message_handle.blank?
|
183
|
+
|
184
|
+
contents = fetch_reified_message_body(message_handle)
|
185
|
+
meta = fetch_reified_message_headers(message_handle)
|
186
|
+
|
187
|
+
[contents, meta.compact]
|
188
|
+
ensure
|
189
|
+
PactFfi.pact_message_iter_delete(iterator) if iterator.present?
|
190
|
+
end
|
191
|
+
|
192
|
+
def fetch_reified_message_headers(message_handle)
|
193
|
+
meta = {"headers" => {}}
|
194
|
+
|
195
|
+
meta[META_CONTENT_TYPE_HEADER] = PactFfi.message_find_metadata(message_handle, META_CONTENT_TYPE_HEADER)
|
196
|
+
|
197
|
+
@meta.each_key do |key|
|
198
|
+
meta[key.to_s] = PactFfi.message_find_metadata(message_handle, key.to_s)
|
199
|
+
end
|
200
|
+
|
201
|
+
@headers.each_key do |key|
|
202
|
+
meta["headers"][key.to_s] = PactFfi.message_find_metadata(message_handle, key.to_s)
|
203
|
+
end
|
204
|
+
|
205
|
+
meta
|
206
|
+
end
|
207
|
+
|
208
|
+
def configure_provider_state(message_pact, reified_metadata)
|
209
|
+
content_type = reified_metadata[META_CONTENT_TYPE_HEADER]
|
210
|
+
@provider_state_meta&.each_pair do |provider_state, meta|
|
211
|
+
if meta.present?
|
212
|
+
meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) }
|
213
|
+
PactFfi.given_with_param(message_pact, provider_state, META_CONTENT_TYPE_HEADER, content_type.to_s) if content_type
|
214
|
+
elsif content_type.present?
|
215
|
+
PactFfi.given_with_param(message_pact, provider_state, META_CONTENT_TYPE_HEADER, content_type.to_s)
|
216
|
+
else
|
217
|
+
PactFfi.given(message_pact, provider_state)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def fetch_reified_message_body(message_handle)
|
223
|
+
if proto_interaction?
|
224
|
+
len = PactFfi::MessageConsumer.get_contents_length(message_handle)
|
225
|
+
ptr = PactFfi::MessageConsumer.get_contents_bin(message_handle)
|
226
|
+
return nil if ptr.blank? || len == 0
|
227
|
+
|
228
|
+
return String.new(ptr.read_string_length(len))
|
229
|
+
end
|
230
|
+
|
231
|
+
contents = PactFfi::MessageConsumer.get_contents(message_handle)
|
232
|
+
return nil if contents.blank?
|
233
|
+
|
234
|
+
JSON.parse(contents)
|
235
|
+
end
|
236
|
+
|
237
|
+
def configure_interaction!(message_pact)
|
238
|
+
interaction_json = build_interaction_json
|
239
|
+
|
240
|
+
if proto_interaction?
|
241
|
+
result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, PROTO_CONTENT_TYPE, interaction_json)
|
242
|
+
if CREATE_INTERACTION_ERRORS[result].present?
|
243
|
+
error = CREATE_INTERACTION_ERRORS[result]
|
244
|
+
raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status])
|
245
|
+
end
|
246
|
+
else
|
247
|
+
result = PactFfi.with_body(message_pact, 0, JSON_CONTENT_TYPE, interaction_json)
|
248
|
+
unless result
|
249
|
+
raise InteractionMismatchesError.new("There was an error while trying to add message interaction contents \"#{@description}\"")
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# meta should be configured last to avoid resetting after body is set
|
254
|
+
InteractionContents.basic(@meta.merge(@headers)).each_pair do |key, value|
|
255
|
+
PactFfi::MessageConsumer.with_metadata_v2(message_pact, key.to_s, JSON.dump(value))
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def init_plugin!(pact_handle)
|
260
|
+
result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, PROTOBUF_PLUGIN_VERSION)
|
261
|
+
return result if INIT_PLUGIN_ERRORS[result].blank?
|
262
|
+
|
263
|
+
error = INIT_PLUGIN_ERRORS[result]
|
264
|
+
raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status])
|
265
|
+
end
|
266
|
+
|
267
|
+
def serialize_metadata(metadata_hash)
|
268
|
+
metadata = metadata_hash.deep_dup
|
269
|
+
serialize_as!(metadata, :basic)
|
270
|
+
|
271
|
+
metadata
|
272
|
+
end
|
273
|
+
|
274
|
+
def proto_interaction?
|
275
|
+
@proto_contents.present?
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pact/ffi/mock_server"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module V2
|
7
|
+
module Consumer
|
8
|
+
class MockServer
|
9
|
+
attr_reader :host, :port, :transport, :handle, :url
|
10
|
+
|
11
|
+
TRANSPORT_HTTP = "http"
|
12
|
+
TRANSPORT_GRPC = "grpc"
|
13
|
+
|
14
|
+
TRANSPORTS = [TRANSPORT_HTTP, TRANSPORT_GRPC].freeze
|
15
|
+
|
16
|
+
class MockServerCreateError < Pact::V2::FfiError; end
|
17
|
+
|
18
|
+
class WritePactsError < Pact::V2::FfiError; end
|
19
|
+
|
20
|
+
# https://docs.rs/pact_ffi/0.4.17/pact_ffi/mock_server/fn.pactffi_create_mock_server_for_transport.html
|
21
|
+
CREATE_TRANSPORT_ERRORS = {
|
22
|
+
-1 => {reason: :invalid_handle, status: -1, description: "An invalid handle was received. Handles should be created with pactffi_new_pact"},
|
23
|
+
-2 => {reason: :invalid_transport_json, status: -2, description: "Transport_config is not valid JSON"},
|
24
|
+
-3 => {reason: :mock_server_not_started, status: -3, description: "The mock server could not be started"},
|
25
|
+
-4 => {reason: :internal_error, status: -4, description: "The method panicked"},
|
26
|
+
-5 => {reason: :invalid_host, status: -5, description: "The address is not valid"}
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
# https://docs.rs/pact_ffi/0.4.17/pact_ffi/mock_server/fn.pactffi_write_pact_file.html
|
30
|
+
WRITE_PACT_FILE_ERRORS = {
|
31
|
+
1 => {reason: :internal_error, status: 1, description: "A general panic was caught"},
|
32
|
+
2 => {reason: :file_not_accessible, status: 2, description: "The pact file was not able to be written"},
|
33
|
+
3 => {reason: :mock_server_not_found, status: 3, description: "A mock server with the provided port was not found"}
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
def self.create_for_grpc!(pact:, host: "127.0.0.1", port: 0)
|
37
|
+
new(pact: pact, transport: TRANSPORT_GRPC, host: host, port: port)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.create_for_http!(pact:, host: "127.0.0.1", port: 0)
|
41
|
+
new(pact: pact, transport: TRANSPORT_HTTP, host: host, port: port)
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(pact:, transport:, host:, port:)
|
45
|
+
raise "Transport #{transport} is not supported yet, available transports are: #{TRANSPORTS.join(",")}" unless TRANSPORTS.include?(transport)
|
46
|
+
|
47
|
+
@pact = pact
|
48
|
+
@transport = transport
|
49
|
+
@host = host
|
50
|
+
@port = port
|
51
|
+
|
52
|
+
@handle = init_transport!
|
53
|
+
# the returned handle is the port number
|
54
|
+
# we set it here, so we can consume a port number of 0
|
55
|
+
# and allow pact to assign a random available port
|
56
|
+
@port = @handle
|
57
|
+
# construct the url for the mock server
|
58
|
+
# as a convenience for the user
|
59
|
+
@url = "#{transport}://#{host}:#{@handle}"
|
60
|
+
# TODO: handle auto-GC of native memory
|
61
|
+
# ObjectSpace.define_finalizer(self, proc do
|
62
|
+
# cleanup
|
63
|
+
# end)
|
64
|
+
end
|
65
|
+
|
66
|
+
def write_pacts!(dir)
|
67
|
+
result = PactFfi::MockServer.write_pact_file(@handle, dir, false)
|
68
|
+
return result if WRITE_PACT_FILE_ERRORS[result].blank?
|
69
|
+
|
70
|
+
error = WRITE_PACT_FILE_ERRORS[result]
|
71
|
+
raise WritePactsError.new("There was an error while trying to write pact file to #{dir}", error[:reason], error[:status])
|
72
|
+
end
|
73
|
+
|
74
|
+
def matched?
|
75
|
+
PactFfi::MockServer.matched(@handle)
|
76
|
+
end
|
77
|
+
|
78
|
+
def mismatches
|
79
|
+
PactFfi::MockServer.mismatches(@handle)
|
80
|
+
end
|
81
|
+
|
82
|
+
def cleanup
|
83
|
+
PactFfi::MockServer.cleanup(@handle)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def init_transport!
|
89
|
+
handle = PactFfi::MockServer.create_for_transport(@pact, @host, @port, @transport, nil)
|
90
|
+
# the returned handle is the port number
|
91
|
+
return handle if CREATE_TRANSPORT_ERRORS[handle].blank?
|
92
|
+
|
93
|
+
error = CREATE_TRANSPORT_ERRORS[handle]
|
94
|
+
raise MockServerCreateError.new("There was an error while trying to create mock server for transport:#{@transport}", error[:reason], error[:status])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module V2
|
5
|
+
module Consumer
|
6
|
+
module PactConfig
|
7
|
+
class Base
|
8
|
+
attr_reader :consumer_name, :provider_name, :pact_dir, :log_level
|
9
|
+
|
10
|
+
def initialize(consumer_name:, provider_name:, opts: {})
|
11
|
+
@consumer_name = consumer_name
|
12
|
+
@provider_name = provider_name
|
13
|
+
@pact_dir = opts[:pact_dir] || (defined?(Rails) ? Rails.root.join("../pacts").to_s : "pacts")
|
14
|
+
@log_level = opts[:log_level] || :info
|
15
|
+
end
|
16
|
+
|
17
|
+
def new_interaction(description = nil)
|
18
|
+
raise Pact::V2::ImplementationRequired, "#new_interaction should be implemented"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module V2
|
7
|
+
module Consumer
|
8
|
+
module PactConfig
|
9
|
+
class Grpc < Base
|
10
|
+
attr_reader :mock_host, :mock_port
|
11
|
+
|
12
|
+
def initialize(consumer_name:, provider_name:, opts: {})
|
13
|
+
super
|
14
|
+
|
15
|
+
@mock_host = opts[:mock_host] || "127.0.0.1"
|
16
|
+
@mock_port = opts[:mock_port] || 3009
|
17
|
+
end
|
18
|
+
|
19
|
+
def new_interaction(description = nil)
|
20
|
+
GrpcInteractionBuilder.new(self, description: description)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module V2
|
7
|
+
module Consumer
|
8
|
+
module PactConfig
|
9
|
+
class Http < Base
|
10
|
+
attr_reader :mock_host, :mock_port, :pact_handle
|
11
|
+
|
12
|
+
def initialize(consumer_name:, provider_name:, opts: {})
|
13
|
+
super
|
14
|
+
|
15
|
+
@mock_host = opts[:mock_host] || "127.0.0.1"
|
16
|
+
@mock_port = opts[:mock_port] || 0
|
17
|
+
@log_level = opts[:log_level] || :info
|
18
|
+
@pact_specification = get_pact_specification(opts)
|
19
|
+
@pact_handle = init_pact
|
20
|
+
end
|
21
|
+
|
22
|
+
def new_interaction(description = nil)
|
23
|
+
HttpInteractionBuilder.new(self, description: description)
|
24
|
+
end
|
25
|
+
|
26
|
+
def reset_pact
|
27
|
+
@pact_handle = init_pact
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_pact_specification(opts)
|
31
|
+
pact_spec_version = opts[:pact_specification] || "V4"
|
32
|
+
unless pact_spec_version.match?(/^v?[1-4](\.\d+){0,2}$/i)
|
33
|
+
raise ArgumentError, "Invalid pact specification version format \n Valid versions are 1, 1.1, 2, 3, 4. Default is V4 \n V prefix is optional, and case insensitive"
|
34
|
+
end
|
35
|
+
pact_spec_version = pact_spec_version.upcase
|
36
|
+
pact_spec_version = "V#{pact_spec_version}" unless pact_spec_version.start_with?("V")
|
37
|
+
pact_spec_version = pact_spec_version.sub(/(\.0+)+$/, "")
|
38
|
+
pact_spec_version = pact_spec_version.tr(".", "_")
|
39
|
+
PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_#{pact_spec_version.upcase}"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def init_pact
|
43
|
+
handle = PactFfi.new_pact(consumer_name, provider_name)
|
44
|
+
PactFfi.with_specification(handle, @pact_specification)
|
45
|
+
PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version)
|
46
|
+
|
47
|
+
Pact::V2::Native::Logger.log_to_stdout(@log_level)
|
48
|
+
|
49
|
+
handle
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module V2
|
7
|
+
module Consumer
|
8
|
+
module PactConfig
|
9
|
+
class Message < Base
|
10
|
+
def new_interaction(description = nil)
|
11
|
+
MessageInteractionBuilder.new(self, description: description)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "pact_config/grpc"
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module V2
|
7
|
+
module Consumer
|
8
|
+
module PactConfig
|
9
|
+
def self.new(transport_type, consumer_name:, provider_name:, opts: {})
|
10
|
+
case transport_type
|
11
|
+
when :http
|
12
|
+
Http.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
|
13
|
+
when :grpc
|
14
|
+
Grpc.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
|
15
|
+
when :message
|
16
|
+
Message.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts)
|
17
|
+
else
|
18
|
+
raise ArgumentError, "unknown transport_type: #{transport_type}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module V2
|
5
|
+
module Matchers
|
6
|
+
# see https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md
|
7
|
+
class Base
|
8
|
+
attr_reader :spec_version, :kind, :template, :opts
|
9
|
+
|
10
|
+
class MatcherInitializationError < Pact::V2::Error; end
|
11
|
+
|
12
|
+
def initialize(spec_version:, kind:, template:, opts: {})
|
13
|
+
@spec_version = spec_version
|
14
|
+
@kind = kind
|
15
|
+
@template = template
|
16
|
+
@opts = opts
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_basic
|
20
|
+
{
|
21
|
+
"pact:matcher:type" => serialize!(@kind.deep_dup, :basic),
|
22
|
+
"value" => serialize!(@template.deep_dup, :basic)
|
23
|
+
}.merge(serialize!(@opts.deep_dup, :basic))
|
24
|
+
end
|
25
|
+
|
26
|
+
def as_plugin
|
27
|
+
params = @opts.values.map { |v| format_primitive(v) }.join(",")
|
28
|
+
value = format_primitive(@template)
|
29
|
+
|
30
|
+
return "matching(#{@kind}, #{params}, #{value})" if params.present?
|
31
|
+
|
32
|
+
"matching(#{@kind}, #{value})"
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def serialize!(data, format)
|
38
|
+
# serialize complex types recursively
|
39
|
+
case data
|
40
|
+
when TrueClass, FalseClass, Numeric, String
|
41
|
+
data
|
42
|
+
when Array
|
43
|
+
data.map { |v| serialize!(v, format) }
|
44
|
+
when Hash
|
45
|
+
data.transform_values { |v| serialize!(v, format) }
|
46
|
+
when Pact::V2::Matchers::Base
|
47
|
+
return data.as_basic if format == :basic
|
48
|
+
data.as_plugin if format == :plugin
|
49
|
+
else
|
50
|
+
data
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def format_primitive(arg)
|
55
|
+
case arg
|
56
|
+
when TrueClass, FalseClass, Numeric
|
57
|
+
arg.to_s
|
58
|
+
when String
|
59
|
+
"'#{arg}'"
|
60
|
+
else
|
61
|
+
raise "#{arg.class} is not a primitive"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module V2
|
5
|
+
module Matchers
|
6
|
+
module V1
|
7
|
+
class Equality < Pact::V2::Matchers::Base
|
8
|
+
def initialize(template)
|
9
|
+
super(spec_version: Pact::V2::Matchers::PACT_SPEC_V1, kind: "equality", template: template)
|
10
|
+
end
|
11
|
+
|
12
|
+
def as_plugin
|
13
|
+
"matching(equalTo, #{format_primitive(@template)})"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module V2
|
5
|
+
module Matchers
|
6
|
+
module V2
|
7
|
+
class Regex < Pact::V2::Matchers::Base
|
8
|
+
def initialize(regex, template)
|
9
|
+
raise MatcherInitializationError, "#{self.class}: #{regex} should be an instance of Regexp" unless regex.is_a?(Regexp)
|
10
|
+
raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of String or Array" unless template.is_a?(String) || template.is_a?(Array)
|
11
|
+
raise MatcherInitializationError, "#{self.class}: #{template} array values should be strings" if template.is_a?(Array) && !template.all?(String)
|
12
|
+
|
13
|
+
super(spec_version: Pact::V2::Matchers::PACT_SPEC_V2, kind: "regex", template: template, opts: {regex: regex.to_s})
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module V2
|
5
|
+
module Matchers
|
6
|
+
module V2
|
7
|
+
class Type < Pact::V2::Matchers::Base
|
8
|
+
def initialize(template)
|
9
|
+
raise MatcherInitializationError, "#{self.class}: template is not a primitive" unless template.is_a?(TrueClass) || template.is_a?(FalseClass) || template.is_a?(Numeric) || template.is_a?(String) || template.is_a?(Array) || template.is_a?(Hash)
|
10
|
+
|
11
|
+
super(spec_version: Pact::V2::Matchers::PACT_SPEC_V2, kind: "type", template: template)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module V2
|
5
|
+
module Matchers
|
6
|
+
module V3
|
7
|
+
class Boolean < Pact::V2::Matchers::Base
|
8
|
+
def initialize(template)
|
9
|
+
raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of Boolean" unless template.is_a?(TrueClass) || template.is_a?(FalseClass)
|
10
|
+
|
11
|
+
super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "boolean", template: template)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|