require "pact_broker/webhooks/trigger_service" require "pact_broker/versions/selectors" require "pact_broker/versions/selector" module PactBroker module Webhooks describe TriggerService do let(:pact) { double("pact", pact_version_sha: pact_version_sha, consumer_name: "foo", provider_name: "bar") } 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(:previous_pact) { double("previous_pact", pact_version_sha: previous_pact_version_sha) } let(:previous_pact_version_sha) { "111" } let(:previous_pacts) { { untagged: previous_pact } } let(:logger) { double("logger").as_null_object } let(:event_context) { { some: "data" } } let(:webhook_options) { { the: "options"} } 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(: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(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(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(TriggerService).to_not receive(:trigger_webhooks).with(anything, anything, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, event_context, anything) subject end end end 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", main_branch: provider_main_branch) } let(:provider_main_branch) { "main" } 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(:trigger_on_contract_requiring_verification_published) { 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: event_name } } let(:webhook_repository) { instance_double(Repository, create_triggered_webhook: triggered_webhook, find_webhooks_to_trigger: webhooks) } subject { TriggerService.create_triggered_webhooks_for_event(pact, verification, event_name, event_context) } it "finds the webhooks" do expect(webhook_repository).to receive(:find_webhooks_to_trigger).with(consumer: consumer, provider: provider, event_name: PactBroker::Webhooks::WebhookEvent::DEFAULT_EVENT_NAME) subject end 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 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 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 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 context "when the event is contract_requiring_verification_published" do before do allow(TriggerService).to receive(:verification_service).and_return(verification_service) allow(verification_service).to receive(:calculate_required_verifications_for_pact).and_return(required_verifications) end let(:event_name) { PactBroker::Webhooks::WebhookEvent::CONTRACT_REQUIRING_VERIFICATION_PUBLISHED } let(:verification_service) { class_double("PactBroker::Verifications::Service").as_stubbed_const } let(:required_verifications) { [required_verification] } let(:required_verification) do instance_double("PactBroker::Verifications::RequiredVerification", provider_version: double("version", number: "1", branch_versions: branch_versions), provider_version_selectors: provider_version_selectors, provider_version_descriptions: ["foo"] ) end let(:provider_version_selectors) { [instance_double("PactBroker::Versions::ResolvedSelector", latest_for_main_branch?: latest_for_main_branch, resolved_branch_name: "the-main-branch")] } let(:latest_for_main_branch) { true } let(:branch_versions) { [instance_double("PactBroker::Versions::BranchVersion", branch_name: "main")] } it "creates a triggered webhook for each required verification" do expect(webhook_repository).to receive(:create_triggered_webhook).with( anything, webhook, pact, verification, TriggerService::RESOURCE_CREATION, PactBroker::Webhooks::WebhookEvent::CONTRACT_REQUIRING_VERIFICATION_PUBLISHED, expected_event_context.merge(provider_version_number: "1", provider_version_branch: "the-main-branch", provider_version_descriptions: ["foo"]) ) subject end it "returns the triggered webhooks" do expect(subject.size).to eq 1 end context "when a version is required because it was the main branch" do it "uses the main branch" do expect(webhook_repository).to receive(:create_triggered_webhook).with( anything, anything, anything, anything, anything, anything, hash_including(provider_version_branch: "the-main-branch") ) subject end end context "when a version was required but not because it was for the main branch" do let(:latest_for_main_branch) { false } context "when there are multiple branches" do let(:branch_versions) do [ instance_double("PactBroker::Versions::BranchVersion", branch_name: "foo"), instance_double("PactBroker::Versions::BranchVersion", branch_name: "bar") ] end it "uses the last branch" do expect(webhook_repository).to receive(:create_triggered_webhook).with( anything, anything, anything, anything, anything, anything, hash_including(provider_version_branch: "bar") ) subject end end context "when there are no branches" do let(:branch_versions) { [] } it "uses nil" do expect(webhook_repository).to receive(:create_triggered_webhook).with( anything, anything, anything, anything, anything, anything, hash_including(provider_version_branch: nil) ) subject end end end context "when there are no required verifications" do let(:required_verifications) { [] } it "does not create any triggered webhooks" do expect(webhook_repository).to_not receive(:create_triggered_webhook) subject end it "returns an empty array" do expect(subject).to eq [] end end end 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"))]} 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 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 context "when there are no consumer_version_selectors" do let(:event_context) { { some: "data" } } 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 describe ".schedule_webhooks" do let(:options) do { database_connector: double("database_connector"), webhook_execution_configuration: webhook_execution_configuration, logging_options: {} } end 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 subject { TriggerService.schedule_webhooks(triggered_webhooks, options) } context "when there is a scheduling error", job: true do before do allow(TriggerService).to receive(:logger).and_return(logger) end let(:logger) { double("logger").as_null_object } before do allow(Job).to receive(:perform_in).and_raise("an error") end 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 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 { TriggerService.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_retry_schedule([10, 60, 120, 300, 600, 1200]) .with_http_success_codes([200]) .with_user_agent("Pact Broker") .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 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 it "does not call the PactBroker.configuration as it will have been reset after the end of the request" do expect(PactBroker).to_not receive(:configuration) subject end end end end end