# frozen_string_literal: true require 'spec_helper' # rubocop: disable RSpec/FactoriesInMigrationSpecs RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover do let(:test_dir) { FileUploader.options['storage_path'] } let(:filename) { 'image.png' } let!(:namespace) { create(:namespace) } let!(:legacy_project) { create(:project, :legacy_storage, namespace: namespace) } let!(:hashed_project) { create(:project, namespace: namespace) } # default project let(:project) { legacy_project } let!(:issue) { create(:issue, project: project) } let!(:note) { create(:note, note: 'some note', project: project, noteable: issue) } let(:legacy_upload) { create_upload(note, filename) } def create_remote_upload(model, filename) create(:upload, :attachment_upload, path: "note/attachment/#{model.id}/#{filename}", secret: nil, store: ObjectStorage::Store::REMOTE, model: model) end def create_upload(model, filename, with_file = true) params = { path: "uploads/-/system/note/attachment/#{model.id}/#{filename}", model: model, store: ObjectStorage::Store::LOCAL } if with_file upload = create(:upload, :with_file, :attachment_upload, params) model.update(attachment: upload.retrieve_uploader) model.attachment.upload else create(:upload, :attachment_upload, params) end end def new_upload Upload.find_by(model_id: project.id, model_type: 'Project') end def expect_error_log expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |logger| expect(logger).to receive(:warn) end end shared_examples 'legacy upload deletion' do it 'removes the upload record' do described_class.new(legacy_upload).execute expect { legacy_upload.reload }.to raise_error(ActiveRecord::RecordNotFound) end end shared_examples 'move error' do it 'does not remove the upload file' do expect_error_log described_class.new(legacy_upload).execute expect(legacy_upload.reload).to eq(legacy_upload) end end shared_examples 'migrates the file correctly' do before do described_class.new(legacy_upload).execute end it 'creates a new uplaod record correctly' do expect(new_upload.secret).not_to be_nil expect(new_upload.path).to end_with("#{new_upload.secret}/image.png") expect(new_upload.model_id).to eq(project.id) expect(new_upload.model_type).to eq('Project') expect(new_upload.uploader).to eq('FileUploader') end it 'updates the legacy upload note so that it references the file in the markdown' do expected_path = File.join('/uploads', new_upload.secret, 'image.png') expected_markdown = "some note \n ![image](#{expected_path})" expect(note.reload.note).to eq(expected_markdown) end it 'removes the attachment from the note model' do expect(note.reload.attachment.file).to be_nil end end context 'when no note found for the upload' do before do legacy_upload.model_id = nil legacy_upload.model_type = 'Note' expect_error_log end it_behaves_like 'legacy upload deletion' end context 'when upload does not belong to a note' do before do legacy_upload.model = create(:appearance) end it 'does not remove the upload' do expect { described_class.new(legacy_upload).execute }.not_to change { Upload.count } end end context 'when the upload move fails' do before do expect(FileUploader).to receive(:copy_to).and_raise('failed') end it_behaves_like 'move error' end context 'when the upload is in local storage' do shared_examples 'legacy local file' do it 'removes the file correctly' do expect(File.exist?(legacy_upload.absolute_path)).to be_truthy described_class.new(legacy_upload).execute expect(File.exist?(legacy_upload.absolute_path)).to be_falsey end it 'moves legacy uploads to the correct location' do described_class.new(legacy_upload).execute expected_path = File.join(test_dir, 'uploads', project.disk_path, new_upload.secret, filename) expect(File.exist?(expected_path)).to be_truthy end end context 'when the upload file does not exist on the filesystem' do let(:legacy_upload) { create_upload(note, filename, false) } before do expect_error_log end it_behaves_like 'legacy upload deletion' end context 'when an upload belongs to a legacy_diff_note' do let!(:merge_request) { create(:merge_request, source_project: project) } let!(:note) do create(:legacy_diff_note_on_merge_request, note: 'some note', project: project, noteable: merge_request) end let(:legacy_upload) do create(:upload, :with_file, :attachment_upload, path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note) end context 'when the file does not exist for the upload' do let(:legacy_upload) do create(:upload, :attachment_upload, path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note) end it_behaves_like 'move error' end context 'when the file does not exist on expected path' do let(:legacy_upload) do create(:upload, :attachment_upload, :with_file, path: "uploads/-/system/note/attachment/some_part/#{note.id}/#{filename}", model: note) end it_behaves_like 'move error' end context 'when the file path does not include system/note/attachment' do let(:legacy_upload) do create(:upload, :attachment_upload, :with_file, path: "uploads/-/system#{note.id}/#{filename}", model: note) end it_behaves_like 'move error' end context 'when the file move raises an error' do before do allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES) end it_behaves_like 'move error' end context 'when upload has mount_point nil' do let(:legacy_upload) do create(:upload, :with_file, :attachment_upload, path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note, mount_point: nil) end it_behaves_like 'migrates the file correctly' it_behaves_like 'legacy local file' it_behaves_like 'legacy upload deletion' end context 'when the file can be handled correctly' do it_behaves_like 'migrates the file correctly' it_behaves_like 'legacy local file' it_behaves_like 'legacy upload deletion' end end context 'when object storage is disabled for FileUploader' do context 'when the file belongs to a legacy project' do let(:project) { legacy_project } it_behaves_like 'migrates the file correctly' it_behaves_like 'legacy local file' it_behaves_like 'legacy upload deletion' end context 'when the file belongs to a hashed project' do let(:project) { hashed_project } it_behaves_like 'migrates the file correctly' it_behaves_like 'legacy local file' it_behaves_like 'legacy upload deletion' end end context 'when object storage is enabled for FileUploader' do # The process of migrating to object storage is a manual one, # so it would go against expectations to automatically migrate these files # to object storage during this migration. # After this migration, these files should be able to successfully migrate to object storage. before do stub_uploads_object_storage(FileUploader) end context 'when the file belongs to a legacy project' do let(:project) { legacy_project } it_behaves_like 'migrates the file correctly' it_behaves_like 'legacy local file' it_behaves_like 'legacy upload deletion' end context 'when the file belongs to a hashed project' do let(:project) { hashed_project } it_behaves_like 'migrates the file correctly' it_behaves_like 'legacy local file' it_behaves_like 'legacy upload deletion' end end end context 'when legacy uploads are stored in object storage' do let(:legacy_upload) { create_remote_upload(note, filename) } let(:remote_file) do { key: "#{legacy_upload.path}" } end let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) } let(:bucket) { connection.directories.create(key: 'uploads') } before do stub_uploads_object_storage(FileUploader) end shared_examples 'legacy remote file' do it 'removes the file correctly' do # expect(bucket.files.get(remote_file[:key])).to be_nil described_class.new(legacy_upload).execute expect(bucket.files.get(remote_file[:key])).to be_nil end it 'moves legacy uploads to the correct remote location' do described_class.new(legacy_upload).execute connection = ::Fog::Storage.new(FileUploader.object_store_credentials) expect(connection.get_object('uploads', new_upload.path)[:status]).to eq(200) end end context 'when the upload file does not exist on the filesystem' do it_behaves_like 'legacy upload deletion' end context 'when the file belongs to a legacy project' do before do bucket.files.create(remote_file) end let(:project) { legacy_project } it_behaves_like 'migrates the file correctly' it_behaves_like 'legacy remote file' it_behaves_like 'legacy upload deletion' end context 'when the file belongs to a hashed project' do before do bucket.files.create(remote_file) end let(:project) { hashed_project } it_behaves_like 'migrates the file correctly' it_behaves_like 'legacy remote file' it_behaves_like 'legacy upload deletion' end end end # rubocop: enable RSpec/FactoriesInMigrationSpecs