|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require "pact/ffi/async_message_pact" |
| 4 | +require "pact/ffi/plugin_consumer" |
| 5 | +require "pact/ffi/logger" |
| 6 | + |
| 7 | +module Pact |
| 8 | + module V2 |
| 9 | + module Consumer |
| 10 | + class PluginAsyncMessageInteractionBuilder |
| 11 | + |
| 12 | + class PluginInitError < Pact::V2::FfiError; end |
| 13 | + |
| 14 | + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html |
| 15 | + INIT_PLUGIN_ERRORS = { |
| 16 | + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, |
| 17 | + 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, |
| 18 | + 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} |
| 19 | + }.freeze |
| 20 | + |
| 21 | + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html |
| 22 | + CREATE_INTERACTION_ERRORS = { |
| 23 | + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, |
| 24 | + 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, |
| 25 | + 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, |
| 26 | + 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, |
| 27 | + 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, |
| 28 | + 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} |
| 29 | + }.freeze |
| 30 | + |
| 31 | + class CreateInteractionError < Pact::V2::FfiError; end |
| 32 | + |
| 33 | + class InteractionMismatchesError < Pact::V2::Error; end |
| 34 | + |
| 35 | + class InteractionBuilderError < Pact::V2::Error; end |
| 36 | + |
| 37 | + def initialize(pact_config, description: nil) |
| 38 | + @pact_config = pact_config |
| 39 | + @description = description || "" |
| 40 | + @contents = nil |
| 41 | + @provider_state_meta = nil |
| 42 | + end |
| 43 | + |
| 44 | + def with_plugin(plugin_name, plugin_version) |
| 45 | + raise InteractionBuilderError.new("plugin_name is required") if plugin_name.blank? |
| 46 | + raise InteractionBuilderError.new("plugin_version is required") if plugin_version.blank? |
| 47 | + |
| 48 | + @plugin_name = plugin_name |
| 49 | + @plugin_version = plugin_version |
| 50 | + self |
| 51 | + end |
| 52 | + |
| 53 | + def given(provider_state, metadata = {}) |
| 54 | + @provider_state_meta = {provider_state => metadata} |
| 55 | + self |
| 56 | + end |
| 57 | + |
| 58 | + def upon_receiving(description) |
| 59 | + @description = description |
| 60 | + self |
| 61 | + end |
| 62 | + |
| 63 | + def with_contents(contents_hash) |
| 64 | + @contents = InteractionContents.plugin(contents_hash) |
| 65 | + self |
| 66 | + end |
| 67 | + |
| 68 | + def with_content_type(content_type) |
| 69 | + @interaction_content_type = content_type || @content_type |
| 70 | + self |
| 71 | + end |
| 72 | + |
| 73 | + def with_plugin_metadata(meta_hash) |
| 74 | + @plugin_metadata = meta_hash |
| 75 | + self |
| 76 | + end |
| 77 | + |
| 78 | + def with_transport(transport) |
| 79 | + @transport = transport |
| 80 | + self |
| 81 | + end |
| 82 | + |
| 83 | + def interaction_json |
| 84 | + result = { |
| 85 | + contents: @contents |
| 86 | + } |
| 87 | + result.merge!(@plugin_metadata) if @plugin_metadata.is_a?(Hash) |
| 88 | + JSON.dump(result) |
| 89 | + end |
| 90 | + |
| 91 | + def validate! |
| 92 | + raise InteractionBuilderError.new("invalid contents format, should be a hash") unless @contents.is_a?(Hash) |
| 93 | + end |
| 94 | + |
| 95 | + def execute(&block) |
| 96 | + raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) |
| 97 | + |
| 98 | + validate! |
| 99 | + |
| 100 | + pact_handle = init_pact |
| 101 | + init_plugin!(pact_handle) |
| 102 | + |
| 103 | + interaction = PactFfi::AsyncMessageConsumer.new(pact_handle, @description) |
| 104 | + |
| 105 | + @provider_state_meta&.each_pair do |provider_state, meta| |
| 106 | + if meta.present? |
| 107 | + meta.each_pair do |k, v| |
| 108 | + if v.nil? || (v.respond_to?(:empty?) && v.empty?) |
| 109 | + PactFfi.given(interaction, provider_state) |
| 110 | + else |
| 111 | + puts "Given #{provider_state} with param #{k}: #{v}" |
| 112 | + PactFfi.given_with_param(interaction, provider_state, k.to_s, v.to_s) |
| 113 | + end |
| 114 | + end |
| 115 | + else |
| 116 | + PactFfi.given(interaction, provider_state) |
| 117 | + end |
| 118 | + end |
| 119 | + |
| 120 | + result = PactFfi.with_body(interaction, 0, @interaction_content_type, interaction_json) |
| 121 | + if CREATE_INTERACTION_ERRORS[result].present? |
| 122 | + error = CREATE_INTERACTION_ERRORS[result] |
| 123 | + raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) |
| 124 | + end |
| 125 | + |
| 126 | + mock_server = MockServer.create_for_transport!(pact: pact_handle, transport: @transport, host: @pact_config.mock_host, port: @pact_config.mock_port) |
| 127 | + |
| 128 | + yield(pact_handle, mock_server) |
| 129 | + if mock_server.matched? |
| 130 | + mock_server.write_pacts!(@pact_config.pact_dir) |
| 131 | + else |
| 132 | + msg = mismatches_error_msg(mock_server) |
| 133 | + raise InteractionMismatchesError.new(msg) |
| 134 | + end |
| 135 | + ensure |
| 136 | + @used = true |
| 137 | + mock_server&.cleanup |
| 138 | + PactFfi::PluginConsumer.cleanup_plugins(pact_handle) if pact_handle |
| 139 | + PactFfi.free_pact_handle(pact_handle) if pact_handle |
| 140 | + end |
| 141 | + |
| 142 | + private |
| 143 | + |
| 144 | + def mismatches_error_msg(mock_server) |
| 145 | + rspec_example_desc = RSpec.current_example&.description |
| 146 | + return "interaction for has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank? |
| 147 | + |
| 148 | + "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}" |
| 149 | + end |
| 150 | + |
| 151 | + def init_pact |
| 152 | + handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name) |
| 153 | + PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) |
| 154 | + PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version) |
| 155 | + |
| 156 | + Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level) |
| 157 | + |
| 158 | + handle |
| 159 | + end |
| 160 | + |
| 161 | + def init_plugin!(pact_handle) |
| 162 | + result = PactFfi::PluginConsumer.using_plugin(pact_handle, @plugin_name, @plugin_version) |
| 163 | + return result if INIT_PLUGIN_ERRORS[result].blank? |
| 164 | + |
| 165 | + error = INIT_PLUGIN_ERRORS[result] |
| 166 | + raise PluginInitError.new("There was an error while trying to initialize plugin #{@plugin_name}/#{@plugin_version}", error[:reason], error[:status]) |
| 167 | + end |
| 168 | + end |
| 169 | + end |
| 170 | + end |
| 171 | +end |
0 commit comments