summaryrefslogtreecommitdiff
path: root/spec/services/web_hooks/log_execution_service_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/services/web_hooks/log_execution_service_spec.rb')
-rw-r--r--spec/services/web_hooks/log_execution_service_spec.rb237
1 files changed, 237 insertions, 0 deletions
diff --git a/spec/services/web_hooks/log_execution_service_spec.rb b/spec/services/web_hooks/log_execution_service_spec.rb
new file mode 100644
index 00000000000..0ba0372b99d
--- /dev/null
+++ b/spec/services/web_hooks/log_execution_service_spec.rb
@@ -0,0 +1,237 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WebHooks::LogExecutionService do
+ include ExclusiveLeaseHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#execute' do
+ around do |example|
+ travel_to(Time.current) { example.run }
+ end
+
+ let_it_be_with_reload(:project_hook) { create(:project_hook) }
+
+ let(:response_category) { :ok }
+ let(:data) do
+ {
+ trigger: 'trigger_name',
+ url: 'https://example.com',
+ request_headers: { 'Header' => 'header value' },
+ request_data: { 'Request Data' => 'request data value' },
+ response_body: 'Response body',
+ response_status: '200',
+ execution_duration: 1.2,
+ internal_error_message: 'error message'
+ }
+ end
+
+ subject(:service) { described_class.new(hook: project_hook, log_data: data, response_category: response_category) }
+
+ it 'logs the data' do
+ expect { service.execute }.to change(::WebHookLog, :count).by(1)
+
+ expect(WebHookLog.recent.first).to have_attributes(data)
+ end
+
+ context 'obtaining an exclusive lease' do
+ let(:lease_key) { "web_hooks:update_hook_failure_state:#{project_hook.id}" }
+
+ it 'updates failure state using a lease that ensures fresh state is written' do
+ service = described_class.new(hook: project_hook, log_data: data, response_category: :error)
+ WebHook.find(project_hook.id).update!(backoff_count: 1)
+
+ lease = stub_exclusive_lease(lease_key, timeout: described_class::LOCK_TTL)
+
+ expect(lease).to receive(:try_obtain)
+ expect(lease).to receive(:cancel)
+ expect { service.execute }.to change { WebHook.find(project_hook.id).backoff_count }.to(2)
+ end
+
+ context 'when a lease cannot be obtained' do
+ where(:response_category, :executable, :needs_updating) do
+ :ok | true | false
+ :ok | false | true
+ :failed | true | true
+ :failed | false | false
+ :error | true | true
+ :error | false | false
+ end
+
+ with_them do
+ subject(:service) { described_class.new(hook: project_hook, log_data: data, response_category: response_category) }
+
+ before do
+ stub_exclusive_lease_taken(lease_key, timeout: described_class::LOCK_TTL)
+ allow(project_hook).to receive(:executable?).and_return(executable)
+ end
+
+ it 'raises an error if the hook needs to be updated' do
+ if needs_updating
+ expect { service.execute }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ else
+ expect { service.execute }.not_to raise_error
+ end
+ end
+ end
+ end
+ end
+
+ context 'when response_category is :ok' do
+ it 'does not increment the failure count' do
+ expect { service.execute }.not_to change(project_hook, :recent_failures)
+ end
+
+ it 'does not change the disabled_until attribute' do
+ expect { service.execute }.not_to change(project_hook, :disabled_until)
+ end
+
+ context 'when the hook had previously failed' do
+ before do
+ project_hook.update!(recent_failures: 2)
+ end
+
+ it 'resets the failure count' do
+ expect { service.execute }.to change(project_hook, :recent_failures).to(0)
+ end
+
+ it 'sends a message to AuthLogger if the hook as not previously enabled' do
+ project_hook.update!(recent_failures: ::WebHook::FAILURE_THRESHOLD + 1)
+
+ expect(Gitlab::AuthLogger).to receive(:info).with include(
+ message: 'WebHook change active_state',
+ # identification
+ hook_id: project_hook.id,
+ hook_type: project_hook.type,
+ project_id: project_hook.project_id,
+ group_id: nil,
+ # relevant data
+ prev_state: :permanently_disabled,
+ new_state: :enabled,
+ duration: 1.2,
+ response_status: '200',
+ recent_hook_failures: 0
+ )
+
+ service.execute
+ end
+ end
+ end
+
+ context 'when response_category is :failed' do
+ let(:response_category) { :failed }
+
+ before do
+ data[:response_status] = '400'
+ end
+
+ it 'increments the failure count' do
+ expect { service.execute }.to change(project_hook, :recent_failures).by(1)
+ end
+
+ it 'does not change the disabled_until attribute' do
+ expect { service.execute }.not_to change(project_hook, :disabled_until)
+ end
+
+ it 'does not allow the failure count to overflow' do
+ project_hook.update!(recent_failures: 32767)
+
+ expect { service.execute }.not_to change(project_hook, :recent_failures)
+ end
+
+ context 'when the web_hooks_disable_failed FF is disabled' do
+ before do
+ # Hook will only be executed if the flag is disabled.
+ stub_feature_flags(web_hooks_disable_failed: false)
+ end
+
+ it 'does not allow the failure count to overflow' do
+ project_hook.update!(recent_failures: 32767)
+
+ expect { service.execute }.not_to change(project_hook, :recent_failures)
+ end
+ end
+
+ it 'sends a message to AuthLogger if the state would change' do
+ project_hook.update!(recent_failures: ::WebHook::FAILURE_THRESHOLD)
+
+ expect(Gitlab::AuthLogger).to receive(:info).with include(
+ message: 'WebHook change active_state',
+ # identification
+ hook_id: project_hook.id,
+ hook_type: project_hook.type,
+ project_id: project_hook.project_id,
+ group_id: nil,
+ # relevant data
+ prev_state: :enabled,
+ new_state: :permanently_disabled,
+ duration: (be > 0),
+ response_status: data[:response_status],
+ recent_hook_failures: ::WebHook::FAILURE_THRESHOLD + 1
+ )
+
+ service.execute
+ end
+ end
+
+ context 'when response_category is :error' do
+ let(:response_category) { :error }
+
+ before do
+ data[:response_status] = '500'
+ end
+
+ it 'does not increment the failure count' do
+ expect { service.execute }.not_to change(project_hook, :recent_failures)
+ end
+
+ it 'backs off' do
+ expect { service.execute }.to change(project_hook, :disabled_until)
+ end
+
+ it 'increases the backoff count' do
+ expect { service.execute }.to change(project_hook, :backoff_count).by(1)
+ end
+
+ it 'sends a message to AuthLogger if the state would change' do
+ expect(Gitlab::AuthLogger).to receive(:info).with include(
+ message: 'WebHook change active_state',
+ # identification
+ hook_id: project_hook.id,
+ hook_type: project_hook.type,
+ project_id: project_hook.project_id,
+ group_id: nil,
+ # relevant data
+ prev_state: :enabled,
+ new_state: :temporarily_disabled,
+ duration: (be > 0),
+ response_status: data[:response_status],
+ recent_hook_failures: 0
+ )
+
+ service.execute
+ end
+
+ context 'when the previous cool-off was near the maximum' do
+ before do
+ project_hook.update!(disabled_until: 5.minutes.ago, backoff_count: 8)
+ end
+
+ it 'sets the disabled_until attribute' do
+ expect { service.execute }.to change(project_hook, :disabled_until).to(1.day.from_now)
+ end
+ end
+
+ context 'when we have backed-off many many times' do
+ before do
+ project_hook.update!(disabled_until: 5.minutes.ago, backoff_count: 365)
+ end
+
+ it 'sets the disabled_until attribute' do
+ expect { service.execute }.to change(project_hook, :disabled_until).to(1.day.from_now)
+ end
+ end
+ end
+ end
+end