From 59592598486d216e15b7ab97ea0e86f844c68d85 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Mon, 26 Apr 2021 10:44:53 +1000 Subject: [PATCH] refactor: use pub/sub to trigger webhook events --- lib/pact_broker/api/resources/pact.rb | 18 +- .../api/resources/verifications.rb | 14 +- .../api/resources/webhook_execution.rb | 2 +- .../resources/webhook_execution_methods.rb | 30 ++ lib/pact_broker/events/event.rb | 5 + lib/pact_broker/pacts/service.rb | 92 ++++- lib/pact_broker/test/test_data_builder.rb | 3 +- lib/pact_broker/verifications/service.rb | 30 +- lib/pact_broker/webhooks/event_listener.rb | 82 ++++ lib/pact_broker/webhooks/job.rb | 2 +- lib/pact_broker/webhooks/service.rb | 61 --- lib/pact_broker/webhooks/trigger_service.rb | 136 +++---- pact_broker.gemspec | 1 + spec/features/execute_unsaved_webhook_spec.rb | 3 - spec/features/publish_pact_spec.rb | 41 +- .../api/resources/verifications_spec.rb | 10 +- .../api/resources/webhook_execution_spec.rb | 4 +- spec/lib/pact_broker/pacts/service_spec.rb | 112 ++++-- .../pact_broker/verifications/service_spec.rb | 37 +- spec/lib/pact_broker/webhooks/job_spec.rb | 10 +- spec/lib/pact_broker/webhooks/service_spec.rb | 237 ----------- .../webhooks/trigger_service_spec.rb | 376 ++++++++++++------ spec/migrations/23_pact_versions_spec.rb | 2 - 23 files changed, 694 insertions(+), 614 deletions(-) create mode 100644 lib/pact_broker/events/event.rb create mode 100644 lib/pact_broker/webhooks/event_listener.rb diff --git a/lib/pact_broker/api/resources/pact.rb b/lib/pact_broker/api/resources/pact.rb index 42df34020..bc49e7313 100644 --- a/lib/pact_broker/api/resources/pact.rb +++ b/lib/pact_broker/api/resources/pact.rb @@ -58,12 +58,13 @@ def resource_exists? def from_json response_code = pact ? 200 : 201 - if request.patch? && resource_exists? - @pact = pact_service.merge_pact(pact_params, webhook_options) - else - @pact = pact_service.create_or_update_pact(pact_params, webhook_options) + handle_webhook_events do + if request.patch? && resource_exists? + @pact = pact_service.merge_pact(pact_params) + else + @pact = pact_service.create_or_update_pact(pact_params) + end end - response.body = to_json response_code end @@ -111,13 +112,6 @@ def pact def pact_params @pact_params ||= PactBroker::Pacts::PactParams.from_request request, path_info end - - def webhook_options - { - database_connector: database_connector, - webhook_execution_configuration: webhook_execution_configuration - } - end end end end diff --git a/lib/pact_broker/api/resources/verifications.rb b/lib/pact_broker/api/resources/verifications.rb index 0bdb771f8..7758dc33c 100644 --- a/lib/pact_broker/api/resources/verifications.rb +++ b/lib/pact_broker/api/resources/verifications.rb @@ -5,6 +5,7 @@ require 'pact_broker/api/decorators/verification_decorator' require 'pact_broker/api/resources/webhook_execution_methods' require 'pact_broker/api/resources/metadata_resource_methods' +require 'pact_broker/webhooks/event_listener' module PactBroker module Api @@ -50,8 +51,10 @@ def create_path end def from_json - verification = verification_service.create(next_verification_number, verification_params, pact, event_context, webhook_options) - response.body = decorator_for(verification).to_json(decorator_options) + handle_webhook_events do + verification = verification_service.create(next_verification_number, verification_params, pact, event_context) + response.body = decorator_for(verification).to_json(decorator_options) + end true end @@ -85,13 +88,6 @@ def event_context metadata end - def webhook_options - { - database_connector: database_connector, - webhook_execution_configuration: webhook_execution_configuration - } - end - def verification_params params(symbolize_names: false).merge('wip' => wip?) end diff --git a/lib/pact_broker/api/resources/webhook_execution.rb b/lib/pact_broker/api/resources/webhook_execution.rb index be79c17f8..5b65c7aec 100644 --- a/lib/pact_broker/api/resources/webhook_execution.rb +++ b/lib/pact_broker/api/resources/webhook_execution.rb @@ -26,7 +26,7 @@ def allowed_methods end def process_post - webhook_execution_result = webhook_service.test_execution(webhook, webhook_execution_configuration.webhook_context, webhook_execution_configuration) + webhook_execution_result = webhook_trigger_service.test_execution(webhook, webhook_execution_configuration.webhook_context, webhook_execution_configuration) response.headers['Content-Type'] = 'application/hal+json;charset=utf-8' response.body = post_response_body(webhook_execution_result) true diff --git a/lib/pact_broker/api/resources/webhook_execution_methods.rb b/lib/pact_broker/api/resources/webhook_execution_methods.rb index ccb693ad7..b536b1e2b 100644 --- a/lib/pact_broker/api/resources/webhook_execution_methods.rb +++ b/lib/pact_broker/api/resources/webhook_execution_methods.rb @@ -1,3 +1,5 @@ +require 'pact_broker/webhooks/event_listener' + module PactBroker module Api module Resources @@ -5,6 +7,34 @@ module WebhookExecutionMethods def webhook_execution_configuration application_context.webhook_execution_configuration_creator.call(self) end + + def webhook_options + { + database_connector: database_connector, + webhook_execution_configuration: webhook_execution_configuration + } + end + + def webhook_event_listener + @webhook_event_listener ||= PactBroker::Webhooks::EventListener.new(webhook_options) + end + + def handle_webhook_events + Wisper.subscribe(webhook_event_listener) do + yield + end + end + + def schedule_triggered_webhooks + webhook_event_listener.schedule_triggered_webhooks + end + + def finish_request + if response.code < 400 + schedule_triggered_webhooks + end + super + end end end end diff --git a/lib/pact_broker/events/event.rb b/lib/pact_broker/events/event.rb new file mode 100644 index 000000000..53f4b511a --- /dev/null +++ b/lib/pact_broker/events/event.rb @@ -0,0 +1,5 @@ +module PactBroker + module Events + Event = Struct.new(:name, :comment, :triggered_webhooks) + end +end diff --git a/lib/pact_broker/pacts/service.rb b/lib/pact_broker/pacts/service.rb index 8abb6fa12..2601cb744 100644 --- a/lib/pact_broker/pacts/service.rb +++ b/lib/pact_broker/pacts/service.rb @@ -4,12 +4,14 @@ require 'pact_broker/pacts/merger' require 'pact_broker/pacts/verifiable_pact' require 'pact_broker/pacts/squash_pacts_for_verification' +require 'wisper' module PactBroker module Pacts module Service extend self + extend Wisper::Publisher extend PactBroker::Repositories extend PactBroker::Services @@ -43,20 +45,20 @@ def delete params pact_repository.delete(params) end - def create_or_update_pact params, webhook_options + def create_or_update_pact params provider = pacticipant_repository.find_by_name_or_create params[:provider_name] consumer = pacticipant_repository.find_by_name_or_create params[:consumer_name] consumer_version = version_repository.find_by_pacticipant_id_and_number_or_create consumer.id, params[:consumer_version_number] existing_pact = pact_repository.find_by_version_and_provider(consumer_version.id, provider.id) if existing_pact - update_pact params, existing_pact, webhook_options + update_pact params, existing_pact else - create_pact params, consumer_version, provider, webhook_options + create_pact params, consumer_version, provider end end - def merge_pact params, webhook_options + def merge_pact params provider = pacticipant_repository.find_by_name_or_create params[:provider_name] consumer = pacticipant_repository.find_by_name_or_create params[:consumer_name] consumer_version = version_repository.find_by_pacticipant_id_and_number_or_create consumer.id, params[:consumer_version_number] @@ -66,7 +68,7 @@ def merge_pact params, webhook_options existing_pact.json_content, params[:json_content] ) - update_pact params, existing_pact, webhook_options + update_pact params, existing_pact end def find_all_pact_versions_between consumer, options @@ -129,8 +131,6 @@ def find_for_verification(provider_name, provider_version_branch, provider_versi verifiable_pacts_specified_in_request + verifiable_wip_pacts end - private - def exclude_specified_pacts(wip_pacts, specified_pacts) wip_pacts.reject do | wip_pact | specified_pacts.any? do | specified_pacts | @@ -139,8 +139,10 @@ def exclude_specified_pacts(wip_pacts, specified_pacts) end end + private :exclude_specified_pacts + # Overwriting an existing pact with the same consumer/provider/consumer version number - def update_pact params, existing_pact, webhook_options + def update_pact params, existing_pact logger.info "Updating existing pact publication with params #{params.reject{ |k, v| k == :json_content}}" logger.debug "Content #{params[:json_content]}" pact_version_sha = generate_sha(params[:json_content]) @@ -149,13 +151,23 @@ def update_pact params, existing_pact, webhook_options updated_pact = pact_repository.update(existing_pact.id, update_params) event_context = { consumer_version_tags: updated_pact.consumer_version_tag_names } - webhook_trigger_service.trigger_webhooks_for_updated_pact(existing_pact, updated_pact, event_context, merge_consumer_version_info(webhook_options, updated_pact)) + event_params = { event_context: event_context, pact: updated_pact } + + broadcast(:contract_published, event_params) + + if existing_pact.pact_version_sha != updated_pact.pact_version_sha + broadcast(:contract_content_changed, event_params.merge(event_comment: "Pact content modified since previous revision")) + else + broadcast(:contract_content_unchanged, event_params.merge(event_comment: "Pact content was unchanged")) + end updated_pact end + private :update_pact + # When no publication for the given consumer/provider/consumer version number exists - def create_pact params, version, provider, webhook_options + def create_pact params, version, provider logger.info "Creating new pact publication with params #{params.reject{ |k, v| k == :json_content}}" logger.debug "Content #{params[:json_content]}" pact_version_sha = generate_sha(params[:json_content]) @@ -167,24 +179,72 @@ def create_pact params, version, provider, webhook_options pact_version_sha: pact_version_sha, json_content: json_content ) - event_context = { consumer_version_tags: pact.consumer_version_tag_names } - webhook_trigger_service.trigger_webhooks_for_new_pact(pact, event_context, merge_consumer_version_info(webhook_options, pact)) + + event_params = { event_context: { consumer_version_tags: pact.consumer_version_tag_names }, pact: pact } + broadcast(:contract_published, event_params) + + content_changed, explanation = pact_is_new_or_newly_tagged_or_pact_has_changed_since_previous_version?(pact) + if content_changed + broadcast(:contract_content_changed, event_params.merge(event_comment: explanation)) + else + broadcast(:contract_content_unchanged, event_params.merge(event_comment: "Pact content the same as previous version and no new tags were applied")) + end + pact end + private :create_pact + def generate_sha(json_content) PactBroker.configuration.sha_generator.call(json_content) end + private :generate_sha + def add_interaction_ids(json_content) Content.from_json(json_content).with_ids.to_json end - def merge_consumer_version_info(webhook_options, pact) - execution_configuration = webhook_options[:webhook_execution_configuration] - .with_webhook_context(consumer_version_tags: pact.consumer_version_tag_names) - webhook_options.merge(webhook_execution_configuration: execution_configuration) + private :add_interaction_ids + + def pact_is_new_or_newly_tagged_or_pact_has_changed_since_previous_version? pact + changed_pacts = pact_repository + .find_previous_pacts(pact) + .reject { |_, previous_pact| !sha_changed_or_no_previous_version?(previous_pact, pact) } + explanation = explanation_for_content_changed(changed_pacts) + return changed_pacts.any?, explanation + end + + private :pact_is_new_or_newly_tagged_or_pact_has_changed_since_previous_version? + + def sha_changed_or_no_previous_version?(previous_pact, new_pact) + previous_pact.nil? || new_pact.pact_version_sha != previous_pact.pact_version_sha + end + + private :sha_changed_or_no_previous_version? + + def explanation_for_content_changed(changed_pacts) + if changed_pacts.any? + messages = changed_pacts.collect do |tag, previous_pact| + if tag == :untagged + if previous_pact + "Pact content has changed since previous untagged version" + else + "First time untagged pact published" + end + else + if previous_pact + "Pact content has changed since the last consumer version tagged with #{tag}" + else + "First time pact published with consumer version tagged #{tag}" + end + end + end + messages.join(',') + end end + + private :explanation_for_content_changed end end end diff --git a/lib/pact_broker/test/test_data_builder.rb b/lib/pact_broker/test/test_data_builder.rb index f36a3c8f4..c16f8b8b1 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -4,6 +4,7 @@ require 'pact_broker/services' require 'pact_broker/webhooks/repository' require 'pact_broker/webhooks/service' +require 'pact_broker/webhooks/trigger_service' require 'pact_broker/webhooks/webhook_execution_result' require 'pact_broker/pacts/repository' require 'pact_broker/pacts/service' @@ -339,7 +340,7 @@ def create_triggered_webhook params = {} event_name = params.key?(:event_name) ? params[:event_name] : @webhook.events.first.name # could be nil, for backwards compatibility verification = @webhook.trigger_on_provider_verification_published? ? @verification : nil event_context = params[:event_context] - @triggered_webhook = webhook_repository.create_triggered_webhook(trigger_uuid, @webhook, @pact, verification, PactBroker::Webhooks::Service::RESOURCE_CREATION, event_name, event_context) + @triggered_webhook = webhook_repository.create_triggered_webhook(trigger_uuid, @webhook, @pact, verification, PactBroker::Webhooks::TriggerService::RESOURCE_CREATION, event_name, event_context) @triggered_webhook.update(status: params[:status]) if params[:status] set_created_at_if_set params[:created_at], :triggered_webhooks, { id: @triggered_webhook.id } self diff --git a/lib/pact_broker/verifications/service.rb b/lib/pact_broker/verifications/service.rb index 93ad29882..19cd1081d 100644 --- a/lib/pact_broker/verifications/service.rb +++ b/lib/pact_broker/verifications/service.rb @@ -3,6 +3,7 @@ require 'pact_broker/verifications/summary_for_consumer_version' require 'pact_broker/logging' require 'pact_broker/hash_refinements' +require 'wisper' module PactBroker @@ -15,12 +16,13 @@ module Service extend PactBroker::Services include PactBroker::Logging using PactBroker::HashRefinements + extend Wisper::Publisher def next_number verification_repository.next_number end - def create next_verification_number, params, pact, event_context, webhook_options + def create next_verification_number, params, pact, event_context logger.info "Creating verification #{next_verification_number} for pact_id=#{pact.id}", payload: params.reject{ |k,_| k == "testResults"} verification = PactBroker::Domain::Verification.new provider_version_number = params.fetch('providerApplicationVersion') @@ -29,15 +31,8 @@ def create next_verification_number, params, pact, event_context, webhook_option verification.number = next_verification_number verification = verification_repository.create(verification, provider_version_number, pact) - execution_configuration = webhook_options[:webhook_execution_configuration] - .with_webhook_context(provider_version_tags: verification.provider_version_tag_names) + broadcast_events(verification, pact, event_context) - webhook_trigger_service.trigger_webhooks_for_verification_results_publication( - pact, - verification, - event_context.merge(provider_version_tags: verification.provider_version_tag_names), - webhook_options.deep_merge(webhook_execution_configuration: execution_configuration) - ) verification end @@ -89,6 +84,23 @@ def verification_summary_for_consumer_version params def delete_all_verifications_between(consumer_name, options) verification_repository.delete_all_verifications_between(consumer_name, options) end + + private + + def broadcast_events(verification, pact, event_context) + event_params = { + pact: pact, + verification: verification, + event_context: event_context.merge(provider_version_tags: verification.provider_version_tag_names) + } + + broadcast(:provider_verification_published, event_params) + if verification.success + broadcast(:provider_verification_succeeded, event_params) + else + broadcast(:provider_verification_failed, event_params) + end + end end end end diff --git a/lib/pact_broker/webhooks/event_listener.rb b/lib/pact_broker/webhooks/event_listener.rb new file mode 100644 index 000000000..b7fd2e6db --- /dev/null +++ b/lib/pact_broker/webhooks/event_listener.rb @@ -0,0 +1,82 @@ +require 'pact_broker/services' +require 'pact_broker/events/event' +require 'pact_broker/logging' + +module PactBroker + module Webhooks + class EventListener + include PactBroker::Services + include PactBroker::Logging + + def initialize(webhook_options) + @webhook_options = webhook_options + # this has the base URL + @base_webhook_context = webhook_options[:webhook_execution_configuration].webhook_context + @detected_events = [] + end + + def contract_published(params) + handle_event_for_webhook(PactBroker::Webhooks::WebhookEvent::CONTRACT_PUBLISHED, params) + end + + def contract_content_changed(params) + handle_event_for_webhook(PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, params) + end + + def contract_content_unchanged(params) + detected_events << PactBroker::Events::Event.new( + "contract_content_unchanged", + params[:event_comment], + [] + ) + log_detected_event + end + + def provider_verification_published(params) + handle_event_for_webhook(PactBroker::Webhooks::WebhookEvent::VERIFICATION_PUBLISHED, params) + end + + def provider_verification_succeeded(params) + handle_event_for_webhook(PactBroker::Webhooks::WebhookEvent::VERIFICATION_SUCCEEDED, params) + end + + def provider_verification_failed(params) + handle_event_for_webhook(PactBroker::Webhooks::WebhookEvent::VERIFICATION_FAILED, params) + end + + def log_detected_event + event = detected_events.last + logger.info "Event detected", payload: { event_name: event.name, event_comment: event.comment } + if event.triggered_webhooks&.any? + triggered_webhook_descriptions = event.triggered_webhooks.collect{ |tw| { webhook_uuid: tw.webhook_uuid, triggered_webhook_uuid: tw.trigger_uuid, webhook_description: tw.webhook.description } } + logger.info "Triggered webhooks for #{event.name}", payload: { triggered_webhooks: triggered_webhook_descriptions } + else + logger.info "No enabled webhooks found for event #{event.name}" + end + end + + def schedule_triggered_webhooks + webhook_trigger_service.schedule_webhooks(detected_events.flat_map(&:triggered_webhooks), webhook_options) + end + + private + + attr_reader :webhook_options, :base_webhook_context, :detected_events + + def handle_event_for_webhook(event_name, params) + triggered_webhooks = webhook_trigger_service.create_triggered_webhooks_for_event( + params.fetch(:pact), + params[:verification], + event_name, + base_webhook_context.merge(params.fetch(:event_context)) + ) + detected_events << PactBroker::Events::Event.new( + event_name, + params[:event_comment], + triggered_webhooks + ) + log_detected_event + end + end + end +end diff --git a/lib/pact_broker/webhooks/job.rb b/lib/pact_broker/webhooks/job.rb index e6a4f6c15..f517a4be0 100644 --- a/lib/pact_broker/webhooks/job.rb +++ b/lib/pact_broker/webhooks/job.rb @@ -36,7 +36,7 @@ def perform_with_connection(data) def perform_with_triggered_webhook @error_count = data[:error_count] || 0 begin - webhook_execution_result = PactBroker::Webhooks::Service.execute_triggered_webhook_now(triggered_webhook, webhook_options(data)) + webhook_execution_result = PactBroker::Webhooks::TriggerService.execute_triggered_webhook_now(triggered_webhook, webhook_options(data)) if webhook_execution_result.success? handle_success else diff --git a/lib/pact_broker/webhooks/service.rb b/lib/pact_broker/webhooks/service.rb index cc7c0ae52..b42653012 100644 --- a/lib/pact_broker/webhooks/service.rb +++ b/lib/pact_broker/webhooks/service.rb @@ -21,9 +21,6 @@ module Webhooks class Service using PactBroker::HashRefinements - RESOURCE_CREATION = PactBroker::Webhooks::TriggeredWebhook::TRIGGER_TYPE_RESOURCE_CREATION - USER = PactBroker::Webhooks::TriggeredWebhook::TRIGGER_TYPE_USER - extend Repositories extend Services include Logging @@ -88,24 +85,6 @@ def self.find_all webhook_repository.find_all end - def self.test_execution webhook, event_context, execution_configuration - merged_options = execution_configuration.with_failure_log_message("Webhook execution failed").to_hash - - verification = nil - if webhook.trigger_on_provider_verification_published? - verification = verification_service.search_for_latest(webhook.consumer_name, webhook.provider_name) || PactBroker::Verifications::PlaceholderVerification.new - end - - pact = pact_service.search_for_latest_pact(consumer_name: webhook.consumer_name, provider_name: webhook.provider_name) || PactBroker::Pacts::PlaceholderPact.new - webhook.execute(pact, verification, event_context.merge(event_name: "test"), merged_options) - end - - def self.execute_triggered_webhook_now triggered_webhook, webhook_execution_configuration_hash - webhook_execution_result = triggered_webhook.execute webhook_execution_configuration_hash - webhook_repository.create_execution triggered_webhook, webhook_execution_result - webhook_execution_result - end - def self.update_triggered_webhook_status triggered_webhook, status webhook_repository.update_triggered_webhook_status triggered_webhook, status end @@ -122,46 +101,6 @@ def self.find_by_consumer_and_provider consumer, provider webhook_repository.find_by_consumer_and_provider consumer, provider end - def self.trigger_webhooks pact, verification, event_name, event_context, options - webhooks = webhook_repository.find_by_consumer_and_or_provider_and_event_name pact.consumer, pact.provider, event_name - - if webhooks.any? - webhook_execution_configuration = options.fetch(:webhook_execution_configuration).with_webhook_context(event_name: event_name) - # bit messy to merge in base_url here, but easier than a big refactor - base_url = options.fetch(:webhook_execution_configuration).webhook_context.fetch(:base_url) - - run_webhooks_later(webhooks, pact, verification, event_name, event_context.merge(event_name: event_name, base_url: base_url), options.merge(webhook_execution_configuration: webhook_execution_configuration)) - else - logger.info "No enabled webhooks found for consumer \"#{pact.consumer.name}\" and provider \"#{pact.provider.name}\" and event #{event_name}" - end - end - - def self.run_webhooks_later webhooks, pact, verification, event_name, event_context, options - webhooks.each do | webhook | - if PactBroker.feature_enabled?(:expand_currently_deployed_provider_versions) && webhook.expand_currently_deployed_provider_versions? - deployed_version_service.find_currently_deployed_versions_for_pacticipant(pact.provider).collect(&:version_number).uniq.each_with_index do | version_number, index | - schedule_webhook(webhook, pact, verification, event_name, event_context.merge(currently_deployed_provider_version_number: version_number), options, index * 5) - end - else - schedule_webhook(webhook, pact, verification, event_name, event_context, options) - end - end - end - - def self.schedule_webhook(webhook, pact, verification, event_name, event_context, options, extra_delay = 0) - begin - trigger_uuid = next_uuid - triggered_webhook = webhook_repository.create_triggered_webhook(trigger_uuid, webhook, pact, verification, RESOURCE_CREATION, event_name, event_context) - logger.info "Scheduling job for webhook with uuid #{webhook.uuid}, context: #{event_context}" - logger.debug "Schedule webhook with options #{options}" - job_data = { triggered_webhook: triggered_webhook }.deep_merge(options) - # Delay slightly to make sure the request transaction has finished before we execute the webhook - Job.perform_in(5 + extra_delay, job_data) - rescue StandardError => e - logger.warn("Error scheduling webhook execution for webhook with uuid #{webhook.uuid}", e) - end - end - def self.find_latest_triggered_webhooks_for_pact pact webhook_repository.find_latest_triggered_webhooks_for_pact pact end diff --git a/lib/pact_broker/webhooks/trigger_service.rb b/lib/pact_broker/webhooks/trigger_service.rb index 0122db774..17da597b8 100644 --- a/lib/pact_broker/webhooks/trigger_service.rb +++ b/lib/pact_broker/webhooks/trigger_service.rb @@ -4,6 +4,8 @@ module PactBroker module Webhooks module TriggerService + RESOURCE_CREATION = PactBroker::Webhooks::TriggeredWebhook::TRIGGER_TYPE_RESOURCE_CREATION + USER = PactBroker::Webhooks::TriggeredWebhook::TRIGGER_TYPE_USER extend self extend PactBroker::Repositories @@ -11,71 +13,70 @@ module TriggerService include PactBroker::Logging using PactBroker::HashRefinements - def trigger_webhooks_for_new_pact(pact, event_context, webhook_options) - webhook_service.trigger_webhooks pact, nil, PactBroker::Webhooks::WebhookEvent::CONTRACT_PUBLISHED, event_context, webhook_options - if pact_is_new_or_newly_tagged_or_pact_has_changed_since_previous_version?(pact) - webhook_service.trigger_webhooks pact, nil, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, event_context, webhook_options - else - logger.info "Pact content has not changed since previous version, not triggering webhooks for changed content" + def next_uuid + SecureRandom.uuid + end + + # TODO support currently deployed + def test_execution webhook, event_context, execution_configuration + merged_options = execution_configuration.with_failure_log_message("Webhook execution failed").to_hash + + verification = nil + if webhook.trigger_on_provider_verification_published? + verification = verification_service.search_for_latest(webhook.consumer_name, webhook.provider_name) || PactBroker::Verifications::PlaceholderVerification.new end + + pact = pact_service.search_for_latest_pact(consumer_name: webhook.consumer_name, provider_name: webhook.provider_name) || PactBroker::Pacts::PlaceholderPact.new + webhook.execute(pact, verification, event_context.merge(event_name: "test"), merged_options) + end + + def execute_triggered_webhook_now triggered_webhook, webhook_execution_configuration_hash + webhook_execution_result = triggered_webhook.execute webhook_execution_configuration_hash + webhook_repository.create_execution triggered_webhook, webhook_execution_result + webhook_execution_result end - def trigger_webhooks_for_updated_pact(existing_pact, updated_pact, event_context, webhook_options) - webhook_service.trigger_webhooks updated_pact, nil, PactBroker::Webhooks::WebhookEvent::CONTRACT_PUBLISHED, event_context, webhook_options - if existing_pact.pact_version_sha != updated_pact.pact_version_sha - logger.info "Existing pact for version #{existing_pact.consumer_version_number} has been updated with new content, triggering webhooks for changed content" - webhook_service.trigger_webhooks updated_pact, nil, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, event_context, webhook_options + # the main entry point + def create_triggered_webhooks_for_event pact, verification, event_name, event_context + webhooks = webhook_repository.find_by_consumer_and_or_provider_and_event_name pact.consumer, pact.provider, event_name + + if webhooks.any? + create_triggered_webhooks_for_webhooks(webhooks, pact, verification, event_name, event_context.merge(event_name: event_name)) else - logger.info "Pact content has not changed since previous revision, not triggering webhooks for changed content" + [] end end - def trigger_webhooks_for_verification_results_publication(pact, verification, event_context, webhook_options) - expand_events(event_context).each do | reconstituted_event_context | - # The pact passed in is the most recent one with the matching SHA. - # Find the pact with the right consumer version number - pact_for_triggered_webhook = find_pact_for_verification_triggered_webhook(pact, reconstituted_event_context) - if verification.success - webhook_service.trigger_webhooks( - pact_for_triggered_webhook, - verification, - PactBroker::Webhooks::WebhookEvent::VERIFICATION_SUCCEEDED, - reconstituted_event_context, - webhook_options - ) - else - webhook_service.trigger_webhooks( - pact_for_triggered_webhook, - verification, - PactBroker::Webhooks::WebhookEvent::VERIFICATION_FAILED, - reconstituted_event_context, - webhook_options - ) - end + # private + def create_triggered_webhooks_for_webhooks webhooks, pact, verification, event_name, event_context + webhooks.flat_map do | webhook | + expanded_event_contexts = expand_events_for_currently_deployed_environments(webhook, pact, event_context) + expanded_event_contexts = expanded_event_contexts.flat_map { | ec | expand_events_for_verification_of_multiple_selected_pacts(ec) } - webhook_service.trigger_webhooks( - pact_for_triggered_webhook, - verification, - PactBroker::Webhooks::WebhookEvent::VERIFICATION_PUBLISHED, - reconstituted_event_context, - webhook_options - ) + expanded_event_contexts.collect do | event_context | + pact_for_triggered_webhook = verification ? find_pact_for_verification_triggered_webhook(pact, event_context) : pact + webhook_repository.create_triggered_webhook(next_uuid, webhook, pact_for_triggered_webhook, verification, RESOURCE_CREATION, event_name, event_context) + end end end - private + def schedule_webhooks(triggered_webhooks, options) + triggered_webhooks.each_with_index do | triggered_webhook, i | + logger.info "Scheduling job for webhook with uuid #{triggered_webhook.webhook.uuid}, context: #{triggered_webhook.event_context}" + logger.debug "Schedule webhook with options #{options}" - def pact_is_new_or_newly_tagged_or_pact_has_changed_since_previous_version? pact - changed_pacts = pact_repository - .find_previous_pacts(pact) - .reject { |_, previous_pact| !sha_changed_or_no_previous_version?(previous_pact, pact) } - print_debug_messages(changed_pacts) - changed_pacts.any? + job_data = { triggered_webhook: triggered_webhook }.deep_merge(options) + begin + # Delay slightly to make sure the request transaction has finished before we execute the webhook + Job.perform_in(5 + (i * 3), job_data) + rescue StandardError => e + logger.warn("Error scheduling webhook execution for webhook with uuid #{triggered_webhook&.webhook&.uuid}", e) + nil + end + end end - def sha_changed_or_no_previous_version?(previous_pact, new_pact) - previous_pact.nil? || new_pact.pact_version_sha != previous_pact.pact_version_sha - end + private def merge_consumer_version_selectors(consumer_version_number, selectors, event_context) event_context.merge( @@ -93,7 +94,7 @@ def merge_consumer_version_selectors(consumer_version_number, selectors, event_c # Actually, we used to trigger one webhook per tag, but given that the most likely use of the # verification published webhook is for reporting git statuses, # it makes more sense to trigger per consumer version number (ie. commit). - def expand_events(event_context) + def expand_events_for_verification_of_multiple_selected_pacts(event_context) triggers = if event_context[:consumer_version_selectors].is_a?(Array) event_context[:consumer_version_selectors] .group_by{ | selector | selector[:consumer_version_number] } @@ -103,6 +104,16 @@ def expand_events(event_context) end end + def expand_events_for_currently_deployed_environments(webhook, pact, event_context) + if PactBroker.feature_enabled?(:expand_currently_deployed_provider_versions) && webhook.expand_currently_deployed_provider_versions? + deployed_version_service.find_currently_deployed_versions_for_pacticipant(pact.provider).collect(&:version_number).uniq.collect do | version_number | + event_context.merge(currently_deployed_provider_version_number: version_number) + end + else + [event_context] + end + end + def find_pact_for_verification_triggered_webhook(pact, reconstituted_event_context) if reconstituted_event_context[:consumer_version_number] find_pact_params = { @@ -115,27 +126,6 @@ def find_pact_for_verification_triggered_webhook(pact, reconstituted_event_conte pact end end - - def print_debug_messages(changed_pacts) - if changed_pacts.any? - messages = changed_pacts.collect do |tag, previous_pact| - if tag == :untagged - if previous_pact - "pact content has changed since previous untagged version" - else - "first time untagged pact published" - end - else - if previous_pact - "pact content has changed since the last consumer version tagged with #{tag}" - else - "first time pact published with consumer version tagged #{tag}" - end - end - end - logger.info("Webhook triggered for the following reasons: #{messages.join(',')}" ) - end - end end end end diff --git a/pact_broker.gemspec b/pact_broker.gemspec index 2350e9114..574aa0830 100644 --- a/pact_broker.gemspec +++ b/pact_broker.gemspec @@ -64,4 +64,5 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'table_print', '~> 1.5' gem.add_runtime_dependency 'semantic_logger', '~> 4.3' gem.add_runtime_dependency 'sanitize', '>= 5.2.1', '~> 5.2' + gem.add_runtime_dependency 'wisper', '2.0.0' end diff --git a/spec/features/execute_unsaved_webhook_spec.rb b/spec/features/execute_unsaved_webhook_spec.rb index eaaf8d4a7..c0d13db2d 100644 --- a/spec/features/execute_unsaved_webhook_spec.rb +++ b/spec/features/execute_unsaved_webhook_spec.rb @@ -3,9 +3,6 @@ require 'rack/pact_broker/database_transaction' describe "Execute a webhook" do - - let(:td) { TestDataBuilder.new } - before do td.create_pact_with_hierarchy("Foo", "1", "Bar") allow(PactBroker.configuration).to receive(:webhook_scheme_whitelist).and_return(%w[http]) diff --git a/spec/features/publish_pact_spec.rb b/spec/features/publish_pact_spec.rb index d8e381354..af2cc5c43 100644 --- a/spec/features/publish_pact_spec.rb +++ b/spec/features/publish_pact_spec.rb @@ -3,8 +3,14 @@ let(:pact_content) { load_fixture('a_consumer-a_provider.json') } let(:path) { "/pacts/provider/A%20Provider/consumer/A%20Consumer/versions/1.2.3" } let(:response_body_json) { JSON.parse(subject.body) } + let(:rack_env) do + { + 'CONTENT_TYPE' => 'application/json', + 'pactbroker.database_connector' => lambda { |&block| block.call } + } + end - subject { put path, pact_content, {'CONTENT_TYPE' => 'application/json' }; last_response } + subject { put(path, pact_content, rack_env) } context "when a pact for this consumer version does not exist" do it "returns a 201 Created" do @@ -23,7 +29,7 @@ context "when a pact for this consumer version does exist" do before do - TestDataBuilder.new.create_pact_with_hierarchy("A Consumer", "1.2.3", "A Provider").and_return(:pact) + td.create_pact_with_hierarchy("A Consumer", "1.2.3", "A Provider").and_return(:pact) end it "returns a 200 Success" do @@ -49,7 +55,7 @@ context "when the pacticipant name is an almost duplicate of an existing pacticipant name" do before do - TestDataBuilder.new.create_pacticipant("A Provider Service") + td.create_pacticipant("A Provider Service") end context "when duplicate checking is on" do @@ -72,4 +78,33 @@ end end end + + context "with a webhook configured", job: true do + before do + td.create_webhook( + method: 'POST', + url: 'http://example.org', + events: [{ name: PactBroker::Webhooks::WebhookEvent::CONTRACT_PUBLISHED }] + ) + end + let!(:request) do + stub_request(:post, 'http://example.org').to_return(:status => 200) + end + + it "executes the webhook" do + subject + expect(request).to have_been_made + end + + context "when an error occurs rendering the pact" do + before do + allow_any_instance_of(PactBroker::Api::Decorators::PactDecorator).to receive(:to_json).and_raise("an error") + end + + it "does not execute the webhook" do + subject + expect(request).to_not have_been_made + end + end + end end diff --git a/spec/lib/pact_broker/api/resources/verifications_spec.rb b/spec/lib/pact_broker/api/resources/verifications_spec.rb index d2fb536f0..f6a19b65e 100644 --- a/spec/lib/pact_broker/api/resources/verifications_spec.rb +++ b/spec/lib/pact_broker/api/resources/verifications_spec.rb @@ -22,11 +22,10 @@ module Resources let(:webhook_execution_configuration) { instance_double(PactBroker::Webhooks::ExecutionConfiguration) } before do + allow_any_instance_of(Verifications).to receive(:handle_webhook_events) { |&block| block.call } allow(PactBroker::Verifications::Service).to receive(:create).and_return(verification) allow(PactBroker::Verifications::Service).to receive(:errors).and_return(double(:errors, messages: ['errors'], empty?: errors_empty)) allow(PactBrokerUrls).to receive(:decode_pact_metadata).and_return(parsed_metadata) - allow_any_instance_of(Verifications).to receive(:webhook_execution_configuration).and_return(webhook_execution_configuration) - allow(webhook_execution_configuration).to receive(:with_webhook_context).and_return(webhook_execution_configuration) end subject { post(url, request_body, rack_env) } @@ -86,11 +85,7 @@ module Resources next_verification_number, hash_including('some' => 'params', 'wip' => false), pact, - parsed_metadata, - { - webhook_execution_configuration: webhook_execution_configuration, - database_connector: database_connector - } + parsed_metadata ) subject end @@ -113,7 +108,6 @@ module Resources anything, hash_including('wip' => true), anything, - anything, anything ) subject diff --git a/spec/lib/pact_broker/api/resources/webhook_execution_spec.rb b/spec/lib/pact_broker/api/resources/webhook_execution_spec.rb index df4f951d4..eaab5aa96 100644 --- a/spec/lib/pact_broker/api/resources/webhook_execution_spec.rb +++ b/spec/lib/pact_broker/api/resources/webhook_execution_spec.rb @@ -34,13 +34,13 @@ module Resources let(:event_context) { { some: "data" } } before do - allow(PactBroker::Webhooks::Service).to receive(:test_execution).and_return(execution_result) + allow(PactBroker::Webhooks::TriggerService).to receive(:test_execution).and_return(execution_result) allow(PactBroker::Api::Decorators::WebhookExecutionResultDecorator).to receive(:new).and_return(decorator) allow_any_instance_of(WebhookExecution).to receive(:webhook_execution_configuration).and_return(webhook_execution_configuration) end it "executes the webhook" do - expect(PactBroker::Webhooks::Service).to receive(:test_execution).with(webhook, event_context, webhook_execution_configuration) + expect(PactBroker::Webhooks::TriggerService).to receive(:test_execution).with(webhook, event_context, webhook_execution_configuration) subject end diff --git a/spec/lib/pact_broker/pacts/service_spec.rb b/spec/lib/pact_broker/pacts/service_spec.rb index bae8a253b..ffdf2fe7d 100644 --- a/spec/lib/pact_broker/pacts/service_spec.rb +++ b/spec/lib/pact_broker/pacts/service_spec.rb @@ -4,17 +4,12 @@ require 'pact_broker/webhooks/execution_configuration' module PactBroker - module Pacts describe Service do - let(:td) { TestDataBuilder.new } - describe "create_or_update_pact" do include_context "stubbed repositories" before do - allow(described_class).to receive(:webhook_service).and_return(webhook_service) - allow(described_class).to receive(:webhook_trigger_service).and_return(webhook_trigger_service) allow(pacticipant_repository).to receive(:find_by_name_or_create).with(params[:consumer_name]).and_return(consumer) allow(pacticipant_repository).to receive(:find_by_name_or_create).with(params[:provider_name]).and_return(provider) allow(version_repository).to receive(:find_by_pacticipant_id_and_number_or_create).and_return(version) @@ -22,18 +17,13 @@ module Pacts allow(pact_repository).to receive(:create).and_return(new_pact) allow(pact_repository).to receive(:update).and_return(new_pact) allow(pact_repository).to receive(:find_previous_pacts).and_return(previous_pacts) - allow(webhook_service).to receive(:trigger_webhooks) - allow(webhook_trigger_service).to receive(:trigger_webhooks_for_new_pact) - allow(webhook_trigger_service).to receive(:trigger_webhooks_for_updated_pact) end - let(:webhook_service) { class_double("PactBroker::Webhooks::Service").as_stubbed_const } - let(:webhook_trigger_service) { class_double("PactBroker::Webhooks::TriggerService").as_stubbed_const } let(:consumer) { double('consumer', id: 1) } let(:provider) { double('provider', id: 2) } let(:version) { double('version', id: 3, pacticipant_id: 1) } let(:existing_pact) { nil } - let(:new_pact) { double('new_pact', consumer_version_tag_names: %w[dev], json_content: json_content) } + let(:new_pact) { double('new_pact', consumer_version_tag_names: %w[dev], json_content: json_content, pact_version_sha: "1") } let(:json_content) { { the: "contract" }.to_json } let(:json_content_with_ids) { { the: "contract with ids" }.to_json } let(:previous_pacts) { [] } @@ -47,18 +37,16 @@ module Pacts end let(:content) { double('content') } let(:content_with_interaction_ids) { double('content_with_interaction_ids', to_json: json_content_with_ids) } - let(:webhook_options) { { webhook_execution_configuration: webhook_execution_configuration } } - let(:webhook_execution_configuration) { instance_double(PactBroker::Webhooks::ExecutionConfiguration) } let(:expected_event_context) { { consumer_version_tags: ["dev"] } } before do allow(Content).to receive(:from_json).and_return(content) allow(content).to receive(:with_ids).and_return(content_with_interaction_ids) allow(PactBroker::Pacts::GenerateSha).to receive(:call).and_call_original - allow(webhook_execution_configuration).to receive(:with_webhook_context).and_return(webhook_execution_configuration) + allow(Service).to receive(:broadcast) end - subject { Service.create_or_update_pact(params, webhook_options) } + subject { Service.create_or_update_pact(params) } context "when no pact exists with the same params" do it "creates the sha before adding the interaction ids" do @@ -72,14 +60,60 @@ module Pacts subject end - it "sets the consumer version tags" do - expect(webhook_execution_configuration).to receive(:with_webhook_context).with(consumer_version_tags: %w[dev]).and_return(webhook_execution_configuration) + it "broadcasts the contract_published event" do + expect(Service).to receive(:broadcast).with(:contract_published, pact: new_pact, event_context: { consumer_version_tags: %w[dev] }) subject end - it "triggers webhooks" do - expect(webhook_trigger_service).to receive(:trigger_webhooks_for_new_pact).with(new_pact, expected_event_context, webhook_options) - subject + # TODO test all this properly! + context "when the latest pact for one of the tags has a different pact_version_sha" do + before do + allow(pact_repository).to receive(:find_previous_pacts).and_return(previous_pacts_by_tag) + end + + let(:previous_dev_pact_version_sha) { "2" } + let(:previous_pacts_by_tag) do + { + dev: double('previous pact', pact_version_sha: previous_dev_pact_version_sha) + } + end + + it "broadcasts the contract_content_changed event" do + expect(Service).to receive(:broadcast).with( + :contract_content_changed, + { + pact: new_pact, + event_comment: "Pact content has changed since the last consumer version tagged with dev", + event_context: { consumer_version_tags: %w[dev] } + } + ) + subject + end + end + + context "when the new pact has not changed content or tags since the previous version with the same tags" do + before do + allow(pact_repository).to receive(:find_previous_pacts).and_return(previous_pacts_by_tag) + end + + let(:previous_dev_pact_version_sha) { "1" } + let(:previous_pacts_by_tag) do + { + dev: double('previous pact', pact_version_sha: previous_dev_pact_version_sha) + } + end + + it "broadcasts the contract_content_unchanged event" do + expect(Service).to receive(:broadcast).with( + :contract_content_unchanged, + { + pact: new_pact, + event_comment: "Pact content the same as previous version and no new tags were applied", + event_context: { consumer_version_tags: %w[dev] } + } + ) + subject + end end end @@ -88,9 +122,11 @@ module Pacts double('existing_pact', id: 4, consumer_version_tag_names: %[dev], - json_content: { the: "contract" }.to_json + json_content: { the: "contract" }.to_json, + pact_version_sha: pact_version_sha ) end + let(:pact_version_sha) { "1" } let(:expected_event_context) { { consumer_version_tags: ["dev"] } } @@ -105,10 +141,40 @@ module Pacts subject end - it "triggers webhooks" do - expect(webhook_trigger_service).to receive(:trigger_webhooks_for_updated_pact).with(existing_pact, new_pact, expected_event_context, webhook_options) + it "broadcasts the contract_published event" do + expect(Service).to receive(:broadcast).with(:contract_published, pact: new_pact, event_context: { consumer_version_tags: %w[dev] }) subject end + + context "when the pact_version_sha is different" do + let(:pact_version_sha) { "2" } + + it "broadcasts the contract_content_changed event" do + expect(Service).to receive(:broadcast).with( + :contract_content_changed, + { + pact: new_pact, + event_comment: "Pact content modified since previous revision", + event_context: { consumer_version_tags: %w[dev] } + } + ) + subject + end + end + + context "when the pact_version_sha is the same" do + it "broadcasts the contract_content_unchanged event" do + expect(Service).to receive(:broadcast).with( + :contract_content_unchanged, + { + pact: new_pact, + event_comment: "Pact content was unchanged", + event_context: { consumer_version_tags: %w[dev] } + } + ) + subject + end + end end end diff --git a/spec/lib/pact_broker/verifications/service_spec.rb b/spec/lib/pact_broker/verifications/service_spec.rb index a7c7990cc..1a6f49e59 100644 --- a/spec/lib/pact_broker/verifications/service_spec.rb +++ b/spec/lib/pact_broker/verifications/service_spec.rb @@ -17,22 +17,20 @@ module Verifications describe "#create" do before do - allow(PactBroker::Webhooks::TriggerService).to receive(:trigger_webhooks_for_verification_results_publication) - allow(webhook_execution_configuration).to receive(:with_webhook_context).and_return(webhook_execution_configuration) + allow(Service).to receive(:broadcast) end - let(:options) { { webhook_execution_configuration: webhook_execution_configuration } } let(:event_context) { { some: "data" } } let(:expected_event_context) { { some: "data", provider_version_tags: ["dev"] } } - let(:webhook_execution_configuration) { instance_double(PactBroker::Webhooks::ExecutionConfiguration) } - let(:params) { { 'success' => true, 'providerApplicationVersion' => '4.5.6', 'wip' => true, 'testResults' => { 'some' => 'results' }} } + let(:params) { { 'success' => success, 'providerApplicationVersion' => '4.5.6', 'wip' => true, 'testResults' => { 'some' => 'results' }} } + let(:success) { true } let(:pact) do td.create_pact_with_hierarchy .create_provider_version('4.5.6') .create_provider_version_tag('dev') .and_return(:pact) end - let(:create_verification) { subject.create 3, params, pact, event_context, options } + let(:create_verification) { subject.create 3, params, pact, event_context } it "logs the creation" do expect(logger).to receive(:info).with(/.*verification.*3/, payload: {"providerApplicationVersion"=>"4.5.6", "success"=>true, "wip"=>true}) @@ -59,20 +57,27 @@ module Verifications expect(verification.provider_version_number).to eq '4.5.6' end - it "sets the provider version tags on the webhook execution configuration" do - expect(webhook_execution_configuration).to receive(:with_webhook_context).with(provider_version_tags: %w[dev]) + it "it broadcasts the provider_verification_published event" do + expect(Service).to receive(:broadcast).with(:provider_verification_published, pact: pact, verification: instance_of(PactBroker::Domain::Verification), event_context: hash_including(provider_version_tags: %w[dev])) create_verification end - it "invokes the webhooks for the verification" do - verification = create_verification - expect(PactBroker::Webhooks::TriggerService).to have_received(:trigger_webhooks_for_verification_results_publication).with( - pact, - verification, - expected_event_context, - options - ) + context "when the verification is successful" do + it "it broadcasts the provider_verification_succeeded event" do + expect(Service).to receive(:broadcast).with(:provider_verification_succeeded, pact: pact, verification: instance_of(PactBroker::Domain::Verification), event_context: hash_including(provider_version_tags: %w[dev])) + create_verification + end end + + context "when the verification is not successful" do + let(:success) { false } + + it "it broadcasts the provider_verification_failed event" do + expect(Service).to receive(:broadcast).with(:provider_verification_failed, pact: pact, verification: instance_of(PactBroker::Domain::Verification), event_context: hash_including(provider_version_tags: %w[dev])) + create_verification + end + end + end describe "#errors" do diff --git a/spec/lib/pact_broker/webhooks/job_spec.rb b/spec/lib/pact_broker/webhooks/job_spec.rb index cda482903..be2ba2a8f 100644 --- a/spec/lib/pact_broker/webhooks/job_spec.rb +++ b/spec/lib/pact_broker/webhooks/job_spec.rb @@ -6,7 +6,7 @@ module Webhooks describe Job do before do PactBroker.configuration.webhook_retry_schedule = [10, 60, 120, 300, 600, 1200] - allow(PactBroker::Webhooks::Service).to receive(:execute_triggered_webhook_now).and_return(result) + allow(PactBroker::Webhooks::TriggerService).to receive(:execute_triggered_webhook_now).and_return(result) allow(PactBroker::Webhooks::Service).to receive(:update_triggered_webhook_status) allow(PactBroker::Webhooks::TriggeredWebhook).to receive(:find).and_return(triggered_webhook) allow(Job).to receive(:logger).and_return(logger) @@ -53,7 +53,7 @@ module Webhooks context "when an error occurs for the first time" do before do - allow(PactBroker::Webhooks::Service).to receive(:execute_triggered_webhook_now).and_raise(error) + allow(PactBroker::Webhooks::TriggerService).to receive(:execute_triggered_webhook_now).and_raise(error) end let(:error) { "an error" } @@ -107,7 +107,7 @@ module Webhooks end it "executes the job with an log message indicating that the webhook will be retried" do - expect(PactBroker::Webhooks::Service).to receive(:execute_triggered_webhook_now) + expect(PactBroker::Webhooks::TriggerService).to receive(:execute_triggered_webhook_now) .with(triggered_webhook, webhook_execution_configuration_hash) subject end @@ -121,7 +121,7 @@ module Webhooks context "when an error occurs for the second time" do before do - allow(PactBroker::Webhooks::Service).to receive(:execute_triggered_webhook_now).and_raise("an error") + allow(PactBroker::Webhooks::TriggerService).to receive(:execute_triggered_webhook_now).and_raise("an error") job_params[:error_count] = 1 end @@ -150,7 +150,7 @@ module Webhooks expect(webhook_execution_configuration).to receive(:with_failure_log_message).with("Webhook execution failed after 7 attempts") expect(webhook_execution_configuration).to receive(:with_success_log_message).with("Successfully executed webhook") - expect(PactBroker::Webhooks::Service).to receive(:execute_triggered_webhook_now) + expect(PactBroker::Webhooks::TriggerService).to receive(:execute_triggered_webhook_now) .with(triggered_webhook, webhook_execution_configuration_hash) subject end diff --git a/spec/lib/pact_broker/webhooks/service_spec.rb b/spec/lib/pact_broker/webhooks/service_spec.rb index 6c5595449..268c37496 100644 --- a/spec/lib/pact_broker/webhooks/service_spec.rb +++ b/spec/lib/pact_broker/webhooks/service_spec.rb @@ -7,7 +7,6 @@ require 'pact_broker/webhooks/execution_configuration' module PactBroker - module Webhooks describe Service do before do @@ -172,242 +171,6 @@ module Webhooks end end - describe ".trigger_webhooks" do - let(:verification) { instance_double(PactBroker::Domain::Verification)} - let(:pact) { instance_double(PactBroker::Domain::Pact, consumer: consumer, provider: provider, consumer_version: consumer_version)} - let(:consumer_version) { PactBroker::Domain::Version.new(number: '1.2.3') } - let(:consumer) { PactBroker::Domain::Pacticipant.new(name: 'Consumer') } - let(:provider) { PactBroker::Domain::Pacticipant.new(name: 'Provider') } - let(:webhooks) { [webhook]} - let(:webhook) do - instance_double(PactBroker::Domain::Webhook, description: 'description', uuid: '1244', expand_currently_deployed_provider_versions?: expand_currently_deployed) - end - let(:expand_currently_deployed) { false } - let(:triggered_webhook) { instance_double(PactBroker::Webhooks::TriggeredWebhook) } - let(:webhook_execution_configuration) { double('webhook_execution_configuration', webhook_context: webhook_context) } - let(:webhook_context) { { base_url: "http://example.org" } } - let(:event_context) { { some: "data" } } - let(:expected_event_context) { { some: "data", event_name: PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, base_url: "http://example.org" } } - let(:options) do - { database_connector: double('database_connector'), - webhook_execution_configuration: webhook_execution_configuration, - logging_options: {} - } - end - - before do - allow(webhook_execution_configuration).to receive(:with_webhook_context).and_return(webhook_execution_configuration) - allow_any_instance_of(PactBroker::Webhooks::Repository).to receive(:find_by_consumer_and_or_provider_and_event_name).and_return(webhooks) - allow_any_instance_of(PactBroker::Webhooks::Repository).to receive(:create_triggered_webhook).and_return(triggered_webhook) - allow(Job).to receive(:perform_in) - end - - subject { Service.trigger_webhooks(pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, event_context, options) } - - it "finds the webhooks" do - expect_any_instance_of(PactBroker::Webhooks::Repository).to receive(:find_by_consumer_and_or_provider_and_event_name).with(consumer, provider, PactBroker::Webhooks::WebhookEvent::DEFAULT_EVENT_NAME) - subject - end - - context "when webhooks are found" do - it "schedules the webhook" do - expect(Service).to receive(:run_webhooks_later).with(webhooks, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context, options) - subject - end - - it "merges the event name in the options" do - expect(webhook_execution_configuration).to receive(:with_webhook_context).with(event_name: PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED) - subject - end - - context "when there should be a webhook triggered for each currently deployed version" do - before do - allow(Service).to receive(:deployed_version_service).and_return(deployed_version_service) - allow(deployed_version_service).to receive(:find_currently_deployed_versions_for_pacticipant).and_return(currently_deployed_versions) - end - let(:expand_currently_deployed) { true } - let(:deployed_version_service) { class_double("PactBroker::Deployments::DeployedVersionService").as_stubbed_const } - let(:currently_deployed_version_1) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "1") } - let(:currently_deployed_version_2) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "2") } - let(:currently_deployed_versions) { [currently_deployed_version_1, currently_deployed_version_2] } - - it "schedules a triggered webhook for each currently deployed version" do - expect(Service).to receive(:schedule_webhook).with(webhook, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context.merge(currently_deployed_provider_version_number: "1"), options, 0) - expect(Service).to receive(:schedule_webhook).with(webhook, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context.merge(currently_deployed_provider_version_number: "2"), options, 5) - subject - end - - context "when the same version is deployed to multiple environments" do - let(:currently_deployed_version_2) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "1") } - - it "only triggers one webhook" do - expect(Service).to receive(:schedule_webhook).with(anything, anything, anything, anything, expected_event_context.merge(currently_deployed_provider_version_number: "1"), anything, 0) - subject - end - end - end - end - - context "when no webhooks are found" do - let(:webhooks) { [] } - it "does nothing" do - expect(Service).to_not receive(:run_webhooks_later) - subject - end - - it "logs that no webhook was found" do - expect(logger).to receive(:info).with(/No enabled webhooks found/) - subject - end - end - - context "when there is a scheduling error", job: true do - before do - allow(Job).to receive(:perform_in).and_raise("an error") - end - - it "logs the error" do - allow(Service.logger).to receive(:warn) - expect(Service.logger).to receive(:warn).with(/Error scheduling/, StandardError) - subject - end - end - end - - describe ".test_execution" do - let(:webhook) do - instance_double(PactBroker::Domain::Webhook, - trigger_on_provider_verification_published?: trigger_on_verification, - consumer_name: 'consumer', - provider_name: 'provider', - execute: result - ) - end - let(:pact) { instance_double(PactBroker::Domain::Pact) } - let(:verification) { instance_double(PactBroker::Domain::Verification) } - let(:trigger_on_verification) { false } - let(:result) { double('result') } - let(:execution_configuration) do - instance_double(PactBroker::Webhooks::ExecutionConfiguration, to_hash: execution_configuration_hash) - end - let(:execution_configuration_hash) { { the: 'options' } } - let(:event_context) { { some: "data" } } - - before do - allow(PactBroker::Pacts::Service).to receive(:search_for_latest_pact).and_return(pact) - allow(PactBroker::Verifications::Service).to receive(:search_for_latest).and_return(verification) - allow(PactBroker.configuration).to receive(:show_webhook_response?).and_return('foo') - allow(execution_configuration).to receive(:with_failure_log_message).and_return(execution_configuration) - end - - subject { Service.test_execution(webhook, event_context, execution_configuration) } - - it "searches for the latest matching pact" do - expect(PactBroker::Pacts::Service).to receive(:search_for_latest_pact).with(consumer_name: 'consumer', provider_name: 'provider') - subject - end - - it "returns the result" do - expect(subject).to be result - end - - context "when the trigger is not for a verification" do - it "executes the webhook with the pact" do - expect(webhook).to receive(:execute).with(pact, nil, event_context.merge(event_name: "test"), execution_configuration_hash) - subject - end - end - - context "when a pact cannot be found" do - let(:pact) { nil } - - it "executes the webhook with a placeholder pact" do - expect(webhook).to receive(:execute).with(an_instance_of(PactBroker::Pacts::PlaceholderPact), anything, anything, anything) - subject - end - end - - context "when the trigger is for a verification publication" do - let(:trigger_on_verification) { true } - - it "searches for the latest matching verification" do - expect(PactBroker::Verifications::Service).to receive(:search_for_latest).with('consumer', 'provider') - subject - end - - it "executes the webhook with the pact and the verification" do - expect(webhook).to receive(:execute).with(pact, verification, event_context.merge(event_name: "test"), execution_configuration_hash) - subject - end - - context "when a verification cannot be found" do - let(:verification) { nil } - - it "executes the webhook with a placeholder verification" do - expect(webhook).to receive(:execute).with(anything, an_instance_of(PactBroker::Verifications::PlaceholderVerification), anything, anything) - subject - end - end - end - end - - describe ".trigger_webhooks integration test", job: true do - let!(:http_request) do - stub_request(:get, "http://example.org"). - to_return(:status => 200) - end - - let(:events) { [{ name: PactBroker::Webhooks::WebhookEvent::DEFAULT_EVENT_NAME }] } - let(:webhook_execution_configuration) do - PactBroker::Webhooks::ExecutionConfiguration.new - .with_webhook_context(base_url: 'http://example.org') - .with_show_response(true) - end - let(:event_context) { { some: "data", base_url: "http://example.org" }} - let(:options) do - { - database_connector: database_connector, - webhook_execution_configuration: webhook_execution_configuration - } - end - let(:logging_options) { { show_response: true } } - let(:database_connector) { ->(&block) { block.call } } - let(:pact) do - td.create_consumer - .create_provider - .create_consumer_version - .create_pact - .create_verification - .create_webhook(method: 'GET', url: 'http://example.org', events: events) - .and_return(:pact) - end - - subject { PactBroker::Webhooks::Service.trigger_webhooks(pact, td.verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, event_context, options) } - - it "executes the HTTP request of the webhook" do - subject - expect(http_request).to have_been_made - end - - it "executes the webhook with the correct options" do - expect_any_instance_of(PactBroker::Domain::WebhookRequest).to receive(:execute).and_call_original - subject - end - - it "saves the triggered webhook" do - expect { subject }.to change { PactBroker::Webhooks::TriggeredWebhook.count }.by(1) - end - - it "saves the execution" do - expect { subject }.to change { PactBroker::Webhooks::Execution.count }.by(1) - end - - it "marks the triggered webhook as a success" do - subject - expect(TriggeredWebhook.first.status).to eq TriggeredWebhook::STATUS_SUCCESS - end - end - describe "parameters" do subject { Service.parameters } diff --git a/spec/lib/pact_broker/webhooks/trigger_service_spec.rb b/spec/lib/pact_broker/webhooks/trigger_service_spec.rb index f5ab73446..1472826f8 100644 --- a/spec/lib/pact_broker/webhooks/trigger_service_spec.rb +++ b/spec/lib/pact_broker/webhooks/trigger_service_spec.rb @@ -7,7 +7,6 @@ module Webhooks let(:pact_version_sha) { "111" } let(:pact_repository) { double("pact_repository", find_previous_pacts: previous_pacts) } let(:pact_service) { class_double("PactBroker::Pacts::Service").as_stubbed_const } - let(:webhook_service) { double("webhook_service", trigger_webhooks: nil) } let(:previous_pact) { double("previous_pact", pact_version_sha: previous_pact_version_sha) } let(:previous_pact_version_sha) { "111" } let(:previous_pacts) { { untagged: previous_pact } } @@ -18,212 +17,325 @@ module Webhooks before do allow(TriggerService).to receive(:pact_repository).and_return(pact_repository) allow(TriggerService).to receive(:pact_service).and_return(pact_service) - allow(TriggerService).to receive(:webhook_service).and_return(webhook_service) allow(TriggerService).to receive(:logger).and_return(logger) end + def find_result_with_message_including(message) + subject.find { | result | result.message.include?(message) } + end + shared_examples_for "triggering a contract_published event" do it "triggers a contract_published webhook" do - expect(webhook_service).to receive(:trigger_webhooks).with(pact, nil, PactBroker::Webhooks::WebhookEvent::CONTRACT_PUBLISHED, event_context, webhook_options) + expect(TriggerService).to receive(:trigger_webhooks).with(pact, nil, PactBroker::Webhooks::WebhookEvent::CONTRACT_PUBLISHED, event_context, webhook_options) subject end end shared_examples_for "triggering a contract_content_changed event" do it "triggers a contract_content_changed webhook" do - expect(webhook_service).to receive(:trigger_webhooks).with(pact, nil, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, event_context, webhook_options) + expect(TriggerService).to receive(:trigger_webhooks).with(pact, nil, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, event_context, webhook_options) subject end end shared_examples_for "not triggering a contract_content_changed event" do it "does not trigger a contract_content_changed webhook" do - expect(webhook_service).to_not receive(:trigger_webhooks).with(anything, anything, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, event_context, anything) + expect(TriggerService).to_not receive(:trigger_webhooks).with(anything, anything, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, event_context, anything) subject end end + end - describe "#trigger_webhooks_for_new_pact" do - subject { TriggerService.trigger_webhooks_for_new_pact(pact, event_context, webhook_options) } + describe TriggerService do + describe ".create_triggered_webhooks_for_event" do + before do + allow(TriggerService).to receive(:webhook_repository).and_return(webhook_repository) + allow(TriggerService).to receive(:logger).and_return(logger) + allow(TriggerService).to receive(:pact_service).and_return(pact_service) + end + let(:pact_service) { class_double("PactBroker::Pacts::Service").as_stubbed_const } + let(:logger) { double('logger').as_null_object } + let(:verification) { instance_double(PactBroker::Domain::Verification)} + let(:pact) { instance_double(PactBroker::Domain::Pact, consumer: consumer, provider: provider, consumer_version: consumer_version)} + let(:consumer_version) { PactBroker::Domain::Version.new(number: '1.2.3') } + let(:consumer) { PactBroker::Domain::Pacticipant.new(name: 'Consumer') } + let(:provider) { PactBroker::Domain::Pacticipant.new(name: 'Provider') } + let(:webhooks) { [webhook]} + let(:webhook) do + instance_double(PactBroker::Domain::Webhook, description: 'description', uuid: '1244', expand_currently_deployed_provider_versions?: expand_currently_deployed) + end + let(:expand_currently_deployed) { false } + let(:triggered_webhook) { instance_double(PactBroker::Webhooks::TriggeredWebhook) } + let(:event_name) { PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED } + let(:event_context) { { some: "data" } } + let(:expected_event_context) { { some: "data", event_name: PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED } } + let(:webhook_repository) { instance_double(Repository, create_triggered_webhook: triggered_webhook, find_by_consumer_and_or_provider_and_event_name: webhooks) } - context "when no previous untagged pact exists" do - let(:previous_pact) { nil } + subject { TriggerService.create_triggered_webhooks_for_event(pact, verification, event_name, event_context) } - include_examples "triggering a contract_published event" - include_examples "triggering a contract_content_changed event" + it "finds the webhooks" do + expect(webhook_repository).to receive(:find_by_consumer_and_or_provider_and_event_name).with(consumer, provider, PactBroker::Webhooks::WebhookEvent::DEFAULT_EVENT_NAME) + subject + end - it "logs the reason why it triggered the contract_content_changed event" do - expect(logger).to receive(:info).with(/first time untagged pact published/) + context "when webhooks are found" do + it "merges the event name in the webhook context" do + expect(webhook_repository).to receive(:create_triggered_webhook).with(anything, anything, anything, anything, anything, anything, hash_including(event_name: PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED)) subject end - end - - context "when a previous untagged pact exists and the sha is different" do - let(:previous_pact_version_sha) { "222" } - let(:previous_pacts) { { :untagged => previous_pact } } + context "when there should be a webhook triggered for each currently deployed version" do + before do + allow(TriggerService).to receive(:deployed_version_service).and_return(deployed_version_service) + allow(deployed_version_service).to receive(:find_currently_deployed_versions_for_pacticipant).and_return(currently_deployed_versions) + end + let(:expand_currently_deployed) { true } + let(:deployed_version_service) { class_double("PactBroker::Deployments::DeployedVersionService").as_stubbed_const } + let(:currently_deployed_version_1) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "1") } + let(:currently_deployed_version_2) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "2") } + let(:currently_deployed_versions) { [currently_deployed_version_1, currently_deployed_version_2] } + + it "creates a triggered webhook for each currently deployed version" do + expect(webhook_repository).to receive(:create_triggered_webhook).with(anything, webhook, pact, verification, TriggerService::RESOURCE_CREATION, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context.merge(currently_deployed_provider_version_number: "1")) + expect(webhook_repository).to receive(:create_triggered_webhook).with(anything, webhook, pact, verification, TriggerService::RESOURCE_CREATION, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context.merge(currently_deployed_provider_version_number: "2")) + subject + end - include_examples "triggering a contract_published event" - include_examples "triggering a contract_content_changed event" + context "when the same version is deployed to multiple environments" do + let(:currently_deployed_version_2) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "1") } - it "logs the reason why it triggered the contract_content_changed event" do - expect(logger).to receive(:info).with(/pact content has changed since previous untagged version/) - subject + it "only creates one triggered webhook" do + expect(webhook_repository).to receive(:create_triggered_webhook).with(anything, anything, anything, anything, anything, anything, expected_event_context.merge(currently_deployed_provider_version_number: "1")) + subject + end + end end - end - - context "when a previous untagged pact exists and the sha is the same" do - let(:previous_pact_version_sha) { pact_version_sha } - let(:previous_pacts) { { :untagged => previous_pact } } + context "when there should be a webhook triggered for each consumer version that had a pact verified" do + # let(:triggered_webhooks) { [instance_double(TriggeredWebhook, event_context: { some: 'context'}, webhook: instance_double(Webhook, uuid: "webhook-uuid"))]} - include_examples "triggering a contract_published event" - include_examples "not triggering a contract_content_changed event" - end + before do + allow(pact_service).to receive(:find_pact).and_return(pact_for_consumer_version_1, pact_for_consumer_version_2) + # allow(pact_service).to receive(:find_pact).with(hash_including(consumer_version_number: "2")).and_return(pact_for_consumer_version_2) + end + let(:event_name) { PactBroker::Webhooks::WebhookEvent::VERIFICATION_SUCCEEDED } + let(:pact) { instance_double(PactBroker::Domain::Pact, provider_name: provider.name, consumer_name: consumer.name, consumer: consumer, provider: provider, consumer_version: consumer_version)} + let(:consumer_version) { PactBroker::Domain::Version.new(number: '1.2.3') } + let(:consumer) { PactBroker::Domain::Pacticipant.new(name: 'Consumer') } + let(:provider) { PactBroker::Domain::Pacticipant.new(name: 'Provider') } + + # See lib/pact_broker/pacts/metadata.rb build_metadata_for_pact_for_verification + let(:selector_1) { { latest: true, consumer_version_number: "1", tag: "prod" } } + let(:selector_2) { { latest: true, consumer_version_number: "1", tag: "main" } } + let(:selector_3) { { latest: true, consumer_version_number: "2", tag: "feat/2" } } + let(:event_context) do + { + consumer_version_selectors: [selector_1, selector_2, selector_3], + other: "foo" + } + end + let(:expected_event_context_1) { { event_name: event_name, consumer_version_number: "1", consumer_version_tags: ["prod", "main"], other: "foo" } } + let(:expected_event_context_2) { { event_name: event_name, consumer_version_number: "2", consumer_version_tags: ["feat/2"], other: "foo" } } + let(:pact_for_consumer_version_1) { double('pact_for_consumer_version_1') } + let(:pact_for_consumer_version_2) { double('pact_for_consumer_version_2') } + + it "finds the pact publication for each consumer version number" do + expect(pact_service).to receive(:find_pact).with(hash_including(consumer_version_number: "1")).and_return(pact_for_consumer_version_1) + expect(pact_service).to receive(:find_pact).with(hash_including(consumer_version_number: "2")).and_return(pact_for_consumer_version_2) + subject + end - context "when no previous pact with a given tag exists" do - let(:previous_pact) { nil } - let(:previous_pacts) { { "dev" => previous_pact } } + context "when there are consumer_version_selectors in the event_context" do + it "creates a triggered webhook for each consumer version (ie. commit)" do + expect(webhook_repository).to receive(:create_triggered_webhook).with(anything, webhook, pact_for_consumer_version_1, verification, TriggerService::RESOURCE_CREATION, event_name, expected_event_context_1) + expect(webhook_repository).to receive(:create_triggered_webhook).with(anything, webhook, pact_for_consumer_version_2, verification, TriggerService::RESOURCE_CREATION, event_name, expected_event_context_2) + subject + end + end - include_examples "triggering a contract_published event" - include_examples "triggering a contract_content_changed event" + context "when there are no consumer_version_selectors" do + let(:event_context) { { some: "data" } } - it "logs the reason why it triggered the contract_content_changed event" do - expect(logger).to receive(:info).with(/first time pact published with consumer version tagged dev/) - subject + it "passes through the event context and only makes one triggered webhook" do + expect(webhook_repository).to receive(:create_triggered_webhook).with(anything, webhook, pact, verification, TriggerService::RESOURCE_CREATION, PactBroker::Webhooks::WebhookEvent::VERIFICATION_SUCCEEDED, event_context.merge(event_name: event_name)) + subject + end + end end end + end - context "when a previous pact with a given tag exists and the sha is different" do - let(:previous_pact_version_sha) { "222" } - let(:previous_pacts) { { "dev" => previous_pact } } - - include_examples "triggering a contract_published event" - include_examples "triggering a contract_content_changed event" + describe ".schedule_webhooks" do + let(:options) do + { database_connector: double('database_connector'), + webhook_execution_configuration: webhook_execution_configuration, + logging_options: {} + } end - - context "when a previous pact with a given tag exists and the sha is the same" do - let(:previous_pact_version_sha) { pact_version_sha } - let(:previous_pacts) { { "dev" => previous_pact } } - - include_examples "triggering a contract_published event" - include_examples "not triggering a contract_content_changed event" + let(:triggered_webhook) { instance_double(PactBroker::Webhooks::TriggeredWebhook, webhook: webhook, event_context: {}) } + let(:triggered_webhooks) { [triggered_webhook] } + let(:webhook_execution_configuration) { double('webhook_execution_configuration', webhook_context: webhook_context) } + let(:webhook_context) { { base_url: "http://example.org" } } + let(:webhook) do + instance_double(PactBroker::Domain::Webhook, description: 'description', uuid: '1244') end - end - describe "#trigger_webhooks_for_updated_pact" do - let(:existing_pact) do - double('existing_pact', - pact_version_sha: existing_pact_version_sha, - consumer_version_number: "1.2.3" - ) - end - let(:existing_pact_version_sha) { pact_version_sha } + subject { TriggerService.schedule_webhooks(triggered_webhooks, options) } - subject { TriggerService.trigger_webhooks_for_updated_pact(existing_pact, pact, event_context, webhook_options) } + context "when there is a scheduling error", job: true do + before do + allow(TriggerService).to receive(:logger).and_return(logger) + end - context "when the pact version sha of the previous revision is different" do - let(:existing_pact_version_sha) { "456" } + let(:logger) { double('logger').as_null_object } - include_examples "triggering a contract_published event" - include_examples "triggering a contract_content_changed event" + before do + allow(Job).to receive(:perform_in).and_raise("an error") + end - it "logs the reason why it triggered the contract_content_changed event" do - expect(logger).to receive(:info).with(/version 1.2.3 has been updated with new content/) + it "logs the error" do + allow(TriggerService.logger).to receive(:warn) + expect(TriggerService.logger).to receive(:warn).with(/Error scheduling/, StandardError) subject end end + end - context "when the pact version sha of the previous revision is not different, not sure if we'll even get this far if it hasn't changed, but just in case..." do - include_examples "triggering a contract_published event" - include_examples "not triggering a contract_content_changed event" + describe ".test_execution" do + let(:webhook) do + instance_double(PactBroker::Domain::Webhook, + trigger_on_provider_verification_published?: trigger_on_verification, + consumer_name: 'consumer', + provider_name: 'provider', + execute: result + ) end - end + let(:pact) { instance_double(PactBroker::Domain::Pact) } + let(:verification) { instance_double(PactBroker::Domain::Verification) } + let(:trigger_on_verification) { false } + let(:result) { double('result') } + let(:execution_configuration) do + instance_double(PactBroker::Webhooks::ExecutionConfiguration, to_hash: execution_configuration_hash) + end + let(:execution_configuration_hash) { { the: 'options' } } + let(:event_context) { { some: "data" } } - describe "#trigger_webhooks_for_verification_results_publication" do before do - allow(pact_service).to receive(:find_pact).and_return(pact_for_consumer_version_1, pact_for_consumer_version_2) - # allow(pact_service).to receive(:find_pact).with(hash_including(consumer_version_number: "2")).and_return(pact_for_consumer_version_2) - end - let(:verification) { double("verification", success: success) } - let(:success) { true } - # See lib/pact_broker/pacts/metadata.rb build_metadata_for_pact_for_verification - let(:selector_1) { { latest: true, consumer_version_number: "1", tag: "prod" } } - let(:selector_2) { { latest: true, consumer_version_number: "1", tag: "main" } } - let(:selector_3) { { latest: true, consumer_version_number: "2", tag: "feat/2" } } - let(:event_context) do - { - consumer_version_selectors: [selector_1, selector_2, selector_3], - other: "foo" - } + allow(PactBroker::Pacts::Service).to receive(:search_for_latest_pact).and_return(pact) + allow(PactBroker::Verifications::Service).to receive(:search_for_latest).and_return(verification) + allow(PactBroker.configuration).to receive(:show_webhook_response?).and_return('foo') + allow(execution_configuration).to receive(:with_failure_log_message).and_return(execution_configuration) end - let(:expected_event_context_1) { { consumer_version_number: "1", consumer_version_tags: ["prod", "main"], other: "foo" } } - let(:expected_event_context_2) { { consumer_version_number: "2", consumer_version_tags: ["feat/2"], other: "foo" } } - let(:pact_for_consumer_version_1) { double('pact_for_consumer_version_1') } - let(:pact_for_consumer_version_2) { double('pact_for_consumer_version_2') } - subject { TriggerService.trigger_webhooks_for_verification_results_publication(pact, verification, event_context, webhook_options) } + subject { TriggerService.test_execution(webhook, event_context, execution_configuration) } - it "find the pact publication for each consumer version number" do - expect(pact_service).to receive(:find_pact).with(hash_including(consumer_version_number: "1")).and_return(pact_for_consumer_version_1) - expect(pact_service).to receive(:find_pact).with(hash_including(consumer_version_number: "2")).and_return(pact_for_consumer_version_2) + it "searches for the latest matching pact" do + expect(PactBroker::Pacts::Service).to receive(:search_for_latest_pact).with(consumer_name: 'consumer', provider_name: 'provider') subject end - context "when the verification is successful" do - context "when there are consumer_version_selectors in the event_context" do - it "triggers a provider_verification_succeeded webhook for each consumer version (ie. commit)" do - expect(webhook_service).to receive(:trigger_webhooks).with(pact_for_consumer_version_1, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_SUCCEEDED, expected_event_context_1, webhook_options) - expect(webhook_service).to receive(:trigger_webhooks).with(pact_for_consumer_version_2, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_SUCCEEDED, expected_event_context_2, webhook_options) - subject - end + it "returns the result" do + expect(subject).to be result + end - it "triggers a provider_verification_published webhook for each consumer version (ie. commit)" do - expect(webhook_service).to receive(:trigger_webhooks).with(pact_for_consumer_version_1, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_PUBLISHED, expected_event_context_1, webhook_options) - expect(webhook_service).to receive(:trigger_webhooks).with(pact_for_consumer_version_2, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_PUBLISHED, expected_event_context_2, webhook_options) - subject - end + context "when the trigger is not for a verification" do + it "executes the webhook with the pact" do + expect(webhook).to receive(:execute).with(pact, nil, event_context.merge(event_name: "test"), execution_configuration_hash) + subject end + end - context "when there are no consumer_version_selectors" do - let(:event_context) { { some: "data" } } + context "when a pact cannot be found" do + let(:pact) { nil } - it "passes through the event context" do - expect(webhook_service).to receive(:trigger_webhooks).with(pact, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_SUCCEEDED, event_context, webhook_options) - expect(webhook_service).to receive(:trigger_webhooks).with(pact, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_PUBLISHED, event_context, webhook_options) - subject - end + it "executes the webhook with a placeholder pact" do + expect(webhook).to receive(:execute).with(an_instance_of(PactBroker::Pacts::PlaceholderPact), anything, anything, anything) + subject end end - context "when the verification is not successful" do - let(:success) { false } + context "when the trigger is for a verification publication" do + let(:trigger_on_verification) { true } - context "when there are consumer_version_selectors in the event_context" do - it "triggers a provider_verification_failed webhook for each consumer version (ie. commit)" do - expect(webhook_service).to receive(:trigger_webhooks).with(pact_for_consumer_version_1, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_FAILED, expected_event_context_1, webhook_options) - expect(webhook_service).to receive(:trigger_webhooks).with(pact_for_consumer_version_2, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_FAILED, expected_event_context_2, webhook_options) - subject - end + it "searches for the latest matching verification" do + expect(PactBroker::Verifications::Service).to receive(:search_for_latest).with('consumer', 'provider') + subject + end - it "triggeres a provider_verification_published webhook for each consumer version (ie. commit)" do - expect(webhook_service).to receive(:trigger_webhooks).with(pact_for_consumer_version_1, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_PUBLISHED, expected_event_context_1, webhook_options) - expect(webhook_service).to receive(:trigger_webhooks).with(pact_for_consumer_version_2, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_PUBLISHED, expected_event_context_2, webhook_options) - subject - end + it "executes the webhook with the pact and the verification" do + expect(webhook).to receive(:execute).with(pact, verification, event_context.merge(event_name: "test"), execution_configuration_hash) + subject end - context "when there are no consumer_version_selectors" do - let(:event_context) { { some: "data" } } + context "when a verification cannot be found" do + let(:verification) { nil } - it "passes through the event context" do - expect(webhook_service).to receive(:trigger_webhooks).with(pact, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_FAILED, event_context, webhook_options) - expect(webhook_service).to receive(:trigger_webhooks).with(pact, verification, PactBroker::Webhooks::WebhookEvent::VERIFICATION_PUBLISHED, event_context, webhook_options) + it "executes the webhook with a placeholder verification" do + expect(webhook).to receive(:execute).with(anything, an_instance_of(PactBroker::Verifications::PlaceholderVerification), anything, anything) subject end end end end + + describe ".trigger_webhooks integration test", job: true do + let!(:http_request) do + stub_request(:get, "http://example.org"). + to_return(:status => 200) + end + + let(:events) { [{ name: PactBroker::Webhooks::WebhookEvent::DEFAULT_EVENT_NAME }] } + let(:webhook_execution_configuration) do + PactBroker::Webhooks::ExecutionConfiguration.new + .with_webhook_context(base_url: 'http://example.org') + .with_show_response(true) + end + let(:event_context) { { some: "data", base_url: "http://example.org" }} + let(:options) do + { + database_connector: database_connector, + webhook_execution_configuration: webhook_execution_configuration + } + end + let(:logging_options) { { show_response: true } } + let(:database_connector) { ->(&block) { block.call } } + let(:pact) do + td.create_consumer + .create_provider + .create_consumer_version + .create_pact + .create_verification + .create_webhook(method: 'GET', url: 'http://example.org', events: events) + .and_return(:pact) + end + + let(:triggered_webhooks) { PactBroker::Webhooks::TriggerService.create_triggered_webhooks_for_event(pact, td.verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, event_context) } + + subject { PactBroker::Webhooks::TriggerService.schedule_webhooks(triggered_webhooks, options) } + + it "executes the HTTP request of the webhook" do + subject + expect(http_request).to have_been_made + end + + it "executes the webhook with the correct options" do + expect_any_instance_of(PactBroker::Domain::WebhookRequest).to receive(:execute).and_call_original + subject + end + + it "saves the triggered webhook" do + expect { subject }.to change { PactBroker::Webhooks::TriggeredWebhook.count }.by(1) + end + + it "saves the execution" do + expect { subject }.to change { PactBroker::Webhooks::Execution.count }.by(1) + end + + it "marks the triggered webhook as a success" do + subject + expect(TriggeredWebhook.first.status).to eq TriggeredWebhook::STATUS_SUCCESS + end + end end end end diff --git a/spec/migrations/23_pact_versions_spec.rb b/spec/migrations/23_pact_versions_spec.rb index cad2a3d96..06b4b543b 100644 --- a/spec/migrations/23_pact_versions_spec.rb +++ b/spec/migrations/23_pact_versions_spec.rb @@ -71,8 +71,6 @@ provider_name: provider[:name], consumer_version_number: '1.2.3', json_content: load_fixture('a_consumer-a_provider.json') - },{ - webhook_execution_configuration: PactBroker::Webhooks::ExecutionConfiguration.new } ) end