2019-07-31 22:56:46 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2015-09-11 14:41:01 +05:30
|
|
|
require 'spec_helper'
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
describe Projects::DestroyService do
|
2018-03-17 18:26:18 +05:30
|
|
|
include ProjectForksHelper
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
let_it_be(:user) { create(:user) }
|
2017-08-17 22:00:37 +05:30
|
|
|
let!(:project) { create(:project, :repository, namespace: user.namespace) }
|
2020-03-13 15:44:24 +05:30
|
|
|
let(:path) { project.repository.disk_path }
|
|
|
|
let(:remove_path) { removal_path(path) }
|
|
|
|
let(:async) { false } # execute or async_execute
|
2015-09-11 14:41:01 +05:30
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
before do
|
|
|
|
stub_container_registry_config(enabled: true)
|
|
|
|
stub_container_registry_tags(repository: :any, tags: [])
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'deleting the project' do
|
2020-03-13 15:44:24 +05:30
|
|
|
before do
|
|
|
|
# Run sidekiq immediately to check that renamed repository will be removed
|
|
|
|
destroy_project(project, user, {})
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'deletes the project', :sidekiq_inline do
|
2017-08-17 22:00:37 +05:30
|
|
|
expect(Project.unscoped.all).not_to include(project)
|
2017-09-10 17:25:29 +05:30
|
|
|
|
2019-12-21 20:55:43 +05:30
|
|
|
expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
|
|
|
|
expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'deleting the project with pipeline and build' do
|
2020-03-13 15:44:24 +05:30
|
|
|
context 'with pipeline and build', :sidekiq_inline do # which has optimistic locking
|
2017-08-17 22:00:37 +05:30
|
|
|
let!(:pipeline) { create(:ci_pipeline, project: project) }
|
|
|
|
let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
|
|
|
|
|
|
|
|
it_behaves_like 'deleting the project'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
shared_examples 'handles errors thrown during async destroy' do |error_message|
|
|
|
|
it 'does not allow the error to bubble up' do
|
|
|
|
expect do
|
2020-03-13 15:44:24 +05:30
|
|
|
destroy_project(project, user, {})
|
2017-09-10 17:25:29 +05:30
|
|
|
end.not_to raise_error
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'unmarks the project as "pending deletion"' do
|
2020-03-13 15:44:24 +05:30
|
|
|
destroy_project(project, user, {})
|
2017-09-10 17:25:29 +05:30
|
|
|
|
|
|
|
expect(project.reload.pending_delete).to be(false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'stores an error message in `projects.delete_error`' do
|
2020-03-13 15:44:24 +05:30
|
|
|
destroy_project(project, user, {})
|
2017-09-10 17:25:29 +05:30
|
|
|
|
|
|
|
expect(project.reload.delete_error).to be_present
|
|
|
|
expect(project.delete_error).to include(error_message)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
it_behaves_like 'deleting the project'
|
2015-09-11 14:41:01 +05:30
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
it 'invalidates personal_project_count cache' do
|
|
|
|
expect(user).to receive(:invalidate_personal_projects_count)
|
2018-11-20 20:47:30 +05:30
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
destroy_project(project, user, {})
|
|
|
|
end
|
2018-10-15 14:42:47 +05:30
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
context 'when project has remote mirrors' do
|
|
|
|
let!(:project) do
|
|
|
|
create(:project, :repository, namespace: user.namespace).tap do |project|
|
|
|
|
project.remote_mirrors.create(url: 'http://test.com')
|
2018-10-15 14:42:47 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
it 'destroys them' do
|
|
|
|
expect(RemoteMirror.count).to eq(1)
|
2018-05-09 12:01:36 +05:30
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
destroy_project(project, user, {})
|
|
|
|
|
|
|
|
expect(RemoteMirror.count).to eq(0)
|
2018-05-09 12:01:36 +05:30
|
|
|
end
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
2018-11-20 20:47:30 +05:30
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
context 'when project has exports' do
|
|
|
|
let!(:project_with_export) do
|
|
|
|
create(:project, :repository, namespace: user.namespace).tap do |project|
|
|
|
|
create(:import_export_upload,
|
|
|
|
project: project,
|
|
|
|
export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz'))
|
2018-11-20 20:47:30 +05:30
|
|
|
end
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
2018-11-20 20:47:30 +05:30
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
it 'destroys project and export' do
|
|
|
|
expect do
|
|
|
|
destroy_project(project_with_export, user, {})
|
|
|
|
end.to change(ImportExportUpload, :count).by(-1)
|
2018-11-20 20:47:30 +05:30
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(Project.all).not_to include(project_with_export)
|
2018-11-20 20:47:30 +05:30
|
|
|
end
|
2015-09-11 14:41:01 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
context 'Sidekiq fake' do
|
|
|
|
before do
|
|
|
|
# Dont run sidekiq to check if renamed repository exists
|
|
|
|
Sidekiq::Testing.fake! { destroy_project(project, user, {}) }
|
|
|
|
end
|
|
|
|
|
|
|
|
it { expect(Project.all).not_to include(project) }
|
2020-03-13 15:44:24 +05:30
|
|
|
|
|
|
|
it do
|
|
|
|
expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
|
|
|
|
end
|
|
|
|
|
|
|
|
it do
|
|
|
|
expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_truthy
|
|
|
|
end
|
2016-06-02 11:05:42 +05:30
|
|
|
end
|
|
|
|
|
2019-10-12 21:52:04 +05:30
|
|
|
context 'when flushing caches fail due to Git errors' do
|
|
|
|
before do
|
|
|
|
allow(project.repository).to receive(:before_delete).and_raise(::Gitlab::Git::CommandError)
|
|
|
|
allow(Gitlab::GitLogger).to receive(:warn).with(
|
2020-03-13 15:44:24 +05:30
|
|
|
class: Repositories::DestroyService.name,
|
2020-04-08 14:13:33 +05:30
|
|
|
container_id: project.id,
|
2019-10-12 21:52:04 +05:30
|
|
|
disk_path: project.disk_path,
|
|
|
|
message: 'Gitlab::Git::CommandError').and_call_original
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'deleting the project'
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when flushing caches fail due to Redis' do
|
2016-11-03 12:29:30 +05:30
|
|
|
before do
|
2017-08-17 22:00:37 +05:30
|
|
|
new_user = create(:user)
|
|
|
|
project.team.add_user(new_user, Gitlab::Access::DEVELOPER)
|
2017-09-10 17:25:29 +05:30
|
|
|
allow_any_instance_of(described_class).to receive(:flush_caches).and_raise(::Redis::CannotConnectError)
|
2016-11-03 12:29:30 +05:30
|
|
|
end
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
it 'keeps project team intact upon an error' do
|
2018-11-18 11:00:15 +05:30
|
|
|
perform_enqueued_jobs do
|
2019-07-07 11:18:12 +05:30
|
|
|
destroy_project(project, user, {})
|
|
|
|
rescue ::Redis::CannotConnectError
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
expect(project.team.members.count).to eq 2
|
2016-11-03 12:29:30 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
context 'with async_execute', :sidekiq_inline do
|
2017-08-17 22:00:37 +05:30
|
|
|
let(:async) { true }
|
|
|
|
|
|
|
|
context 'async delete of project with private issue visibility' do
|
|
|
|
before do
|
|
|
|
project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'deleting the project'
|
2016-06-02 11:05:42 +05:30
|
|
|
end
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
it_behaves_like 'deleting the project with pipeline and build'
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
context 'errors' do
|
|
|
|
context 'when `remove_legacy_registry_tags` fails' do
|
|
|
|
before do
|
|
|
|
expect_any_instance_of(described_class)
|
|
|
|
.to receive(:remove_legacy_registry_tags).and_return(false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'handles errors thrown during async destroy', "Failed to remove some tags"
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when `remove_repository` fails' do
|
|
|
|
before do
|
|
|
|
expect_any_instance_of(described_class)
|
|
|
|
.to receive(:remove_repository).and_return(false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'handles errors thrown during async destroy', "Failed to remove project repository"
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when `execute` raises expected error' do
|
|
|
|
before do
|
|
|
|
expect_any_instance_of(Project)
|
|
|
|
.to receive(:destroy!).and_raise(StandardError.new("Other error message"))
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'handles errors thrown during async destroy', "Other error message"
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when `execute` raises unexpected error' do
|
|
|
|
before do
|
|
|
|
expect_any_instance_of(Project)
|
2018-03-17 18:26:18 +05:30
|
|
|
.to receive(:destroy!).and_raise(Exception.new('Other error message'))
|
2017-09-10 17:25:29 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it 'allows error to bubble up and rolls back project deletion' do
|
|
|
|
expect do
|
2020-03-13 15:44:24 +05:30
|
|
|
destroy_project(project, user, {})
|
2018-03-17 18:26:18 +05:30
|
|
|
end.to raise_error(Exception, 'Other error message')
|
2017-09-10 17:25:29 +05:30
|
|
|
|
|
|
|
expect(project.reload.pending_delete).to be(false)
|
|
|
|
expect(project.delete_error).to include("Other error message")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
2016-06-02 11:05:42 +05:30
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
describe 'container registry' do
|
|
|
|
context 'when there are regular container repositories' do
|
|
|
|
let(:container_repository) { create(:container_repository) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
stub_container_registry_tags(repository: project.full_path + '/image',
|
|
|
|
tags: ['tag'])
|
|
|
|
project.container_repositories << container_repository
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when image repository deletion succeeds' do
|
|
|
|
it 'removes tags' do
|
|
|
|
expect_any_instance_of(ContainerRepository)
|
|
|
|
.to receive(:delete_tags!).and_return(true)
|
|
|
|
|
|
|
|
destroy_project(project, user)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when image repository deletion fails' do
|
|
|
|
it 'raises an exception' do
|
|
|
|
expect_any_instance_of(ContainerRepository)
|
2018-12-05 23:21:45 +05:30
|
|
|
.to receive(:delete_tags!).and_raise(RuntimeError)
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
expect(destroy_project(project, user)).to be false
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
2016-06-02 11:05:42 +05:30
|
|
|
end
|
2019-10-12 21:52:04 +05:30
|
|
|
|
|
|
|
context 'when registry is disabled' do
|
|
|
|
before do
|
|
|
|
stub_container_registry_config(enabled: false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not attempting to remove any tags' do
|
|
|
|
expect(Projects::ContainerRepository::DestroyService).not_to receive(:new)
|
|
|
|
|
|
|
|
destroy_project(project, user)
|
|
|
|
end
|
|
|
|
end
|
2016-06-02 11:05:42 +05:30
|
|
|
end
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
context 'when there are tags for legacy root repository' do
|
|
|
|
before do
|
|
|
|
stub_container_registry_tags(repository: project.full_path,
|
|
|
|
tags: ['tag'])
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when image repository tags deletion succeeds' do
|
|
|
|
it 'removes tags' do
|
|
|
|
expect_any_instance_of(ContainerRepository)
|
|
|
|
.to receive(:delete_tags!).and_return(true)
|
2016-06-02 11:05:42 +05:30
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
destroy_project(project, user)
|
|
|
|
end
|
|
|
|
end
|
2016-06-02 11:05:42 +05:30
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
context 'when image repository tags deletion fails' do
|
|
|
|
it 'raises an exception' do
|
|
|
|
expect_any_instance_of(ContainerRepository)
|
|
|
|
.to receive(:delete_tags!).and_return(false)
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
expect(destroy_project(project, user)).to be false
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|
|
|
|
end
|
2016-06-02 11:05:42 +05:30
|
|
|
end
|
2015-09-11 14:41:01 +05:30
|
|
|
end
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
context 'for a forked project with LFS objects' do
|
|
|
|
let(:forked_project) { fork_project(project, user) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
project.lfs_objects << create(:lfs_object)
|
|
|
|
forked_project.reload
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'destroys the fork' do
|
|
|
|
expect { destroy_project(forked_project, user) }
|
|
|
|
.not_to raise_error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'as the root of a fork network' do
|
2020-01-01 13:55:28 +05:30
|
|
|
let!(:fork_1) { fork_project(project, user) }
|
|
|
|
let!(:fork_2) { fork_project(project, user) }
|
2018-03-17 18:26:18 +05:30
|
|
|
|
|
|
|
it 'updates the fork network with the project name' do
|
2020-01-01 13:55:28 +05:30
|
|
|
fork_network = project.fork_network
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
destroy_project(project, user)
|
|
|
|
|
|
|
|
fork_network.reload
|
|
|
|
|
|
|
|
expect(fork_network.deleted_root_project_name).to eq(project.full_name)
|
|
|
|
expect(fork_network.root_project).to be_nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-03-02 22:35:43 +05:30
|
|
|
context 'repository +deleted path removal' do
|
|
|
|
context 'regular phase' do
|
|
|
|
it 'schedules +deleted removal of existing repos' do
|
|
|
|
service = described_class.new(project, user, {})
|
|
|
|
allow(service).to receive(:schedule_stale_repos_removal)
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(Repositories::ShellDestroyService).to receive(:new).and_call_original
|
2019-03-02 22:35:43 +05:30
|
|
|
expect(GitlabShellWorker).to receive(:perform_in)
|
|
|
|
.with(5.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path))
|
|
|
|
|
|
|
|
service.execute
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'stale cleanup' do
|
2020-03-13 15:44:24 +05:30
|
|
|
let(:async) { true }
|
2019-03-02 22:35:43 +05:30
|
|
|
|
|
|
|
it 'schedules +deleted wiki and repo removal' do
|
|
|
|
allow(ProjectDestroyWorker).to receive(:perform_async)
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(Repositories::ShellDestroyService).to receive(:new).with(project.repository).and_call_original
|
2019-03-02 22:35:43 +05:30
|
|
|
expect(GitlabShellWorker).to receive(:perform_in)
|
|
|
|
.with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path))
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
expect(Repositories::ShellDestroyService).to receive(:new).with(project.wiki.repository).and_call_original
|
2019-03-02 22:35:43 +05:30
|
|
|
expect(GitlabShellWorker).to receive(:perform_in)
|
|
|
|
.with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.wiki.disk_path))
|
|
|
|
|
|
|
|
destroy_project(project, user, {})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-04-08 14:13:33 +05:30
|
|
|
context 'snippets' do
|
|
|
|
let!(:snippet1) { create(:project_snippet, project: project, author: user) }
|
|
|
|
let!(:snippet2) { create(:project_snippet, project: project, author: user) }
|
|
|
|
|
|
|
|
it 'does not include snippets when deleting in batches' do
|
|
|
|
expect(project).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:container_repositories, :snippets] })
|
|
|
|
|
|
|
|
destroy_project(project, user)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'calls the bulk snippet destroy service' do
|
|
|
|
expect(project.snippets.count).to eq 2
|
|
|
|
|
|
|
|
expect(Snippets::BulkDestroyService).to receive(:new)
|
|
|
|
.with(user, project.snippets).and_call_original
|
|
|
|
|
|
|
|
expect do
|
|
|
|
destroy_project(project, user)
|
|
|
|
end.to change(Snippet, :count).by(-2)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when an error is raised deleting snippets' do
|
|
|
|
it 'does not delete project' do
|
|
|
|
allow_next_instance_of(Snippets::BulkDestroyService) do |instance|
|
|
|
|
allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
|
|
|
|
end
|
|
|
|
|
|
|
|
expect(destroy_project(project, user)).to be_falsey
|
|
|
|
expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_truthy
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
def destroy_project(project, user, params = {})
|
|
|
|
described_class.new(project, user, params).public_send(async ? :async_execute : :execute)
|
2018-05-09 12:01:36 +05:30
|
|
|
end
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
def removal_path(path)
|
|
|
|
"#{path}+#{project.id}#{Repositories::DestroyService::DELETED_FLAG}"
|
2015-09-11 14:41:01 +05:30
|
|
|
end
|
|
|
|
end
|