summaryrefslogtreecommitdiff
path: root/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb')
-rw-r--r--spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb272
1 files changed, 272 insertions, 0 deletions
diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
new file mode 100644
index 00000000000..122774a9028
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
@@ -0,0 +1,272 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
+ shared_examples 'is tolerant of invalid records' do
+ specify do
+ hook.url = nil
+
+ expect(hook).to be_invalid
+ run_expectation
+ end
+ end
+
+ describe '.executable/.disabled', :freeze_time do
+ let!(:not_executable) do
+ [
+ [4, nil], # Exceeded the grace period, set by #fail!
+ [4, 1.second.from_now], # Exceeded the grace period, set by #backoff!
+ [4, Time.current] # Exceeded the grace period, set by #backoff!, edge-case
+ ].map do |(recent_failures, disabled_until)|
+ create(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
+disabled_until: disabled_until)
+ end
+ end
+
+ let!(:executables) do
+ expired = 1.second.ago
+ borderline = Time.current
+ suspended = 1.second.from_now
+
+ [
+ # Most of these are impossible states, but are included for completeness
+ [0, nil],
+ [1, nil],
+ [3, nil],
+ [4, expired],
+
+ # Impossible cases:
+ [3, suspended],
+ [3, expired],
+ [3, borderline],
+ [1, suspended],
+ [1, expired],
+ [1, borderline],
+ [0, borderline],
+ [0, suspended],
+ [0, expired]
+ ].map do |(recent_failures, disabled_until)|
+ create(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
+disabled_until: disabled_until)
+ end
+ end
+
+ it 'finds the correct set of project hooks' do
+ expect(find_hooks.executable).to match_array executables
+ expect(find_hooks.executable).to all(be_executable)
+
+ # As expected, and consistent
+ expect(find_hooks.disabled).to match_array not_executable
+ expect(find_hooks.disabled.map(&:executable?)).not_to include(true)
+
+ # Nothing is missing
+ expect(find_hooks.executable.to_a + find_hooks.disabled.to_a).to match_array(find_hooks.to_a)
+ end
+ end
+
+ describe '#executable?', :freeze_time do
+ let(:web_hook) { create(hook_factory, **default_factory_arguments) }
+
+ where(:recent_failures, :not_until, :executable) do
+ [
+ [0, :not_set, true],
+ [0, :past, true],
+ [0, :future, true],
+ [0, :now, true],
+ [1, :not_set, true],
+ [1, :past, true],
+ [1, :future, true],
+ [3, :not_set, true],
+ [3, :past, true],
+ [3, :future, true],
+ [4, :not_set, false],
+ [4, :past, true], # expired suspension
+ [4, :now, false], # active suspension
+ [4, :future, false] # active suspension
+ ]
+ end
+
+ with_them do
+ # Phasing means we cannot put these values in the where block,
+ # which is not subject to the frozen time context.
+ let(:disabled_until) do
+ case not_until
+ when :not_set
+ nil
+ when :past
+ 1.minute.ago
+ when :future
+ 1.minute.from_now
+ when :now
+ Time.current
+ end
+ end
+
+ before do
+ web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
+ end
+
+ it 'has the correct state' do
+ expect(web_hook.executable?).to eq(executable)
+ end
+ end
+ end
+
+ describe '#enable!' do
+ it 'makes a hook executable if it was marked as failed' do
+ hook.recent_failures = 1000
+
+ expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
+ end
+
+ it 'makes a hook executable if it is currently backed off' do
+ hook.recent_failures = 1000
+ hook.disabled_until = 1.hour.from_now
+
+ expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
+ end
+
+ it 'does not update hooks unless necessary' do
+ hook
+
+ sql_count = ActiveRecord::QueryRecorder.new { hook.enable! }.count
+
+ expect(sql_count).to eq(0)
+ end
+
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ hook.recent_failures = 1000
+
+ expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
+ end
+ end
+ end
+
+ describe '#backoff!' do
+ context 'when we have not backed off before' do
+ it 'does not disable the hook' do
+ expect { hook.backoff! }.not_to change { hook.executable? }.from(true)
+ end
+ end
+
+ context 'when we have exhausted the grace period' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ end
+
+ context 'when the hook is permanently disabled' do
+ before do
+ allow(hook).to receive(:permanently_disabled?).and_return(true)
+ end
+
+ it 'does not set disabled_until' do
+ expect { hook.backoff! }.not_to change { hook.disabled_until }
+ end
+
+ it 'does not increment the backoff count' do
+ expect { hook.backoff! }.not_to change { hook.backoff_count }
+ end
+ end
+
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ expect { hook.backoff! }.to change { hook.backoff_count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe '#failed!' do
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ expect { hook.failed! }.to change { hook.recent_failures }.by(1)
+ end
+ end
+ end
+
+ describe '#disable!' do
+ it 'disables a hook' do
+ expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
+ end
+
+ it 'does nothing if the hook is already disabled' do
+ allow(hook).to receive(:permanently_disabled?).and_return(true)
+
+ sql_count = ActiveRecord::QueryRecorder.new { hook.disable! }.count
+
+ expect(sql_count).to eq(0)
+ end
+
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
+ end
+ end
+ end
+
+ describe '#temporarily_disabled?' do
+ it 'is false when not temporarily disabled' do
+ expect(hook).not_to be_temporarily_disabled
+ end
+
+ it 'allows FAILURE_THRESHOLD initial failures before we back-off' do
+ WebHook::FAILURE_THRESHOLD.times do
+ hook.backoff!
+ expect(hook).not_to be_temporarily_disabled
+ end
+
+ hook.backoff!
+ expect(hook).to be_temporarily_disabled
+ end
+
+ context 'when hook has been told to back off' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ hook.backoff!
+ end
+
+ it 'is true' do
+ expect(hook).to be_temporarily_disabled
+ end
+ end
+ end
+
+ describe '#permanently_disabled?' do
+ it 'is false when not disabled' do
+ expect(hook).not_to be_permanently_disabled
+ end
+
+ context 'when hook has been disabled' do
+ before do
+ hook.disable!
+ end
+
+ it 'is true' do
+ expect(hook).to be_permanently_disabled
+ end
+ end
+ end
+
+ describe '#alert_status' do
+ subject(:status) { hook.alert_status }
+
+ it { is_expected.to eq :executable }
+
+ context 'when hook has been disabled' do
+ before do
+ hook.disable!
+ end
+
+ it { is_expected.to eq :disabled }
+ end
+
+ context 'when hook has been backed off' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.disabled_until = 1.hour.from_now
+ end
+
+ it { is_expected.to eq :temporarily_disabled }
+ end
+ end
+end