debian-mirror-gitlab/spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb
2021-11-18 22:05:49 +05:30

473 lines
15 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator, :aggregate_failures do
let(:base_payload) do
{
"class" => "ARandomWorker",
"queue" => "a_worker",
"retry" => true,
"jid" => "d774900367dc8b2962b2479c",
"created_at" => 1234567890,
"enqueued_at" => 1234567890
}
end
def job_payload(args = {})
base_payload.merge('args' => args)
end
let(:worker_class) do
Class.new do
def self.name
"TestSizeLimiterWorker"
end
include ApplicationWorker
def perform(*args); end
end
end
before do
# Settings aren't in the database in specs, but stored in memory, this is fine
# for these tests.
allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(true)
stub_const("TestSizeLimiterWorker", worker_class)
end
describe '#initialize' do
context 'configuration from application settings' do
let(:validator) { described_class.new(worker_class, job_payload) }
it 'has the right defaults' do
expect(validator.mode).to eq(described_class::COMPRESS_MODE)
expect(validator.compression_threshold).to eq(described_class::DEFAULT_COMPRESSION_THRESHOLD_BYTES)
expect(validator.size_limit).to eq(described_class::DEFAULT_SIZE_LIMIT)
end
it 'allows configuration through application settings' do
stub_application_setting(
sidekiq_job_limiter_mode: 'track',
sidekiq_job_limiter_compression_threshold_bytes: 1,
sidekiq_job_limiter_limit_bytes: 2
)
expect(validator.mode).to eq(described_class::TRACK_MODE)
expect(validator.compression_threshold).to eq(1)
expect(validator.size_limit).to eq(2)
end
end
context 'when the input mode is valid' do
it 'does not log a warning message' do
expect(::Sidekiq.logger).not_to receive(:warn)
described_class.new(TestSizeLimiterWorker, job_payload, mode: 'track')
described_class.new(TestSizeLimiterWorker, job_payload, mode: 'compress')
end
end
context 'when the input mode is invalid' do
it 'defaults to track mode and logs a warning message' do
expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter mode: invalid. Fallback to track mode.')
validator = described_class.new(TestSizeLimiterWorker, job_payload, mode: 'invalid')
expect(validator.mode).to eql('track')
end
end
context 'when the input mode is empty' do
it 'defaults to track mode' do
expect(::Sidekiq.logger).not_to receive(:warn)
validator = described_class.new(TestSizeLimiterWorker, job_payload, mode: nil)
expect(validator.mode).to eql('track')
end
end
context 'when the size input is valid' do
it 'does not log a warning message' do
expect(::Sidekiq.logger).not_to receive(:warn)
described_class.new(TestSizeLimiterWorker, job_payload, size_limit: 300)
described_class.new(TestSizeLimiterWorker, job_payload, size_limit: 0)
end
end
context 'when the size input is invalid' do
it 'logs a warning message' do
expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter limit: -1')
validator = described_class.new(TestSizeLimiterWorker, job_payload, size_limit: -1)
expect(validator.size_limit).to be(0)
end
end
context 'when the size input is empty' do
it 'defaults to 0' do
expect(::Sidekiq.logger).not_to receive(:warn)
validator = described_class.new(TestSizeLimiterWorker, job_payload, size_limit: nil)
expect(validator.size_limit).to be(described_class::DEFAULT_SIZE_LIMIT)
end
end
context 'when the compression threshold is valid' do
it 'does not log a warning message' do
expect(::Sidekiq.logger).not_to receive(:warn)
described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 300)
described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 1)
end
end
context 'when the compression threshold is negative' do
it 'logs a warning message' do
expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter compression threshold: -1')
described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: -1)
end
it 'falls back to the default' do
validator = described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: -1)
expect(validator.compression_threshold).to be(100_000)
end
end
context 'when the compression threshold is zero' do
it 'logs a warning message' do
expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter compression threshold: 0')
described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 0)
end
it 'falls back to the default' do
validator = described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 0)
expect(validator.compression_threshold).to be(100_000)
end
end
context 'when the compression threshold is empty' do
it 'defaults to 100_000' do
expect(::Sidekiq.logger).not_to receive(:warn)
validator = described_class.new(TestSizeLimiterWorker, job_payload)
expect(validator.compression_threshold).to be(100_000)
end
end
end
shared_examples 'validate limit job payload size' do
context 'in track mode' do
let(:compression_threshold) { nil }
let(:mode) { 'track' }
context 'when size limit negative' do
let(:size_limit) { -1 }
it 'does not track jobs' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300))
end
it 'does not raise exception' do
expect { validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300)) }.not_to raise_error
end
end
context 'when size limit is 0' do
let(:size_limit) { 0 }
let(:job) { job_payload(a: 'a' * 300) }
it 'does not track jobs' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
validate.call(TestSizeLimiterWorker, job)
end
it 'does not raise exception' do
expect do
validate.call(TestSizeLimiterWorker, job)
end.not_to raise_error
end
it 'marks the job as validated' do
validate.call(TestSizeLimiterWorker, job)
expect(job['size_limiter']).to eq('validated')
end
end
context 'when job size is bigger than size limit' do
let(:size_limit) { 50 }
let(:job) { job_payload(a: 'a' * 300) }
it 'tracks job' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
be_a(Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError)
)
validate.call(TestSizeLimiterWorker, job)
end
it 'does not raise an exception' do
expect do
validate.call(TestSizeLimiterWorker, job)
end.not_to raise_error
end
it 'marks the job as tracked' do
validate.call(TestSizeLimiterWorker, job)
expect(job['size_limiter']).to eq('tracked')
end
context 'when the worker has big_payload attribute' do
before do
worker_class.big_payload!
end
it 'does not track jobs' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300))
validate.call('TestSizeLimiterWorker', job_payload(a: 'a' * 300))
end
it 'does not raise an exception' do
expect do
validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300))
end.not_to raise_error
expect do
validate.call('TestSizeLimiterWorker', job_payload(a: 'a' * 300))
end.not_to raise_error
end
it 'marks the job as validated' do
validate.call(TestSizeLimiterWorker, job)
expect(job['size_limiter']).to eq('validated')
end
end
end
context 'when job size is less than size limit' do
let(:size_limit) { 50 }
let(:job) { job_payload(a: 'a') }
it 'does not track job' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
validate.call(TestSizeLimiterWorker, job)
end
it 'does not raise an exception' do
expect { validate.call(TestSizeLimiterWorker, job) }.not_to raise_error
end
it 'marks the job as validated' do
validate.call(TestSizeLimiterWorker, job)
expect(job['size_limiter']).to eq('validated')
end
end
end
context 'in compress mode' do
let(:size_limit) { 50 }
let(:compression_threshold) { 30 }
let(:mode) { 'compress' }
context 'when job size is less than compression threshold' do
let(:job) { job_payload(a: 'a' * 10) }
it 'does not raise an exception' do
expect(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).not_to receive(:compress)
expect { validate.call(TestSizeLimiterWorker, job) }.not_to raise_error
end
it 'marks the job as validated' do
validate.call(TestSizeLimiterWorker, job)
expect(job['size_limiter']).to eq('validated')
end
end
context 'when job size is bigger than compression threshold and less than size limit after compressed' do
let(:args) { { a: 'a' * 300 } }
let(:job) { job_payload(args) }
it 'does not raise an exception' do
expect(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).to receive(:compress).with(
job, Sidekiq.dump_json(args)
).and_return('a' * 40)
expect do
validate.call(TestSizeLimiterWorker, job)
end.not_to raise_error
end
it 'marks the job as validated' do
validate.call(TestSizeLimiterWorker, job)
expect(job['size_limiter']).to eq('validated')
end
end
context 'when job size is bigger than compression threshold and size limit is 0' do
let(:size_limit) { 0 }
let(:args) { { a: 'a' * 300 } }
let(:job) { job_payload(args) }
it 'does not raise an exception and compresses the arguments' do
expect(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).to receive(:compress).with(
job, Sidekiq.dump_json(args)
).and_return('a' * 40)
expect do
validate.call(TestSizeLimiterWorker, job)
end.not_to raise_error
end
it 'marks the job as validated' do
validate.call(TestSizeLimiterWorker, job)
expect(job['size_limiter']).to eq('validated')
end
end
context 'when the job was already compressed' do
let(:job) do
job_payload({ a: 'a' * 10 })
.merge(Gitlab::SidekiqMiddleware::SizeLimiter::Compressor::COMPRESSED_KEY => true)
end
it 'does not compress the arguments again' do
expect(Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).not_to receive(:compress)
expect { validate.call(TestSizeLimiterWorker, job) }.not_to raise_error
end
end
context 'when job size is bigger than compression threshold and bigger than size limit after compressed' do
let(:args) { { a: 'a' * 3000 } }
let(:job) { job_payload(args) }
it 'raises an exception' do
expect(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).to receive(:compress).with(
job, Sidekiq.dump_json(args)
).and_return('a' * 60)
expect do
validate.call(TestSizeLimiterWorker, job)
end.to raise_error(Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError)
expect(job['size_limiter']).to eq(nil)
end
it 'does not raise an exception when the worker allows big payloads' do
worker_class.big_payload!
expect(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).to receive(:compress).with(
job, Sidekiq.dump_json(args)
).and_return('a' * 60)
expect do
validate.call(TestSizeLimiterWorker, job)
end.not_to raise_error
expect(job['size_limiter']).to eq('validated')
end
end
end
end
describe '.validate!' do
let(:validate) { ->(worker_class, job) { described_class.validate!(worker_class, job) } }
it_behaves_like 'validate limit job payload size' do
before do
stub_application_setting(
sidekiq_job_limiter_mode: mode,
sidekiq_job_limiter_compression_threshold_bytes: compression_threshold,
sidekiq_job_limiter_limit_bytes: size_limit
)
end
end
it "skips background migrations" do
expect(described_class).not_to receive(:new)
described_class::EXEMPT_WORKER_NAMES.each do |class_name|
validate.call(class_name.constantize, job_payload)
end
end
it "skips jobs that are already validated" do
expect(described_class).to receive(:new).once.and_call_original
job = job_payload
described_class.validate!(TestSizeLimiterWorker, job)
described_class.validate!(TestSizeLimiterWorker, job)
end
end
describe '.validated?' do
let(:job) { job_payload }
it 'returns true when the job is already validated' do
described_class.validate!(TestSizeLimiterWorker, job)
expect(described_class.validated?(job)).to eq(true)
end
it 'returns false when job is not yet validated' do
expect(described_class.validated?(job)).to eq(false)
end
end
describe '#validate!' do
context 'when creating an instance with the related configuration variables' do
let(:validate) do
->(worker_clas, job) do
described_class.new(worker_class, job).validate!
end
end
before do
stub_application_setting(
sidekiq_job_limiter_mode: mode,
sidekiq_job_limiter_compression_threshold_bytes: compression_threshold,
sidekiq_job_limiter_limit_bytes: size_limit
)
end
it_behaves_like 'validate limit job payload size'
end
context 'when creating an instance with mode and size limit' do
let(:validate) do
->(worker_clas, job) do
validator = described_class.new(
worker_class, job,
mode: mode, size_limit: size_limit, compression_threshold: compression_threshold
)
validator.validate!
end
end
it_behaves_like 'validate limit job payload size'
end
end
end