debian-mirror-gitlab/spec/models/hooks/web_hook_spec.rb

578 lines
20 KiB
Ruby
Raw Normal View History

2019-07-07 11:18:12 +05:30
# frozen_string_literal: true
2014-09-02 18:07:02 +05:30
require 'spec_helper'
2023-01-10 11:22:00 +05:30
RSpec.describe WebHook, feature_category: :integrations do
2021-06-08 01:23:25 +05:30
include AfterNextHelpers
let_it_be(:project) { create(:project) }
let(:hook) { build(:project_hook, project: project) }
around do |example|
2021-11-11 11:23:49 +05:30
if example.metadata[:skip_freeze_time]
example.run
else
freeze_time { example.run }
end
2021-06-08 01:23:25 +05:30
end
2017-09-10 17:25:29 +05:30
describe 'associations' do
2019-12-21 20:55:43 +05:30
it { is_expected.to have_many(:web_hook_logs) }
2017-09-10 17:25:29 +05:30
end
describe 'validations' do
2015-04-26 12:48:37 +05:30
it { is_expected.to validate_presence_of(:url) }
2014-09-02 18:07:02 +05:30
2022-07-23 23:45:48 +05:30
describe 'url_variables' do
it { is_expected.to allow_value({}).for(:url_variables) }
it { is_expected.to allow_value({ 'foo' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'FOO' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'MY_TOKEN' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'foo2' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'x' => 'y' }).for(:url_variables) }
2023-03-04 22:38:38 +05:30
it { is_expected.to allow_value({ 'x' => ('a' * 2048) }).for(:url_variables) }
2022-07-23 23:45:48 +05:30
it { is_expected.to allow_value({ 'foo' => 'bar', 'bar' => 'baz' }).for(:url_variables) }
it { is_expected.to allow_value((1..20).to_h { ["k#{_1}", 'value'] }).for(:url_variables) }
2023-01-13 00:05:48 +05:30
it { is_expected.to allow_value({ 'MY-TOKEN' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'my_secr3t-token' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'x-y-z' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'x_y_z' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'f.o.o' => 'bar' }).for(:url_variables) }
2022-07-23 23:45:48 +05:30
it { is_expected.not_to allow_value([]).for(:url_variables) }
it { is_expected.not_to allow_value({ 'foo' => 1 }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'bar' => :baz }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'bar' => nil }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'foo' => '' }).for(:url_variables) }
2023-03-04 22:38:38 +05:30
it { is_expected.not_to allow_value({ 'foo' => ('a' * 2049) }).for(:url_variables) }
2022-07-23 23:45:48 +05:30
it { is_expected.not_to allow_value({ 'has spaces' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value({ '' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value({ '1foo' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value((1..21).to_h { ["k#{_1}", 'value'] }).for(:url_variables) }
2023-01-13 00:05:48 +05:30
it { is_expected.not_to allow_value({ 'MY--TOKEN' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'MY__SECRET' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'x-_y' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'x..y' => 'foo' }).for(:url_variables) }
2022-07-23 23:45:48 +05:30
end
2016-06-02 11:05:42 +05:30
describe 'url' do
2017-09-10 17:25:29 +05:30
it { is_expected.to allow_value('http://example.com').for(:url) }
it { is_expected.to allow_value('https://example.com').for(:url) }
it { is_expected.to allow_value(' https://example.com ').for(:url) }
it { is_expected.to allow_value('http://test.com/api').for(:url) }
it { is_expected.to allow_value('http://test.com/api?key=abc').for(:url) }
it { is_expected.to allow_value('http://test.com/api?key=abc&type=def').for(:url) }
2014-09-02 18:07:02 +05:30
2017-09-10 17:25:29 +05:30
it { is_expected.not_to allow_value('example.com').for(:url) }
it { is_expected.not_to allow_value('ftp://example.com').for(:url) }
it { is_expected.not_to allow_value('herp-and-derp').for(:url) }
2016-06-02 11:05:42 +05:30
2021-10-27 15:23:28 +05:30
context 'when url is local' do
let(:url) { 'http://localhost:9000' }
it { is_expected.not_to allow_value(url).for(:url) }
it 'is valid if application settings allow local requests from web hooks' do
settings = ApplicationSetting.new(allow_local_requests_from_web_hooks_and_services: true)
allow(ApplicationSetting).to receive(:current).and_return(settings)
is_expected.to allow_value(url).for(:url)
end
end
2016-06-02 11:05:42 +05:30
it 'strips :url before saving it' do
2017-09-10 17:25:29 +05:30
hook.url = ' https://example.com '
2021-04-29 21:17:54 +05:30
hook.save!
2016-06-02 11:05:42 +05:30
expect(hook.url).to eq('https://example.com')
end
2022-08-13 15:12:31 +05:30
context 'when there are URL variables' do
subject { hook }
before do
2023-01-13 00:05:48 +05:30
hook.url_variables = { 'one' => 'a', 'two' => 'b', 'url' => 'http://example.com' }
2022-08-13 15:12:31 +05:30
end
it { is_expected.to allow_value('http://example.com').for(:url) }
it { is_expected.to allow_value('http://example.com/{one}/{two}').for(:url) }
it { is_expected.to allow_value('http://example.com/{one}').for(:url) }
it { is_expected.to allow_value('http://example.com/{two}').for(:url) }
it { is_expected.to allow_value('http://user:s3cret@example.com/{two}').for(:url) }
it { is_expected.to allow_value('http://{one}:{two}@example.com').for(:url) }
2023-01-13 00:05:48 +05:30
it { is_expected.to allow_value('http://{one}').for(:url) }
it { is_expected.to allow_value('{url}').for(:url) }
2022-08-13 15:12:31 +05:30
it { is_expected.not_to allow_value('http://example.com/{one}/{two}/{three}').for(:url) }
it { is_expected.not_to allow_value('http://example.com/{foo}').for(:url) }
it { is_expected.not_to allow_value('http:{user}:{pwd}//example.com/{foo}').for(:url) }
it 'mentions all missing variable names' do
hook.url = 'http://example.com/{one}/{foo}/{two}/{three}'
expect(hook).to be_invalid
expect(hook.errors[:url].to_sentence).to eq "Invalid URL template. Missing keys: [\"foo\", \"three\"]"
end
end
2014-09-02 18:07:02 +05:30
end
2018-03-17 18:26:18 +05:30
describe 'token' do
it { is_expected.to allow_value("foobar").for(:token) }
it { is_expected.not_to allow_values("foo\nbar", "foo\r\nbar").for(:token) }
end
2018-11-20 20:47:30 +05:30
describe 'push_events_branch_filter' do
2023-01-13 00:05:48 +05:30
before do
subject.branch_filter_strategy = strategy
end
context 'with "all branches" strategy' do
let(:strategy) { 'all_branches' }
it {
is_expected.to allow_values(
"good_branch_name",
"another/good-branch_name",
"good branch name",
"good~branchname",
"good_branchname(",
"good_branchname[",
""
).for(:push_events_branch_filter)
}
end
context 'with "wildcard" strategy' do
let(:strategy) { 'wildcard' }
it {
is_expected.to allow_values(
"good_branch_name",
"another/good-branch_name",
"good_branch_name(",
""
).for(:push_events_branch_filter)
}
it {
is_expected.not_to allow_values(
"bad branch name",
"bad~branchname",
"bad_branch_name["
).for(:push_events_branch_filter)
}
it 'gets rid of whitespace' do
hook.push_events_branch_filter = ' branch '
hook.save!
expect(hook.push_events_branch_filter).to eq('branch')
end
2018-11-20 20:47:30 +05:30
2023-01-13 00:05:48 +05:30
it 'stores whitespace only as empty' do
hook.push_events_branch_filter = ' '
hook.save!
expect(hook.push_events_branch_filter).to eq('')
end
2018-11-20 20:47:30 +05:30
end
2023-01-13 00:05:48 +05:30
context 'with "regex" strategy' do
let(:strategy) { 'regex' }
2018-11-20 20:47:30 +05:30
2023-01-13 00:05:48 +05:30
it {
is_expected.to allow_values(
"good_branch_name",
"another/good-branch_name",
"good branch name",
"good~branch~name",
""
).for(:push_events_branch_filter)
}
it { is_expected.not_to allow_values("bad_branch_name(", "bad_branch_name[").for(:push_events_branch_filter) }
2018-11-20 20:47:30 +05:30
end
end
2023-01-10 11:22:00 +05:30
describe 'before_validation :reset_token' do
subject(:hook) { build_stubbed(:project_hook, :token, project: project) }
it 'resets token if url changed' do
hook.url = 'https://webhook.example.com/new-hook'
expect(hook).to be_valid
expect(hook.token).to be_nil
end
it 'does not reset token if new url is set together with the same token' do
hook.url = 'https://webhook.example.com/new-hook'
current_token = hook.token
hook.token = current_token
expect(hook).to be_valid
expect(hook.token).to eq(current_token)
expect(hook.url).to eq('https://webhook.example.com/new-hook')
end
it 'does not reset token if new url is set together with a new token' do
hook.url = 'https://webhook.example.com/new-hook'
hook.token = 'token'
expect(hook).to be_valid
expect(hook.token).to eq('token')
expect(hook.url).to eq('https://webhook.example.com/new-hook')
end
end
describe 'before_validation :reset_url_variables' do
subject(:hook) { build_stubbed(:project_hook, :url_variables, project: project, url: 'http://example.com/{abc}') }
it 'resets url variables if url changed' do
hook.url = 'http://example.com/new-hook'
expect(hook).to be_valid
expect(hook.url_variables).to eq({})
end
it 'resets url variables if url is changed but url variables stayed the same' do
hook.url = 'http://test.example.com/{abc}'
expect(hook).not_to be_valid
expect(hook.url_variables).to eq({})
end
2023-05-08 21:46:49 +05:30
it 'resets url variables if url is changed and url variables are appended' do
hook.url = 'http://suspicious.example.com/{abc}/{foo}'
hook.url_variables = hook.url_variables.merge('foo' => 'bar')
expect(hook).not_to be_valid
expect(hook.url_variables).to eq({})
end
it 'resets url variables if url is changed and url variables are removed' do
hook.url = 'http://suspicious.example.com/{abc}'
hook.url_variables = hook.url_variables.except("def")
expect(hook).not_to be_valid
expect(hook.url_variables).to eq({})
end
2023-01-10 11:22:00 +05:30
it 'does not reset url variables if both url and url variables are changed' do
hook.url = 'http://example.com/{one}/{two}'
hook.url_variables = { 'one' => 'foo', 'two' => 'bar' }
expect(hook).to be_valid
expect(hook.url_variables).to eq({ 'one' => 'foo', 'two' => 'bar' })
end
2023-05-08 21:46:49 +05:30
context 'without url variables' do
2023-06-20 00:43:36 +05:30
subject(:hook) { build_stubbed(:project_hook, project: project, url: 'http://example.com', url_variables: nil) }
2023-05-08 21:46:49 +05:30
it 'does not reset url variables' do
hook.url = 'http://example.com/{one}/{two}'
hook.url_variables = { 'one' => 'foo', 'two' => 'bar' }
expect(hook).to be_valid
expect(hook.url_variables).to eq({ 'one' => 'foo', 'two' => 'bar' })
end
end
2023-01-10 11:22:00 +05:30
end
2023-01-13 00:05:48 +05:30
it "only consider these branch filter strategies are valid" do
expected_valid_types = %w[all_branches regex wildcard]
expect(described_class.branch_filter_strategies.keys).to contain_exactly(*expected_valid_types)
end
2014-09-02 18:07:02 +05:30
end
2018-12-05 23:21:45 +05:30
describe 'encrypted attributes' do
2023-04-23 21:23:45 +05:30
subject { described_class.attr_encrypted_attributes.keys }
2018-12-05 23:21:45 +05:30
2022-07-23 23:45:48 +05:30
it { is_expected.to contain_exactly(:token, :url, :url_variables) }
2018-12-05 23:21:45 +05:30
end
2017-09-10 17:25:29 +05:30
describe 'execute' do
let(:data) { { key: 'value' } }
let(:hook_name) { 'project hook' }
2015-12-23 02:04:40 +05:30
2021-06-08 01:23:25 +05:30
it '#execute' do
expect_next(WebHookService).to receive(:execute)
hook.execute(data, hook_name)
2015-12-23 02:04:40 +05:30
end
2022-04-04 11:22:00 +05:30
it 'passes force: false to the web hook service by default' do
expect(WebHookService)
.to receive(:new).with(hook, data, hook_name, force: false).and_return(double(execute: :done))
2021-06-08 01:23:25 +05:30
2022-04-04 11:22:00 +05:30
expect(hook.execute(data, hook_name)).to eq :done
end
2022-04-04 11:22:00 +05:30
it 'passes force: true to the web hook service if required' do
expect(WebHookService)
.to receive(:new).with(hook, data, hook_name, force: true).and_return(double(execute: :forced))
expect(hook.execute(data, hook_name, force: true)).to eq :forced
end
2017-09-10 17:25:29 +05:30
it '#async_execute' do
2021-06-08 01:23:25 +05:30
expect_next(WebHookService).to receive(:async_execute)
hook.async_execute(data, hook_name)
end
it 'does not async execute non-executable hooks' do
2022-11-25 23:54:43 +05:30
allow(hook).to receive(:executable?).and_return(false)
2021-06-08 01:23:25 +05:30
expect(WebHookService).not_to receive(:new)
2017-09-10 17:25:29 +05:30
hook.async_execute(data, hook_name)
end
2014-09-02 18:07:02 +05:30
end
2019-12-21 20:55:43 +05:30
describe '#destroy' do
2022-07-23 23:45:48 +05:30
it 'does not cascade to web_hook_logs' do
2019-12-21 20:55:43 +05:30
web_hook = create(:project_hook)
create_list(:web_hook_log, 3, web_hook: web_hook)
2022-07-23 23:45:48 +05:30
expect { web_hook.destroy! }.not_to change(web_hook.web_hook_logs, :count)
2019-12-21 20:55:43 +05:30
end
end
2021-06-08 01:23:25 +05:30
describe '#next_backoff' do
context 'when there was no last backoff' do
before do
hook.backoff_count = 0
end
it 'is 10 minutes' do
2023-05-27 22:25:52 +05:30
expect(hook.next_backoff).to eq(WebHooks::AutoDisabling::INITIAL_BACKOFF)
2021-06-08 01:23:25 +05:30
end
end
context 'when we have backed off once' do
before do
hook.backoff_count = 1
end
it 'is twice the initial value' do
2023-05-27 22:25:52 +05:30
expect(hook.next_backoff).to eq(2 * WebHooks::AutoDisabling::INITIAL_BACKOFF)
2021-06-08 01:23:25 +05:30
end
end
context 'when we have backed off 3 times' do
before do
hook.backoff_count = 3
end
it 'grows exponentially' do
2023-05-27 22:25:52 +05:30
expect(hook.next_backoff).to eq(2 * 2 * 2 * WebHooks::AutoDisabling::INITIAL_BACKOFF)
2021-06-08 01:23:25 +05:30
end
end
context 'when the previous backoff was large' do
before do
hook.backoff_count = 8 # last value before MAX_BACKOFF
end
it 'does not exceed the max backoff value' do
2023-05-27 22:25:52 +05:30
expect(hook.next_backoff).to eq(WebHooks::AutoDisabling::MAX_BACKOFF)
2021-06-08 01:23:25 +05:30
end
end
end
2023-04-23 21:23:45 +05:30
describe '#rate_limited?' do
it 'is false when hook has not been rate limited' do
expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
expect(rate_limiter).to receive(:rate_limited?).and_return(false)
end
expect(hook).not_to be_rate_limited
end
it 'is true when hook has been rate limited' do
expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
expect(rate_limiter).to receive(:rate_limited?).and_return(true)
end
2021-10-27 15:23:28 +05:30
2023-04-23 21:23:45 +05:30
expect(hook).to be_rate_limited
2021-10-27 15:23:28 +05:30
end
end
2023-04-23 21:23:45 +05:30
describe '#rate_limit' do
it 'returns the hook rate limit' do
expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
expect(rate_limiter).to receive(:limit).and_return(10)
end
2021-06-08 01:23:25 +05:30
2023-04-23 21:23:45 +05:30
expect(hook.rate_limit).to eq(10)
2021-06-08 01:23:25 +05:30
end
2023-04-23 21:23:45 +05:30
end
2021-09-04 01:27:46 +05:30
2023-04-23 21:23:45 +05:30
describe '#to_json' do
it 'does not error' do
expect { hook.to_json }.not_to raise_error
end
2021-09-04 01:27:46 +05:30
2023-04-23 21:23:45 +05:30
it 'does not contain binary attributes' do
expect(hook.to_json).not_to include('encrypted_url_variables')
2021-09-04 01:27:46 +05:30
end
2023-04-23 21:23:45 +05:30
end
2021-09-04 01:27:46 +05:30
2023-04-23 21:23:45 +05:30
describe '#interpolated_url' do
subject(:hook) { build(:project_hook, project: project) }
2021-09-04 01:27:46 +05:30
2023-04-23 21:23:45 +05:30
context 'when the hook URL does not contain variables' do
before do
hook.url = 'http://example.com'
end
it { is_expected.to have_attributes(interpolated_url: hook.url) }
2021-09-04 01:27:46 +05:30
end
2023-04-23 21:23:45 +05:30
it 'is not vulnerable to malicious input' do
hook.url = 'something%{%<foo>2147483628G}'
hook.url_variables = { 'foo' => '1234567890.12345678' }
2021-09-04 01:27:46 +05:30
2023-04-23 21:23:45 +05:30
expect(hook).to have_attributes(interpolated_url: hook.url)
end
context 'when the hook URL contains variables' do
before do
hook.url = 'http://example.com/{path}/resource?token={token}'
hook.url_variables = { 'path' => 'abc', 'token' => 'xyz' }
2021-10-27 15:23:28 +05:30
end
2023-04-23 21:23:45 +05:30
it { is_expected.to have_attributes(interpolated_url: 'http://example.com/abc/resource?token=xyz') }
context 'when a variable is missing' do
before do
hook.url_variables = { 'path' => 'present' }
end
it 'raises an error' do
# We expect validations to prevent this entirely - this is not user-error
expect { hook.interpolated_url }
.to raise_error(described_class::InterpolationError, include('Missing key token'))
end
end
context 'when the URL appears to include percent formatting' do
before do
hook.url = 'http://example.com/%{path}/resource?token=%{token}'
end
it 'succeeds, interpolates correctly' do
expect(hook.interpolated_url).to eq 'http://example.com/%abc/resource?token=%xyz'
end
end
end
end
describe '#update_last_failure' do
it 'is a method of this class' do
expect { described_class.new(project: project).update_last_failure }.not_to raise_error
end
end
describe '#masked_token' do
it { expect(hook.masked_token).to be_nil }
context 'with a token' do
let(:hook) { build(:project_hook, :token, project: project) }
it { expect(hook.masked_token).to eq described_class::SECRET_MASK }
2021-09-04 01:27:46 +05:30
end
end
2023-01-13 00:05:48 +05:30
describe '#backoff!' do
2022-11-25 23:54:43 +05:30
context 'when we have not backed off before' do
it 'increments the recent_failures count' do
expect { hook.backoff! }.to change(hook, :recent_failures).by(1)
end
2021-09-04 01:27:46 +05:30
end
2023-01-13 00:05:48 +05:30
context 'when the recent failure value is the max value of a smallint' do
before do
hook.update!(recent_failures: 32767, disabled_until: 1.hour.ago)
end
it 'reduces to MAX_FAILURES' do
2023-05-27 22:25:52 +05:30
expect { hook.backoff! }.to change(hook, :recent_failures).to(WebHooks::AutoDisabling::MAX_FAILURES)
2023-01-13 00:05:48 +05:30
end
end
context 'when the recent failure value is MAX_FAILURES' do
before do
2023-05-27 22:25:52 +05:30
hook.update!(recent_failures: WebHooks::AutoDisabling::MAX_FAILURES, disabled_until: 1.hour.ago)
2023-01-13 00:05:48 +05:30
end
it 'does not change recent_failures' do
expect { hook.backoff! }.not_to change(hook, :recent_failures)
end
end
2022-11-25 23:54:43 +05:30
context 'when we have exhausted the grace period' do
2022-01-26 12:08:38 +05:30
before do
2023-05-27 22:25:52 +05:30
hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
2022-01-26 12:08:38 +05:30
end
2022-11-25 23:54:43 +05:30
it 'sets disabled_until to the next backoff' do
expect { hook.backoff! }.to change(hook, :disabled_until).to(hook.next_backoff.from_now)
2022-01-26 12:08:38 +05:30
end
2022-11-25 23:54:43 +05:30
it 'increments the backoff count' do
expect { hook.backoff! }.to change(hook, :backoff_count).by(1)
2021-11-11 11:23:49 +05:30
end
2022-11-25 23:54:43 +05:30
context 'when we have backed off MAX_FAILURES times' do
before do
2023-05-27 22:25:52 +05:30
stub_const("WebHooks::AutoDisabling::MAX_FAILURES", 5)
(WebHooks::AutoDisabling::FAILURE_THRESHOLD + 5).times { hook.backoff! }
2022-11-25 23:54:43 +05:30
end
it 'does not let the backoff count exceed the maximum failure count' do
expect { hook.backoff! }.not_to change(hook, :backoff_count)
end
it 'does not change disabled_until', :skip_freeze_time do
travel_to(hook.disabled_until - 1.minute) do
expect { hook.backoff! }.not_to change(hook, :disabled_until)
end
end
it 'changes disabled_until when it has elapsed', :skip_freeze_time do
travel_to(hook.disabled_until + 1.minute) do
expect { hook.backoff! }.to change { hook.disabled_until }
2023-05-27 22:25:52 +05:30
expect(hook.backoff_count).to eq(WebHooks::AutoDisabling::MAX_FAILURES)
2022-11-25 23:54:43 +05:30
end
2021-11-11 11:23:49 +05:30
end
end
2021-10-27 15:23:28 +05:30
end
2021-09-04 01:27:46 +05:30
end
2023-01-13 00:05:48 +05:30
describe '#failed!' do
2021-09-04 01:27:46 +05:30
it 'increments the failure count' do
expect { hook.failed! }.to change(hook, :recent_failures).by(1)
end
2023-01-13 00:05:48 +05:30
context 'when the recent failure value is the max value of a smallint' do
before do
hook.update!(recent_failures: 32767)
end
it 'does not change recent_failures' do
expect { hook.failed! }.not_to change(hook, :recent_failures)
end
end
2021-10-27 15:23:28 +05:30
it 'does not update the hook if the the failure count exceeds the maximum value' do
2023-05-27 22:25:52 +05:30
hook.recent_failures = WebHooks::AutoDisabling::MAX_FAILURES
2021-09-04 01:27:46 +05:30
2021-10-27 15:23:28 +05:30
sql_count = ActiveRecord::QueryRecorder.new { hook.failed! }.count
expect(sql_count).to eq(0)
end
2023-01-13 00:05:48 +05:30
end
2014-09-02 18:07:02 +05:30
end