259 lines
7.4 KiB
Ruby
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
|