destroy_service_spec.rb 7.87 KB
Newer Older
1
2
require 'spec_helper'

3
describe Projects::DestroyService do
4
5
  include ProjectForksHelper

6
  let!(:user) { create(:user) }
7
  let!(:project) { create(:project, :repository, namespace: user.namespace) }
8
9
  let!(:path) { project.repository.path_to_repo }
  let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
10
  let!(:async) { false } # execute or async_execute
11

12
13
14
15
16
  before do
    stub_container_registry_config(enabled: true)
    stub_container_registry_tags(repository: :any, tags: [])
  end

17
18
  shared_examples 'deleting the project' do
    it 'deletes the project' do
19
      expect(Project.unscoped.all).not_to include(project)
20
21
22

      expect(project.gitlab_shell.exists?(project.repository_storage_path, path + '.git')).to be_falsey
      expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path + '.git')).to be_falsey
23
24
25
    end
  end

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  shared_examples 'deleting the project with pipeline and build' do
    context 'with pipeline and build' do # which has optimistic locking
      let!(:pipeline) { create(:ci_pipeline, project: project) }
      let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }

      before do
        perform_enqueued_jobs do
          destroy_project(project, user, {})
        end
      end

      it_behaves_like 'deleting the project'
    end
  end

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
  shared_examples 'handles errors thrown during async destroy' do |error_message|
    it 'does not allow the error to bubble up' do
      expect do
        Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
      end.not_to raise_error
    end

    it 'unmarks the project as "pending deletion"' do
      Sidekiq::Testing.inline! { destroy_project(project, user, {}) }

      expect(project.reload.pending_delete).to be(false)
    end

    it 'stores an error message in `projects.delete_error`' do
      Sidekiq::Testing.inline! { destroy_project(project, user, {}) }

      expect(project.reload.delete_error).to be_present
      expect(project.delete_error).to include(error_message)
    end
  end

62
63
64
65
66
67
  context 'Sidekiq inline' do
    before do
      # Run sidekiq immediatly to check that renamed repository will be removed
      Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
    end

68
    it_behaves_like 'deleting the project'
69
70
71
72
73
74
75
76
  end

  context 'Sidekiq fake' do
    before do
      # Dont run sidekiq to check if renamed repository exists
      Sidekiq::Testing.fake! { destroy_project(project, user, {}) }
    end

77
    it { expect(Project.all).not_to include(project) }
78
79
    it { expect(Dir.exist?(path)).to be_falsey }
    it { expect(Dir.exist?(remove_path)).to be_truthy }
80
81
  end

82
83
84
85
  context 'when flushing caches fail' do
    before do
      new_user = create(:user)
      project.team.add_user(new_user, Gitlab::Access::DEVELOPER)
86
      allow_any_instance_of(described_class).to receive(:flush_caches).and_raise(::Redis::CannotConnectError)
87
88
89
90
91
92
    end

    it 'keeps project team intact upon an error' do
      Sidekiq::Testing.inline! do
        begin
          destroy_project(project, user, {})
93
        rescue ::Redis::CannotConnectError
94
95
96
        end
      end

97
      expect(project.team.members.count).to eq 2
98
99
100
    end
  end

101
102
  context 'with async_execute' do
    let(:async) { true }
103

104
105
106
107
108
109
110
111
    context 'async delete of project with private issue visibility' do
      before do
        project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
        # Run sidekiq immediately to check that renamed repository will be removed
        Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
      end

      it_behaves_like 'deleting the project'
112
113
    end

114
    it_behaves_like 'deleting the project with pipeline and build'
115
116
117
118

    context 'errors' do
      context 'when `remove_legacy_registry_tags` fails' do
        before do
119
          expect_any_instance_of(described_class)
120
121
122
123
124
125
126
127
            .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
128
          expect_any_instance_of(described_class)
129
130
131
132
133
134
            .to receive(:remove_repository).and_return(false)
        end

        it_behaves_like 'handles errors thrown during async destroy', "Failed to remove project repository"
      end

135
      context 'when `execute` raises expected error' do
136
        before do
137
138
          expect_any_instance_of(Project)
            .to receive(:destroy!).and_raise(StandardError.new("Other error message"))
139
140
141
142
        end

        it_behaves_like 'handles errors thrown during async destroy', "Other error message"
      end
143

144
145
146
      context 'when `execute` raises unexpected error' do
        before do
          expect_any_instance_of(Project)
147
            .to receive(:destroy!).and_raise(Exception.new('Other error message'))
148
        end
149

150
151
152
        it 'allows error to bubble up and rolls back project deletion' do
          expect do
            Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
153
          end.to raise_error(Exception, 'Other error message')
154

155
156
157
          expect(project.reload.pending_delete).to be(false)
          expect(project.delete_error).to include("Other error message")
        end
158
159
      end
    end
160
161
  end

162
163
164
  describe 'container registry' do
    context 'when there are regular container repositories' do
      let(:container_repository) { create(:container_repository) }
Andre Guedes's avatar
Andre Guedes committed
165

166
167
168
169
170
      before do
        stub_container_registry_tags(repository: project.full_path + '/image',
                                     tags: ['tag'])
        project.container_repositories << container_repository
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
171

172
173
174
175
      context 'when image repository deletion succeeds' do
        it 'removes tags' do
          expect_any_instance_of(ContainerRepository)
            .to receive(:delete_tags!).and_return(true)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
176

177
178
179
180
181
182
183
184
185
          destroy_project(project, user)
        end
      end

      context 'when image repository deletion fails' do
        it 'raises an exception' do
          expect_any_instance_of(ContainerRepository)
            .to receive(:delete_tags!).and_return(false)

186
          expect(destroy_project(project, user)).to be false
187
        end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
188
189
190
      end
    end

191
    context 'when there are tags for legacy root repository' do
Andre Guedes's avatar
Andre Guedes committed
192
      before do
193
194
195
196
197
198
199
200
201
202
203
        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)

          destroy_project(project, user)
        end
Andre Guedes's avatar
Andre Guedes committed
204
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
205

206
207
208
209
      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)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
210

211
          expect(destroy_project(project, user)).to be false
212
213
        end
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
214
215
216
    end
  end

217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
  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.forked_project_link.destroy
      forked_project.reload
    end

    it 'destroys the fork' do
      expect { destroy_project(forked_project, user) }
        .not_to raise_error
    end
  end

232
233
234
235
236
237
238
239
240
241
242
243
244
  context 'as the root of a fork network' do
    let!(:fork_network) { create(:fork_network, root_project: project) }

    it 'updates the fork network with the project name' do
      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

245
  def destroy_project(project, user, params = {})
246
247
248
249
250
    if async
      Projects::DestroyService.new(project, user, params).async_execute
    else
      Projects::DestroyService.new(project, user, params).execute
    end
251
252
  end
end