gitlab-org--gitlab-foss/spec/services/bulk_imports/file_download_service_spec.rb

259 lines
7.4 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::FileDownloadService do
describe '#execute' do
let_it_be(:allowed_content_types) { %w(application/gzip application/octet-stream) }
let_it_be(:file_size_limit) { 5.gigabytes }
let_it_be(:config) { build(:bulk_import_configuration) }
let_it_be(:content_type) { 'application/octet-stream' }
let_it_be(:content_disposition) { nil }
let_it_be(:filename) { 'file_download_service_spec' }
let_it_be(:tmpdir) { Dir.tmpdir }
let_it_be(:filepath) { File.join(tmpdir, filename) }
let_it_be(:content_length) { 1000 }
let(:chunk_double) { double('chunk', size: 100, code: 200) }
let(:response_double) do
double(
code: 200,
success?: true,
parsed_response: {},
headers: {
'content-length' => content_length,
'content-type' => content_type,
'content-disposition' => content_disposition
}
)
end
subject do
described_class.new(
configuration: config,
relative_url: '/test',
tmpdir: tmpdir,
filename: filename,
file_size_limit: file_size_limit,
allowed_content_types: allowed_content_types
)
end
before do
allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
allow(client).to receive(:head).and_return(response_double)
allow(client).to receive(:stream).and_yield(chunk_double)
end
end
shared_examples 'downloads file' do
it 'downloads file' do
subject.execute
expect(File.exist?(filepath)).to eq(true)
expect(File.read(filepath)).to include('chunk')
end
end
include_examples 'downloads file'
context 'when content-type is application/gzip' do
let_it_be(:content_type) { 'application/gzip' }
include_examples 'downloads file'
end
context 'when url is not valid' do
it 'raises an error' do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
double = instance_double(BulkImports::Configuration, url: 'https://localhost', access_token: 'token')
service = described_class.new(
configuration: double,
relative_url: '/test',
tmpdir: tmpdir,
filename: filename,
file_size_limit: file_size_limit,
allowed_content_types: allowed_content_types
)
expect { service.execute }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
end
end
context 'when content-type is not valid' do
let(:content_type) { 'invalid' }
it 'raises an error' do
expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid content type')
end
end
context 'when content-length is not valid' do
context 'when content-length exceeds limit' do
let(:file_size_limit) { 1 }
it 'raises an error' do
expect { subject.execute }.to raise_error(
described_class::ServiceError,
'File size 1000 Bytes exceeds limit of 1 Byte'
)
end
end
context 'when content-length is missing' do
let(:content_length) { nil }
it 'raises an error' do
expect { subject.execute }.to raise_error(
described_class::ServiceError,
'Missing content-length header'
)
end
end
end
context 'when content-length is equals the file size limit' do
let(:content_length) { 150 }
let(:file_size_limit) { 150 }
it 'does not raise an error' do
expect { subject.execute }.not_to raise_error
end
end
context 'when partially downloaded file exceeds limit' do
let(:content_length) { 151 }
let(:file_size_limit) { 150 }
it 'raises an error' do
expect { subject.execute }.to raise_error(
described_class::ServiceError,
'File size 151 Bytes exceeds limit of 150 Bytes'
)
end
end
context 'when chunk code is not 200' do
let(:chunk_double) { double('chunk', size: 1000, code: 307) }
it 'raises an error' do
expect { subject.execute }.to raise_error(
described_class::ServiceError,
'File download error 307'
)
end
end
context 'when file is a symlink' do
let_it_be(:symlink) { File.join(tmpdir, 'symlink') }
before do
FileUtils.ln_s(File.join(tmpdir, filename), symlink, force: true)
end
subject do
described_class.new(
configuration: config,
relative_url: '/test',
tmpdir: tmpdir,
filename: 'symlink',
file_size_limit: file_size_limit,
allowed_content_types: allowed_content_types
)
end
it 'raises an error and removes the file' do
expect { subject.execute }.to raise_error(
described_class::ServiceError,
'Invalid downloaded file'
)
expect(File.exist?(symlink)).to eq(false)
end
end
context 'when dir is not in tmpdir' do
subject do
described_class.new(
configuration: config,
relative_url: '/test',
tmpdir: '/etc',
filename: filename,
file_size_limit: file_size_limit,
allowed_content_types: allowed_content_types
)
end
it 'raises an error' do
expect { subject.execute }.to raise_error(
StandardError,
'path /etc is not allowed'
)
end
end
context 'when dir path is being traversed' do
subject do
described_class.new(
configuration: config,
relative_url: '/test',
tmpdir: File.join(Dir.mktmpdir('bulk_imports'), 'test', '..'),
filename: filename,
file_size_limit: file_size_limit,
allowed_content_types: allowed_content_types
)
end
it 'raises an error' do
expect { subject.execute }.to raise_error(
Gitlab::Utils::PathTraversalAttackError,
'Invalid path'
)
end
end
context 'when using the remote filename' do
let_it_be(:filename) { nil }
context 'when no filename is given' do
it 'raises an error when the filename is not provided in the request header' do
expect { subject.execute }.to raise_error(
described_class::ServiceError,
'Remote filename not provided in content-disposition header'
)
end
end
context 'with a given filename' do
let_it_be(:content_disposition) { 'filename="avatar.png"' }
it 'uses the given filename' do
expect(subject.execute).to eq(File.join(tmpdir, "avatar.png"))
end
end
context 'when the filename is a path' do
let_it_be(:content_disposition) { 'filename="../../avatar.png"' }
it 'raises an error when the filename is not provided in the request header' do
expect(subject.execute).to eq(File.join(tmpdir, "avatar.png"))
end
end
context 'when the filename is longer the the limit' do
let_it_be(:content_disposition) { 'filename="../../xxx.b"' }
before do
stub_const("#{described_class}::FILENAME_SIZE_LIMIT", 1)
end
it 'raises an error when the filename is not provided in the request header' do
expect(subject.execute).to eq(File.join(tmpdir, "x.b"))
end
end
end
end
end