tree_restorer_spec.rb 36.6 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
require 'spec_helper'

5 6 7 8
def match_mr1_note(content_regex)
  MergeRequest.find_by(title: 'MR1').notes.select { |n| n.note.match(/#{content_regex}/)}.first
end

9
describe Gitlab::ImportExport::Project::TreeRestorer, quarantine: { flaky: 'https://gitlab.com/gitlab-org/gitlab/-/issues/213793' } do
10 11
  include ImportExport::CommonUtil

12 13
  let(:shared) { project.import_export_shared }

14
  RSpec.shared_examples 'project tree restorer work properly' do |reader, ndjson_enabled|
15 16 17 18 19 20 21 22
    describe 'restore project tree' do
      before_all do
        # Using an admin for import, so we can check assignment of existing members
        @user = create(:admin)
        @existing_members = [
          create(:user, email: 'bernard_willms@gitlabexample.com'),
          create(:user, email: 'saul_will@gitlabexample.com')
        ]
23

24 25 26
        RSpec::Mocks.with_temporary_scope do
          @project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
          @shared = @project.import_export_shared
27

28
          allow(Feature).to receive(:enabled?) { true }
29 30
          stub_feature_flags(project_import_ndjson: ndjson_enabled)

31 32
          setup_import_export_config('complex')
          setup_reader(reader)
J
James Lopez 已提交
33

34 35
          allow_any_instance_of(Repository).to receive(:fetch_source_branch!).and_return(true)
          allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false)
J
James Lopez 已提交
36

37
          expect(@shared).not_to receive(:error)
38 39
          expect_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch).with('feature', 'DCBA')
          allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch)
J
James Lopez 已提交
40

41
          project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project)
42

43 44
          @restored_project_json = project_tree_restorer.restore
        end
45
      end
J
James Lopez 已提交
46

47 48
      after(:context) do
        cleanup_artifacts_from_extract_archive('complex')
49
      end
J
James Lopez 已提交
50

51 52 53 54
      context 'JSON' do
        it 'restores models based on JSON' do
          expect(@restored_project_json).to be_truthy
        end
F
Felipe Artur 已提交
55

56 57
        it 'restore correct project features' do
          project = Project.find_by_path('project')
58

59 60 61 62 63 64
          expect(project.project_feature.issues_access_level).to eq(ProjectFeature::PRIVATE)
          expect(project.project_feature.builds_access_level).to eq(ProjectFeature::PRIVATE)
          expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::PRIVATE)
          expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::PRIVATE)
          expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::PRIVATE)
        end
65

66 67 68
        it 'has the project description' do
          expect(Project.find_by_path('project').description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.')
        end
69

70 71 72 73 74 75 76
        it 'has the same label associated to two issues' do
          expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
        end

        it 'has milestones associated to two separate issues' do
          expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
        end
77

78 79 80 81 82
        context 'when importing a project with cached_markdown_version and note_html' do
          context 'for an Issue' do
            it 'does not import note_html' do
              note_content = 'Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi'
              issue_note = Issue.find_by(description: 'Aliquam enim illo et possimus.').notes.select { |n| n.note.match(/#{note_content}/)}.first
C
charlieablett 已提交
83

84 85
              expect(issue_note.note_html).to match(/#{note_content}/)
            end
86
          end
C
charlieablett 已提交
87

88 89 90 91
          context 'for a Merge Request' do
            it 'does not import note_html' do
              note_content = 'Sit voluptatibus eveniet architecto quidem'
              merge_request_note = match_mr1_note(note_content)
C
charlieablett 已提交
92

93 94
              expect(merge_request_note.note_html).to match(/#{note_content}/)
            end
95
          end
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115

          context 'merge request system note metadata' do
            it 'restores title action for unmark wip' do
              merge_request_note = match_mr1_note('unmarked as a \\*\\*Work In Progress\\*\\*')

              expect(merge_request_note.noteable_type).to eq('MergeRequest')
              expect(merge_request_note.system).to eq(true)
              expect(merge_request_note.system_note_metadata.action).to eq('title')
              expect(merge_request_note.system_note_metadata.commit_count).to be_nil
            end

            it 'restores commit action and commit count for pushing 3 commits' do
              merge_request_note = match_mr1_note('added 3 commits')

              expect(merge_request_note.noteable_type).to eq('MergeRequest')
              expect(merge_request_note.system).to eq(true)
              expect(merge_request_note.system_note_metadata.action).to eq('commit')
              expect(merge_request_note.system_note_metadata.commit_count).to eq(3)
            end
          end
C
charlieablett 已提交
116 117
        end

118 119 120
        it 'creates a valid pipeline note' do
          expect(Ci::Pipeline.find_by_sha('sha-notes').notes).not_to be_empty
        end
121

122 123 124
        it 'pipeline has the correct user ID' do
          expect(Ci::Pipeline.find_by_sha('sha-notes').user_id).to eq(@user.id)
        end
J
James Lopez 已提交
125

126 127 128
        it 'restores pipelines with missing ref' do
          expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
        end
129

130 131
        it 'restores pipeline for merge request' do
          pipeline = Ci::Pipeline.find_by_sha('048721d90c449b244b7b4c53a9186b04330174ec')
132

133 134 135 136 137 138 139
          expect(pipeline).to be_valid
          expect(pipeline.tag).to be_falsey
          expect(pipeline.source).to eq('merge_request_event')
          expect(pipeline.merge_request.id).to be > 0
          expect(pipeline.merge_request.target_branch).to eq('feature')
          expect(pipeline.merge_request.source_branch).to eq('feature_conflict')
        end
140

141 142
        it 'restores pipelines based on ascending id order' do
          expected_ordered_shas = %w[
143 144 145 146 147 148 149
          2ea1f3dec713d940208fb5ce4a38765ecb5d3f73
          ce84140e8b878ce6e7c4d298c7202ff38170e3ac
          048721d90c449b244b7b4c53a9186b04330174ec
          sha-notes
          5f923865dde3436854e9ceb9cdb7815618d4e849
          d2d430676773caa88cdaf7c55944073b2fd5561a
          2ea1f3dec713d940208fb5ce4a38765ecb5d3f73
150
          ]
151

152
          project = Project.find_by_path('project')
153

154 155 156
          project.ci_pipelines.order(:id).each_with_index do |pipeline, i|
            expect(pipeline['sha']).to eq expected_ordered_shas[i]
          end
157 158
        end

159 160
        it 'preserves updated_at on issues' do
          issue = Issue.find_by(description: 'Aliquam enim illo et possimus.')
161

162 163
          expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
        end
164

165 166 167 168
        it 'has multiple issue assignees' do
          expect(Issue.find_by(title: 'Voluptatem').assignees).to contain_exactly(@user, *@existing_members)
          expect(Issue.find_by(title: 'Issue without assignees').assignees).to be_empty
        end
J
James Lopez 已提交
169

170 171
        it 'restores timelogs for issues' do
          timelog = Issue.find_by(title: 'issue_with_timelogs').timelogs.last
172

173 174 175 176
          aggregate_failures do
            expect(timelog.time_spent).to eq(72000)
            expect(timelog.spent_at).to eq("2019-12-27T00:00:00.000Z")
          end
177 178
        end

179 180 181
        it 'contains the merge access levels on a protected branch' do
          expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
        end
182

183 184 185
        it 'contains the push access levels on a protected branch' do
          expect(ProtectedBranch.first.push_access_levels).not_to be_empty
        end
186

187 188 189
        it 'contains the create access levels on a protected tag' do
          expect(ProtectedTag.first.create_access_levels).not_to be_empty
        end
190

191 192 193
        it 'restores issue resource label events' do
          expect(Issue.find_by(title: 'Voluptatem').resource_label_events).not_to be_empty
        end
194

195 196 197
        it 'restores merge requests resource label events' do
          expect(MergeRequest.find_by(title: 'MR1').resource_label_events).not_to be_empty
        end
198

199 200
        it 'restores suggestion' do
          note = Note.find_by("note LIKE 'Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum%'")
201

202 203 204
          expect(note.suggestions.count).to eq(1)
          expect(note.suggestions.first.from_content).to eq("Original line\n")
        end
205

206 207
        context 'event at forth level of the tree' do
          let(:event) { Event.find_by(action: 6) }
208

209 210 211
          it 'restores the event' do
            expect(event).not_to be_nil
          end
212

213 214 215
          it 'has the action' do
            expect(event.action).not_to be_nil
          end
J
James Lopez 已提交
216

217 218 219
          it 'event belongs to note, belongs to merge request, belongs to a project' do
            expect(event.note.noteable.project).not_to be_nil
          end
220
        end
J
James Lopez 已提交
221

222 223 224
        it 'has the correct data for merge request diff files' do
          expect(MergeRequestDiffFile.where.not(diff: nil).count).to eq(55)
        end
225

226 227 228
        it 'has the correct data for merge request diff commits' do
          expect(MergeRequestDiffCommit.count).to eq(77)
        end
229

230 231 232 233
        it 'has the correct data for merge request latest_merge_request_diff' do
          MergeRequest.find_each do |merge_request|
            expect(merge_request.latest_merge_request_diff_id).to eq(merge_request.merge_request_diffs.maximum(:id))
          end
234 235
        end

236 237 238
        it 'has labels associated to label links, associated to issues' do
          expect(Label.first.label_links.first.target).not_to be_nil
        end
239

240 241 242
        it 'has project labels' do
          expect(ProjectLabel.count).to eq(3)
        end
243

244 245 246
        it 'has no group labels' do
          expect(GroupLabel.count).to eq(0)
        end
247

248 249 250
        it 'has issue boards' do
          expect(Project.find_by_path('project').boards.count).to eq(1)
        end
J
Jason Colyer 已提交
251

252 253 254
        it 'has lists associated with the issue board' do
          expect(Project.find_by_path('project').boards.find_by_name('TestBoardABC').lists.count).to eq(3)
        end
J
Jason Colyer 已提交
255

256 257 258
        it 'has a project feature' do
          expect(@project.project_feature).not_to be_nil
        end
259

260 261 262
        it 'has custom attributes' do
          expect(@project.custom_attributes.count).to eq(2)
        end
263

264 265 266
        it 'has badges' do
          expect(@project.project_badges.count).to eq(2)
        end
267

268 269 270
        it 'has snippets' do
          expect(@project.snippets.count).to eq(1)
        end
271

272 273
        it 'has award emoji for a snippet' do
          award_emoji = @project.snippets.first.award_emoji
274

275 276
          expect(award_emoji.map(&:name)).to contain_exactly('thumbsup', 'coffee')
        end
277

278 279 280
        it 'snippet has notes' do
          expect(@project.snippets.first.notes.count).to eq(1)
        end
281

282 283
        it 'snippet has award emojis on notes' do
          award_emoji = @project.snippets.first.notes.first.award_emoji.first
284

285 286
          expect(award_emoji.name).to eq('thumbsup')
        end
287

288 289 290
        it 'restores `ci_cd_settings` : `group_runners_enabled` setting' do
          expect(@project.ci_cd_settings.group_runners_enabled?).to eq(false)
        end
291

292 293 294 295
        it 'restores `auto_devops`' do
          expect(@project.auto_devops_enabled?).to eq(true)
          expect(@project.auto_devops.deploy_strategy).to eq('continuous')
        end
296

297 298 299
        it 'restores the correct service' do
          expect(CustomIssueTrackerService.first).not_to be_nil
        end
300

301 302
        it 'restores zoom meetings' do
          meetings = @project.issues.first.zoom_meetings
303

304 305 306
          expect(meetings.count).to eq(1)
          expect(meetings.first.url).to eq('https://zoom.us/j/123456789')
        end
307

308 309
        it 'restores sentry issues' do
          sentry_issue = @project.issues.first.sentry_issue
310

311 312
          expect(sentry_issue.sentry_issue_identifier).to eq(1234567891)
        end
313

314 315
        it 'has award emoji for an issue' do
          award_emoji = @project.issues.first.award_emoji.first
316

317 318
          expect(award_emoji.name).to eq('musical_keyboard')
        end
319

320 321
        it 'has award emoji for a note in an issue' do
          award_emoji = @project.issues.first.notes.first.award_emoji.first
322

323 324
          expect(award_emoji.name).to eq('clapper')
        end
325

326 327
        it 'restores container_expiration_policy' do
          policy = Project.find_by_path('project').container_expiration_policy
328

329 330 331 332 333
          aggregate_failures do
            expect(policy).to be_an_instance_of(ContainerExpirationPolicy)
            expect(policy).to be_persisted
            expect(policy.cadence).to eq('3month')
          end
334 335
        end

336 337
        it 'restores error_tracking_setting' do
          setting = @project.error_tracking_setting
338

339 340 341 342 343
          aggregate_failures do
            expect(setting.api_url).to eq("https://gitlab.example.com/api/0/projects/sentry-org/sentry-project")
            expect(setting.project_name).to eq("Sentry Project")
            expect(setting.organization_name).to eq("Sentry Org")
          end
344 345
        end

346 347
        it 'restores external pull requests' do
          external_pr = @project.external_pull_requests.last
348

349 350 351 352 353 354
          aggregate_failures do
            expect(external_pr.pull_request_iid).to eq(4)
            expect(external_pr.source_branch).to eq("feature")
            expect(external_pr.target_branch).to eq("master")
            expect(external_pr.status).to eq("open")
          end
355 356
        end

357 358
        it 'restores pipeline schedules' do
          pipeline_schedule = @project.pipeline_schedules.last
359

360 361 362 363 364 365 366
          aggregate_failures do
            expect(pipeline_schedule.description).to eq('Schedule Description')
            expect(pipeline_schedule.ref).to eq('master')
            expect(pipeline_schedule.cron).to eq('0 4 * * 0')
            expect(pipeline_schedule.cron_timezone).to eq('UTC')
            expect(pipeline_schedule.active).to eq(true)
          end
367 368
        end

369 370 371
        it 'restores releases with links' do
          release = @project.releases.last
          link = release.links.last
372

373 374 375 376 377 378
          aggregate_failures do
            expect(release.tag).to eq('release-1.1')
            expect(release.description).to eq('Some release notes')
            expect(release.name).to eq('release-1.1')
            expect(release.sha).to eq('901de3a8bd5573f4a049b1457d28bc1592ba6bf9')
            expect(release.released_at).to eq('2019-12-26T10:17:14.615Z')
379

380 381 382
            expect(link.url).to eq('http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download')
            expect(link.name).to eq('release-1.1.dmg')
          end
383 384
        end

385 386 387 388
        context 'Merge requests' do
          it 'always has the new project as a target' do
            expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project)
          end
J
James Lopez 已提交
389

390 391 392
          it 'has the same source project as originally if source/target are the same' do
            expect(MergeRequest.find_by_title('MR1').source_project).to eq(@project)
          end
J
James Lopez 已提交
393

394 395 396
          it 'has the new project as target if source/target differ' do
            expect(MergeRequest.find_by_title('MR2').target_project).to eq(@project)
          end
J
James Lopez 已提交
397

398 399 400
          it 'has no source if source/target differ' do
            expect(MergeRequest.find_by_title('MR2').source_project_id).to be_nil
          end
401

402 403
          it 'has award emoji' do
            award_emoji = MergeRequest.find_by_title('MR1').award_emoji
404

405 406
            expect(award_emoji.map(&:name)).to contain_exactly('thumbsup', 'drum')
          end
407

408 409 410 411
          context 'notes' do
            it 'has award emoji' do
              merge_request_note = match_mr1_note('Sit voluptatibus eveniet architecto quidem')
              award_emoji = merge_request_note.award_emoji.first
412

413 414
              expect(award_emoji.name).to eq('tada')
            end
415 416
          end
        end
417

418 419 420 421 422
        context 'tokens are regenerated' do
          it 'has new CI trigger tokens' do
            expect(Ci::Trigger.where(token: %w[cdbfasdf44a5958c83654733449e585 33a66349b5ad01fc00174af87804e40]))
              .to be_empty
          end
423

424 425 426
          it 'has a new CI build token' do
            expect(Ci::Build.where(token: 'abcd')).to be_empty
          end
427
        end
K
Kamil Trzciński 已提交
428

429 430 431 432
        context 'has restored the correct number of records' do
          it 'has the correct number of merge requests' do
            expect(@project.merge_requests.size).to eq(9)
          end
K
Kamil Trzciński 已提交
433

434 435 436
          it 'only restores valid triggers' do
            expect(@project.triggers.size).to eq(1)
          end
K
Kamil Trzciński 已提交
437

438 439
          it 'has the correct number of pipelines and statuses' do
            expect(@project.ci_pipelines.size).to eq(7)
K
Kamil Trzciński 已提交
440

441 442 443 444
            @project.ci_pipelines.order(:id).zip([2, 0, 2, 2, 2, 2, 0])
              .each do |(pipeline, expected_status_size)|
              expect(pipeline.statuses.size).to eq(expected_status_size)
            end
445
          end
K
Kamil Trzciński 已提交
446
        end
447

448 449 450 451
        context 'when restoring hierarchy of pipeline, stages and jobs' do
          it 'restores pipelines' do
            expect(Ci::Pipeline.all.count).to be 7
          end
452

453 454 455
          it 'restores pipeline stages' do
            expect(Ci::Stage.all.count).to be 6
          end
456

457 458 459
          it 'correctly restores association between stage and a pipeline' do
            expect(Ci::Stage.all).to all(have_attributes(pipeline_id: a_value > 0))
          end
460

461 462 463
          it 'restores statuses' do
            expect(CommitStatus.all.count).to be 10
          end
464

465 466 467
          it 'correctly restores association between a stage and a job' do
            expect(CommitStatus.all).to all(have_attributes(stage_id: a_value > 0))
          end
468

469 470 471
          it 'correctly restores association between a pipeline and a job' do
            expect(CommitStatus.all).to all(have_attributes(pipeline_id: a_value > 0))
          end
472

473 474 475
          it 'restores a Hash for CommitStatus options' do
            expect(CommitStatus.all.map(&:options).compact).to all(be_a(Hash))
          end
476

477 478
          it 'restores external pull request for the restored pipeline' do
            pipeline_with_external_pr = @project.ci_pipelines.find_by(source: 'external_pull_request_event')
479

480 481
            expect(pipeline_with_external_pr.external_pull_request).to be_persisted
          end
482

483 484 485
          it 'has no import failures' do
            expect(@project.import_failures.size).to eq 0
          end
486
        end
487
      end
488
    end
489

490 491 492 493 494
    shared_examples 'restores group correctly' do |**results|
      it 'has group label' do
        expect(project.group.labels.size).to eq(results.fetch(:labels, 0))
        expect(project.group.labels.where(type: "GroupLabel").where.not(project_id: nil).count).to eq(0)
      end
495

496 497 498
      it 'has group milestone' do
        expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0))
      end
J
James Lopez 已提交
499

500 501 502 503
      it 'has the correct visibility level' do
        # INTERNAL in the `project.json`, group's is PRIVATE
        expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
      end
J
James Lopez 已提交
504
    end
505

506 507 508 509 510 511 512
    context 'project.json file access check' do
      let(:user) { create(:user) }
      let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
      let(:project_tree_restorer) do
        described_class.new(user: user, shared: shared, project: project)
      end
      let(:restored_project_json) { project_tree_restorer.restore }
513

514 515 516 517
      it 'does not read a symlink' do
        Dir.mktmpdir do |tmpdir|
          setup_symlink(tmpdir, 'project.json')
          allow(shared).to receive(:export_path).and_call_original
518

519 520 521
          expect(project_tree_restorer.restore).to eq(false)
          expect(shared.errors).to include('invalid import format')
        end
522
      end
523
    end
524

525 526 527 528 529
    context 'Light JSON' do
      let(:user) { create(:user) }
      let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
      let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
      let(:restored_project_json) { project_tree_restorer.restore }
530

531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556
      context 'with a simple project' do
        before do
          setup_import_export_config('light')
          setup_reader(reader)

          expect(restored_project_json).to eq(true)
        end

        after do
          cleanup_artifacts_from_extract_archive('light')
        end

        it 'issue system note metadata restored successfully' do
          note_content = 'created merge request !1 to address this issue'
          note = project.issues.first.notes.select { |n| n.note.match(/#{note_content}/)}.first

          expect(note.noteable_type).to eq('Issue')
          expect(note.system).to eq(true)
          expect(note.system_note_metadata.action).to eq('merge')
          expect(note.system_note_metadata.commit_count).to be_nil
        end

        context 'when there is an existing build with build token' do
          before do
            create(:ci_build, token: 'abcd')
          end
557

558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578
          it_behaves_like 'restores project successfully',
            issues: 1,
            labels: 2,
            label_with_priorities: 'A project label',
            milestones: 1,
            first_issue_labels: 1,
            services: 1
        end

        context 'when there is an existing build with build token' do
          before do
            create(:ci_build, token: 'abcd')
          end

          it_behaves_like 'restores project successfully',
            issues: 1,
            labels: 2,
            label_with_priorities: 'A project label',
            milestones: 1,
            first_issue_labels: 1
        end
579 580
      end

581
      context 'multiple pipelines reference the same external pull request' do
582
        before do
583 584 585 586 587 588 589 590
          setup_import_export_config('multi_pipeline_ref_one_external_pr')
          setup_reader(reader)

          expect(restored_project_json).to eq(true)
        end

        after do
          cleanup_artifacts_from_extract_archive('multi_pipeline_ref_one_external_pr')
591
        end
592

593
        it_behaves_like 'restores project successfully',
594 595 596 597 598 599 600 601 602 603 604 605 606 607 608
          issues: 0,
          labels: 0,
          milestones: 0,
          ci_pipelines: 2,
          external_pull_requests: 1,
          import_failures: 0

        it 'restores external pull request for the restored pipelines' do
          external_pr = project.external_pull_requests.first

          project.ci_pipelines.each do |pipeline_with_external_pr|
            expect(pipeline_with_external_pr.external_pull_request).to be_persisted
            expect(pipeline_with_external_pr.external_pull_request).to eq(external_pr)
          end
        end
609 610
      end

611 612 613 614 615 616
      context 'when post import action throw non-retriable exception' do
        let(:exception) { StandardError.new('post_import_error') }

        before do
          setup_import_export_config('light')
          setup_reader(reader)
617

618 619 620 621
          expect(project)
            .to receive(:merge_requests)
            .and_raise(exception)
        end
622

623 624 625
        after do
          cleanup_artifacts_from_extract_archive('light')
        end
626

627 628 629
        it 'report post import error' do
          expect(restored_project_json).to eq(false)
          expect(shared.errors).to include('post_import_error')
630 631 632
        end
      end

633 634
      context 'when post import action throw retriable exception one time' do
        let(:exception) { GRPC::DeadlineExceeded.new }
635

636 637 638
        before do
          setup_import_export_config('light')
          setup_reader(reader)
639

640 641 642 643 644 645 646 647
          expect(project)
            .to receive(:merge_requests)
            .and_raise(exception)
          expect(project)
            .to receive(:merge_requests)
            .and_call_original
          expect(restored_project_json).to eq(true)
        end
648

649 650 651
        after do
          cleanup_artifacts_from_extract_archive('light')
        end
652

653 654 655 656 657 658 659 660
        it_behaves_like 'restores project successfully',
          issues: 1,
          labels: 2,
          label_with_priorities: 'A project label',
          milestones: 1,
          first_issue_labels: 1,
          services: 1,
          import_failures: 1
661

662 663
        it 'records the failures in the database' do
          import_failure = ImportFailure.last
664

665 666 667 668 669 670 671 672
          expect(import_failure.project_id).to eq(project.id)
          expect(import_failure.relation_key).to be_nil
          expect(import_failure.relation_index).to be_nil
          expect(import_failure.exception_class).to eq('GRPC::DeadlineExceeded')
          expect(import_failure.exception_message).to be_present
          expect(import_failure.correlation_id_value).not_to be_empty
          expect(import_failure.created_at).to be_present
        end
673 674
      end

675 676 677 678 679
      context 'when the project has overridden params in import data' do
        before do
          setup_import_export_config('light')
          setup_reader(reader)
        end
680

681 682 683
        after do
          cleanup_artifacts_from_extract_archive('light')
        end
684

685 686 687 688 689
        it 'handles string versions of visibility_level' do
          # Project needs to be in a group for visibility level comparison
          # to happen
          group = create(:group)
          project.group = group
690

691
          project.create_import_data(data: { override_params: { visibility_level: Gitlab::VisibilityLevel::INTERNAL.to_s } })
692

693 694 695
          expect(restored_project_json).to eq(true)
          expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
        end
696

697 698
        it 'overwrites the params stored in the JSON' do
          project.create_import_data(data: { override_params: { description: "Overridden" } })
699

700 701 702
          expect(restored_project_json).to eq(true)
          expect(project.description).to eq("Overridden")
        end
703

704 705
        it 'does not allow setting params that are excluded from import_export settings' do
          project.create_import_data(data: { override_params: { lfs_enabled: true } })
706

707 708 709 710 711 712 713 714 715 716 717
          expect(restored_project_json).to eq(true)
          expect(project.lfs_enabled).to be_falsey
        end

        it 'overrides project feature access levels' do
          access_level_keys = project.project_feature.attributes.keys.select { |a| a =~ /_access_level/ }

          # `pages_access_level` is not included, since it is not available in the public API
          # and has a dependency on project's visibility level
          # see ProjectFeature model
          access_level_keys.delete('pages_access_level')
718

719
          disabled_access_levels = Hash[access_level_keys.collect { |item| [item, 'disabled'] }]
720

721
          project.create_import_data(data: { override_params: disabled_access_levels })
722

723
          expect(restored_project_json).to eq(true)
724

725 726 727 728
          aggregate_failures do
            access_level_keys.each do |key|
              expect(project.public_send(key)).to eq(ProjectFeature::DISABLED)
            end
729 730 731
          end
        end
      end
732

733 734 735 736 737 738 739 740 741
      context 'with a project that has a group' do
        let!(:project) do
          create(:project,
                 :builds_disabled,
                 :issues_disabled,
                 name: 'project',
                 path: 'project',
                 group: create(:group, visibility_level: Gitlab::VisibilityLevel::PRIVATE))
        end
742

743 744 745
        before do
          setup_import_export_config('group')
          setup_reader(reader)
746

747 748
          expect(restored_project_json).to eq(true)
        end
749

750 751 752
        after do
          cleanup_artifacts_from_extract_archive('group')
        end
753

754 755 756 757 758 759
        it_behaves_like 'restores project successfully',
          issues: 3,
          labels: 2,
          label_with_priorities: 'A project label',
          milestones: 2,
          first_issue_labels: 1
760

761 762 763 764
        it_behaves_like 'restores group correctly',
          labels: 0,
          milestones: 0,
          first_issue_labels: 1
765

766 767 768 769
        it 'restores issue states' do
          expect(project.issues.with_state(:closed).count).to eq(1)
          expect(project.issues.with_state(:opened).count).to eq(2)
        end
770 771
      end

772 773 774 775 776 777 778 779 780
      context 'with existing group models' do
        let!(:project) do
          create(:project,
                 :builds_disabled,
                 :issues_disabled,
                 name: 'project',
                 path: 'project',
                 group: create(:group))
        end
781

782 783 784 785
        before do
          setup_import_export_config('light')
          setup_reader(reader)
        end
786

787 788 789
        after do
          cleanup_artifacts_from_extract_archive('light')
        end
790

791 792
        it 'does not import any templated services' do
          expect(restored_project_json).to eq(true)
793

794 795
          expect(project.services.where(template: true).count).to eq(0)
        end
796

797 798
        it 'does not import any instance services' do
          expect(restored_project_json).to eq(true)
799

800 801
          expect(project.services.where(instance: true).count).to eq(0)
        end
802

803 804
        it 'imports labels' do
          create(:group_label, name: 'Another label', group: project.group)
805

806
          expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
807

808 809 810
          expect(restored_project_json).to eq(true)
          expect(project.labels.count).to eq(1)
        end
811

812 813
        it 'imports milestones' do
          create(:milestone, name: 'A milestone', group: project.group)
814

815
          expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
816

817 818 819 820
          expect(restored_project_json).to eq(true)
          expect(project.group.milestones.count).to eq(1)
          expect(project.milestones.count).to eq(0)
        end
821 822
      end

823 824 825 826 827 828 829 830 831
      context 'with clashing milestones on IID' do
        let!(:project) do
          create(:project,
                 :builds_disabled,
                 :issues_disabled,
                 name: 'project',
                 path: 'project',
                 group: create(:group))
        end
832

833 834 835 836
        before do
          setup_import_export_config('milestone-iid')
          setup_reader(reader)
        end
837

838 839 840
        after do
          cleanup_artifacts_from_extract_archive('milestone-iid')
        end
841

842 843
        it 'preserves the project milestone IID' do
          expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
J
James Lopez 已提交
844

845 846 847 848 849 850
          expect(restored_project_json).to eq(true)
          expect(project.milestones.count).to eq(2)
          expect(Milestone.find_by_title('Another milestone').iid).to eq(1)
          expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2)
        end
      end
J
James Lopez 已提交
851

852 853 854 855 856
      context 'with external authorization classification labels' do
        before do
          setup_import_export_config('light')
          setup_reader(reader)
        end
J
James Lopez 已提交
857

858 859 860
        after do
          cleanup_artifacts_from_extract_archive('light')
        end
J
James Lopez 已提交
861

862 863
        it 'converts empty external classification authorization labels to nil' do
          project.create_import_data(data: { override_params: { external_authorization_classification_label: "" } })
J
James Lopez 已提交
864

865 866 867
          expect(restored_project_json).to eq(true)
          expect(project.external_authorization_classification_label).to be_nil
        end
868

869 870
        it 'preserves valid external classification authorization labels' do
          project.create_import_data(data: { override_params: { external_authorization_classification_label: "foobar" } })
871

872 873
          expect(restored_project_json).to eq(true)
          expect(project.external_authorization_classification_label).to eq("foobar")
874 875 876 877
        end
      end
    end

878 879 880 881 882 883
    context 'Minimal JSON' do
      let(:project) { create(:project) }
      let(:user) { create(:user) }
      let(:tree_hash) { { 'visibility_level' => visibility } }
      let(:restorer) do
        described_class.new(user: user, shared: shared, project: project)
J
James Lopez 已提交
884 885
      end

886 887 888 889
      before do
        allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(true)
        allow_any_instance_of(Gitlab::ImportExport::JSON::NdjsonReader).to receive(:exist?).and_return(false)
        allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:tree_hash) { tree_hash }
J
James Lopez 已提交
890 891
      end

892
      context 'no group visibility' do
J
James Lopez 已提交
893 894 895
        let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }

        it 'uses the project visibility' do
896 897
          expect(restorer.restore).to eq(true)
          expect(restorer.project.visibility_level).to eq(visibility)
J
James Lopez 已提交
898 899 900
        end
      end

901 902 903
      context 'with restricted internal visibility' do
        describe 'internal project' do
          let(:visibility) { Gitlab::VisibilityLevel::INTERNAL }
904

905
          it 'uses private visibility' do
906 907
            stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])

908 909
            expect(restorer.restore).to eq(true)
            expect(restorer.project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
910 911
          end
        end
J
James Lopez 已提交
912
      end
913

914 915 916 917 918 919
      context 'with group visibility' do
        before do
          group = create(:group, visibility_level: group_visibility)

          project.update(group: group)
        end
920

921 922 923
        context 'private group visibility' do
          let(:group_visibility) { Gitlab::VisibilityLevel::PRIVATE }
          let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
924

925 926 927 928 929 930 931 932 933 934 935 936 937 938 939
          it 'uses the group visibility' do
            expect(restorer.restore).to eq(true)
            expect(restorer.project.visibility_level).to eq(group_visibility)
          end
        end

        context 'public group visibility' do
          let(:group_visibility) { Gitlab::VisibilityLevel::PUBLIC }
          let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }

          it 'uses the project visibility' do
            expect(restorer.restore).to eq(true)
            expect(restorer.project.visibility_level).to eq(visibility)
          end
        end
940

941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959
        context 'internal group visibility' do
          let(:group_visibility) { Gitlab::VisibilityLevel::INTERNAL }
          let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }

          it 'uses the group visibility' do
            expect(restorer.restore).to eq(true)
            expect(restorer.project.visibility_level).to eq(group_visibility)
          end

          context 'with restricted internal visibility' do
            it 'sets private visibility' do
              stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])

              expect(restorer.restore).to eq(true)
              expect(restorer.project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
            end
          end
        end
      end
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990

      context 'with project members' do
        let(:user) { create(:user, :admin) }
        let(:user2) { create(:user) }
        let(:project_members) do
          [
            {
              "id" => 2,
              "access_level" => 40,
              "source_type" => "Project",
              "notification_level" => 3,
              "user" => {
                "id" => user2.id,
                "email" => user2.email,
                "username" => 'test'
              }
            }
          ]
        end
        let(:tree_hash) { { 'project_members' => project_members } }

        before do
          project.add_maintainer(user)
        end

        it 'restores project members' do
          restorer.restore

          expect(project.members.map(&:user)).to contain_exactly(user, user2)
        end
      end
991
    end
992

993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
    context 'JSON with invalid records' do
      subject(:restored_project_json) { project_tree_restorer.restore }

      let(:user) { create(:user) }
      let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
      let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }

      before do
        setup_import_export_config('with_invalid_records')
        setup_reader(reader)

        subject
      end

      after do
        cleanup_artifacts_from_extract_archive('with_invalid_records')
1009
      end
1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036

      context 'when failures occur because a relation fails to be processed' do
        it_behaves_like 'restores project successfully',
          issues: 0,
          labels: 0,
          label_with_priorities: nil,
          milestones: 1,
          first_issue_labels: 0,
          services: 0,
          import_failures: 1

        it 'records the failures in the database' do
          import_failure = ImportFailure.last

          expect(import_failure.project_id).to eq(project.id)
          expect(import_failure.relation_key).to eq('milestones')
          expect(import_failure.relation_index).to be_present
          expect(import_failure.exception_class).to eq('ActiveRecord::RecordInvalid')
          expect(import_failure.exception_message).to be_present
          expect(import_failure.correlation_id_value).not_to be_empty
          expect(import_failure.created_at).to be_present
        end
      end
    end
  end

  context 'enable ndjson import' do
1037
    it_behaves_like 'project tree restorer work properly', :legacy_reader, true
1038

1039
    it_behaves_like 'project tree restorer work properly', :ndjson_reader, true
1040 1041 1042
  end

  context 'disable ndjson import' do
1043
    it_behaves_like 'project tree restorer work properly', :legacy_reader, false
1044
  end
1045
end