238 lines
7.7 KiB
Ruby
238 lines
7.7 KiB
Ruby
|
# 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
|