merge_request_spec.rb 59.8 KB
Newer Older
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1 2
require 'spec_helper'

3
describe MergeRequest do
4
  include RepoHelpers
5
  include ProjectForksHelper
6

7 8
  subject { create(:merge_request) }

9
  describe 'associations' do
10 11
    it { is_expected.to belong_to(:target_project).class_name('Project') }
    it { is_expected.to belong_to(:source_project).class_name('Project') }
12
    it { is_expected.to belong_to(:merge_user).class_name("User") }
13
    it { is_expected.to belong_to(:assignee) }
14
    it { is_expected.to have_many(:merge_request_diffs) }
15 16
  end

17 18 19 20 21 22 23 24 25 26
  describe 'modules' do
    subject { described_class }

    it { is_expected.to include_module(InternalId) }
    it { is_expected.to include_module(Issuable) }
    it { is_expected.to include_module(Referable) }
    it { is_expected.to include_module(Sortable) }
    it { is_expected.to include_module(Taskable) }
  end

27 28 29 30 31
  describe "act_as_paranoid" do
    it { is_expected.to have_db_column(:deleted_at) }
    it { is_expected.to have_db_index(:deleted_at) }
  end

32
  describe 'validation' do
33 34
    it { is_expected.to validate_presence_of(:target_branch) }
    it { is_expected.to validate_presence_of(:source_branch) }
35

36
    context "Validation of merge user with Merge When Pipeline Succeeds" do
37 38 39 40 41
      it "allows user to be nil when the feature is disabled" do
        expect(subject).to be_valid
      end

      it "is invalid without merge user" do
42
        subject.merge_when_pipeline_succeeds = true
43 44 45 46
        expect(subject).not_to be_valid
      end

      it "is valid with merge user" do
47
        subject.merge_when_pipeline_succeeds = true
48 49 50 51 52
        subject.merge_user = build(:user)

        expect(subject).to be_valid
      end
    end
53 54 55

    context 'for forks' do
      let(:project) { create(:project) }
56 57
      let(:fork1) { fork_project(project) }
      let(:fork2) { fork_project(project) }
58 59 60 61 62 63 64 65

      it 'allows merge requests for sibling-forks' do
        subject.source_project = fork1
        subject.target_project = fork2

        expect(subject).to be_valid
      end
    end
66 67
  end

68
  describe 'respond to' do
69 70 71
    it { is_expected.to respond_to(:unchecked?) }
    it { is_expected.to respond_to(:can_be_merged?) }
    it { is_expected.to respond_to(:cannot_be_merged?) }
72
    it { is_expected.to respond_to(:merge_params) }
73
    it { is_expected.to respond_to(:merge_when_pipeline_succeeds) }
74
  end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
75

76 77 78 79 80 81
  describe '.in_projects' do
    it 'returns the merge requests for a set of projects' do
      expect(described_class.in_projects(Project.all)).to eq([subject])
    end
  end

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
  describe '.set_latest_merge_request_diff_ids!' do
    def create_merge_request_with_diffs(source_branch, diffs: 2)
      params = {
        target_project: project,
        target_branch: 'master',
        source_project: project,
        source_branch: source_branch
      }

      create(:merge_request, params).tap do |mr|
        diffs.times { mr.merge_request_diffs.create }
      end
    end

    let(:project) { create(:project) }

    it 'sets IDs for merge requests, whether they are already set or not' do
      merge_requests = [
        create_merge_request_with_diffs('feature'),
        create_merge_request_with_diffs('feature-conflict'),
        create_merge_request_with_diffs('wip', diffs: 0),
        create_merge_request_with_diffs('csv')
      ]

      merge_requests.take(2).each do |merge_request|
        merge_request.update_column(:latest_merge_request_diff_id, nil)
      end

      expected = merge_requests.map do |merge_request|
        merge_request.merge_request_diffs.maximum(:id)
      end

      expect { project.merge_requests.set_latest_merge_request_diff_ids! }
        .to change { merge_requests.map { |mr| mr.reload.latest_merge_request_diff_id } }.to(expected)
    end
  end

119
  describe '#target_branch_sha' do
120
    let(:project) { create(:project, :repository) }
121

122
    subject { create(:merge_request, source_project: project, target_project: project) }
123

124
    context 'when the target branch does not exist' do
125
      before do
126
        project.repository.rm_branch(subject.author, subject.target_branch)
127
        subject.clear_memoized_shas
128
      end
129 130

      it 'returns nil' do
131
        expect(subject.target_branch_sha).to be_nil
132 133
      end
    end
134 135 136 137 138 139

    it 'returns memoized value' do
      subject.target_branch_sha = '8ffb3c15a5475e59ae909384297fede4badcb4c7'

      expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7'
    end
140 141
  end

142 143 144 145 146
  describe '#card_attributes' do
    it 'includes the author name' do
      allow(subject).to receive(:author).and_return(double(name: 'Robert'))
      allow(subject).to receive(:assignee).and_return(nil)

147 148
      expect(subject.card_attributes)
        .to eq({ 'Author' => 'Robert', 'Assignee' => nil })
149 150 151 152 153 154
    end

    it 'includes the assignee name' do
      allow(subject).to receive(:author).and_return(double(name: 'Robert'))
      allow(subject).to receive(:assignee).and_return(double(name: 'Douwe'))

155 156
      expect(subject.card_attributes)
        .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
157 158 159
    end
  end

160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
  describe '#assignee_ids' do
    it 'returns an array of the assigned user id' do
      subject.assignee_id = 123

      expect(subject.assignee_ids).to eq([123])
    end
  end

  describe '#assignee_ids=' do
    it 'sets assignee_id to the last id in the array' do
      subject.assignee_ids = [123, 456]

      expect(subject.assignee_id).to eq(456)
    end
  end

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
  describe '#assignee_or_author?' do
    let(:user) { create(:user) }

    it 'returns true for a user that is assigned to a merge request' do
      subject.assignee = user

      expect(subject.assignee_or_author?(user)).to eq(true)
    end

    it 'returns true for a user that is the author of a merge request' do
      subject.author = user

      expect(subject.assignee_or_author?(user)).to eq(true)
    end

    it 'returns false for a user that is not the assignee or author' do
      expect(subject.assignee_or_author?(user)).to eq(false)
    end
  end

196 197 198 199 200 201 202 203 204 205 206
  describe '#cache_merge_request_closes_issues!' do
    before do
      subject.project.team << [subject.author, :developer]
      subject.target_branch = subject.project.default_branch
    end

    it 'caches closed issues' do
      issue  = create :issue, project: subject.project
      commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
      allow(subject).to receive(:commits).and_return([commit])

207
      expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1)
208 209
    end

210 211 212 213
    context 'when both internal and external issue trackers are enabled' do
      before do
        subject.project.has_external_issue_tracker = true
        subject.project.save!
214
        create(:jira_service, project: subject.project)
215 216 217 218 219 220
      end

      it 'does not cache issues from external trackers' do
        issue  = ExternalIssue.new('JIRA-123', subject.project)
        commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
        allow(subject).to receive(:commits).and_return([commit])
221

222
        expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
        expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
      end

      it 'caches an internal issue' do
        issue  = create(:issue, project: subject.project)
        commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
        allow(subject).to receive(:commits).and_return([commit])

        expect { subject.cache_merge_request_closes_issues!(subject.author) }
          .to change(subject.merge_requests_closing_issues, :count).by(1)
      end
    end

    context 'when only external issue tracker enabled' do
      before do
        subject.project.has_external_issue_tracker = true
        subject.project.issues_enabled = false
        subject.project.save!
      end

      it 'does not cache issues from external trackers' do
        issue  = ExternalIssue.new('JIRA-123', subject.project)
        commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
        allow(subject).to receive(:commits).and_return([commit])

        expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
      end

      it 'does not cache an internal issue' do
        issue  = create(:issue, project: subject.project)
        commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
        allow(subject).to receive(:commits).and_return([commit])

        expect { subject.cache_merge_request_closes_issues!(subject.author) }
          .not_to change(subject.merge_requests_closing_issues, :count)
      end
259 260 261
    end
  end

262
  describe '#source_branch_sha' do
263
    let(:last_branch_commit) { subject.source_project.repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + subject.source_branch) }
264 265 266 267

    context 'with diffs' do
      subject { create(:merge_request, :with_diffs) }
      it 'returns the sha of the source branch last commit' do
268
        expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
269 270 271
      end
    end

272 273 274
    context 'without diffs' do
      subject { create(:merge_request, :without_diffs) }
      it 'returns the sha of the source branch last commit' do
275
        expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
276
      end
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291

      context 'when there is a tag name matching the branch name' do
        let(:tag_name) { subject.source_branch }

        it 'returns the sha of the source branch last commit' do
          subject.source_project.repository.add_tag(subject.author,
                                                    tag_name,
                                                    subject.target_branch_sha,
                                                    'Add a tag')

          expect(subject.source_branch_sha).to eq(last_branch_commit.sha)

          subject.source_project.repository.rm_tag(subject.author, tag_name)
        end
      end
292 293
    end

294 295 296
    context 'when the merge request is being created' do
      subject { build(:merge_request, source_branch: nil, compare_commits: []) }
      it 'returns nil' do
297
        expect(subject.source_branch_sha).to be_nil
298 299
      end
    end
300 301 302 303 304 305

    it 'returns memoized value' do
      subject.source_branch_sha = '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'

      expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
    end
306 307
  end

308
  describe '#to_reference' do
309
    let(:project) { build(:project, name: 'sample-project') }
310 311
    let(:merge_request) { build(:merge_request, target_project: project, iid: 1) }

312
    it 'returns a String reference to the object' do
313
      expect(merge_request.to_reference).to eq "!1"
314 315 316
    end

    it 'supports a cross-project reference' do
317
      another_project = build(:project, name: 'another-project', namespace: project.namespace)
318
      expect(merge_request.to_reference(another_project)).to eq "sample-project!1"
319
    end
320 321

    it 'returns a String reference with the full path' do
322
      expect(merge_request.to_reference(full: true)).to eq(project.full_path + '!1')
323
    end
324
  end
325

326
  describe '#raw_diffs' do
327 328 329 330 331 332 333
    let(:merge_request) { build(:merge_request) }
    let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }

    context 'when there are MR diffs' do
      it 'delegates to the MR diffs' do
        merge_request.merge_request_diff = MergeRequestDiff.new

334
        expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options)
335

336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
        merge_request.raw_diffs(options)
      end
    end

    context 'when there are no MR diffs' do
      it 'delegates to the compare object' do
        merge_request.compare = double(:compare)

        expect(merge_request.compare).to receive(:raw_diffs).with(options)

        merge_request.raw_diffs(options)
      end
    end
  end

351 352 353 354 355 356
  describe '#diffs' do
    let(:merge_request) { build(:merge_request) }
    let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }

    context 'when there are MR diffs' do
      it 'delegates to the MR diffs' do
357
        merge_request.save
358

359
        expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
360 361 362 363 364 365

        merge_request.diffs(options)
      end
    end

    context 'when there are no MR diffs' do
366
      it 'delegates to the compare object, setting expanded: true' do
367 368
        merge_request.compare = double(:compare)

369
        expect(merge_request.compare).to receive(:diffs).with(options.merge(expanded: true))
370 371 372 373 374 375

        merge_request.diffs(options)
      end
    end
  end

376 377 378 379 380 381
  describe '#diff_size' do
    let(:merge_request) do
      build(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master')
    end

    context 'when there are MR diffs' do
382
      it 'returns the correct count' do
383
        merge_request.save
384 385

        expect(merge_request.diff_size).to eq('105')
386 387
      end

388 389 390 391 392
      it 'returns the correct overflow count' do
        allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
        merge_request.save

        expect(merge_request.diff_size).to eq('2+')
393 394 395
      end

      it 'does not perform highlighting' do
396 397
        merge_request.save

398 399 400 401 402 403 404
        expect(Gitlab::Diff::Highlight).not_to receive(:new)

        merge_request.diff_size
      end
    end

    context 'when there are no MR diffs' do
405
      def set_compare(merge_request)
406 407 408 409 410 411 412 413 414 415
        merge_request.compare = CompareService.new(
          merge_request.source_project,
          merge_request.source_branch
        ).execute(
          merge_request.target_project,
          merge_request.target_branch
        )
      end

      it 'returns the correct count' do
416 417 418 419 420 421 422 423 424 425
        set_compare(merge_request)

        expect(merge_request.diff_size).to eq('105')
      end

      it 'returns the correct overflow count' do
        allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
        set_compare(merge_request)

        expect(merge_request.diff_size).to eq('2+')
426 427 428
      end

      it 'does not perform highlighting' do
429 430
        set_compare(merge_request)

431 432 433 434 435 436 437
        expect(Gitlab::Diff::Highlight).not_to receive(:new)

        merge_request.diff_size
      end
    end
  end

438
  describe "#related_notes" do
439
    let!(:merge_request) { create(:merge_request) }
440 441

    before do
442
      allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] }
443 444
      create(:note_on_commit, commit_id: merge_request.commits.first.id,
                              project: merge_request.project)
445
      create(:note, noteable: merge_request, project: merge_request.project)
446 447
    end

448
    it "includes notes for commits" do
449
      expect(merge_request.commits).not_to be_empty
450
      expect(merge_request.related_notes.count).to eq(2)
451
    end
452

453
    it "includes notes for commits from target project as well" do
454 455 456
      create(:note_on_commit, commit_id: merge_request.commits.first.id,
                              project: merge_request.target_project)

457
      expect(merge_request.commits).not_to be_empty
458
      expect(merge_request.related_notes.count).to eq(3)
459
    end
460
  end
461

462 463
  describe '#for_fork?' do
    it 'returns true if the merge request is for a fork' do
464 465
      subject.source_project = build_stubbed(:project, namespace: create(:group))
      subject.target_project = build_stubbed(:project, namespace: create(:group))
466

467
      expect(subject.for_fork?).to be_truthy
468
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
469

470
    it 'returns false if is not for a fork' do
471
      expect(subject.for_fork?).to be_falsey
472 473 474
    end
  end

475
  describe '#closes_issues' do
476 477
    let(:issue0) { create :issue, project: subject.project }
    let(:issue1) { create :issue, project: subject.project }
478 479 480 481

    let(:commit0) { double('commit0', safe_message: "Fixes #{issue0.to_reference}") }
    let(:commit1) { double('commit1', safe_message: "Fixes #{issue0.to_reference}") }
    let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") }
482 483

    before do
484
      subject.project.team << [subject.author, :developer]
485
      allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
486 487
    end

488
    it 'accesses the set of issues that will be closed on acceptance' do
489 490
      allow(subject.project).to receive(:default_branch)
        .and_return(subject.target_branch)
491

492
      closed = subject.closes_issues
493

494 495
      expect(closed).to include(issue0, issue1)
    end
496

497 498 499
    it 'only lists issues as to be closed if it targets the default branch' do
      allow(subject.project).to receive(:default_branch).and_return('master')
      subject.target_branch = 'something-else'
500

501
      expect(subject.closes_issues).to be_empty
502
    end
503
  end
504

505
  describe '#issues_mentioned_but_not_closing' do
506 507 508 509
    let(:closing_issue) { create :issue, project: subject.project }
    let(:mentioned_issue) { create :issue, project: subject.project }

    let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") }
510

511
    it 'detects issues mentioned in description but not closed' do
512
      subject.project.team << [subject.author, :developer]
513
      subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}"
514

515
      allow(subject).to receive(:commits).and_return([commit])
516 517
      allow(subject.project).to receive(:default_branch)
        .and_return(subject.target_branch)
518

519
      expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
520
    end
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537

    context 'when the project has an external issue tracker' do
      before do
        subject.project.team << [subject.author, :developer]
        commit = double(:commit, safe_message: 'Fixes TEST-3')

        create(:jira_service, project: subject.project)

        allow(subject).to receive(:commits).and_return([commit])
        allow(subject).to receive(:description).and_return('Is related to TEST-2 and TEST-3')
        allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
      end

      it 'detects issues mentioned in description but not closed' do
        expect(subject.issues_mentioned_but_not_closing(subject.author).map(&:to_s)).to match_array(['TEST-2'])
      end
    end
538 539
  end

540
  describe "#work_in_progress?" do
541 542 543
    ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
      it "detects the '#{wip_prefix}' prefix" do
        subject.title = "#{wip_prefix}#{subject.title}"
544
        expect(subject.work_in_progress?).to eq true
545
      end
546 547
    end

548 549
    it "doesn't detect WIP for words starting with WIP" do
      subject.title = "Wipwap #{subject.title}"
550
      expect(subject.work_in_progress?).to eq false
551 552
    end

553 554
    it "doesn't detect WIP for words containing with WIP" do
      subject.title = "WupWipwap #{subject.title}"
555
      expect(subject.work_in_progress?).to eq false
556 557
    end

558
    it "doesn't detect WIP by default" do
559
      expect(subject.work_in_progress?).to eq false
560 561 562
    end
  end

563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
  describe "#wipless_title" do
    ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
      it "removes the '#{wip_prefix}' prefix" do
        wipless_title = subject.title
        subject.title = "#{wip_prefix}#{subject.title}"

        expect(subject.wipless_title).to eq wipless_title
      end

      it "is satisfies the #work_in_progress? method" do
        subject.title = "#{wip_prefix}#{subject.title}"
        subject.title = subject.wipless_title

        expect(subject.work_in_progress?).to eq false
      end
    end
  end

  describe "#wip_title" do
    it "adds the WIP: prefix to the title" do
      wip_title = "WIP: #{subject.title}"

      expect(subject.wip_title).to eq wip_title
586
    end
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602

    it "does not add the WIP: prefix multiple times" do
      wip_title = "WIP: #{subject.title}"
      subject.title = subject.wip_title
      subject.title = subject.wip_title

      expect(subject.wip_title).to eq wip_title
    end

    it "is satisfies the #work_in_progress? method" do
      subject.title = subject.wip_title

      expect(subject.work_in_progress?).to eq true
    end
  end

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
603
  describe '#can_remove_source_branch?' do
604 605
    let(:user) { create(:user) }
    let(:user2) { create(:user) }
606 607 608 609

    before do
      subject.source_project.team << [user, :master]

610 611 612 613
      subject.source_branch = "feature"
      subject.target_branch = "master"
      subject.save!
    end
614

615
    it "can't be removed when its a protected branch" do
616
      allow(ProtectedBranch).to receive(:protected?).and_return(true)
617 618 619
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end

620
    it "can't remove a root ref" do
621 622
      subject.source_branch = "master"
      subject.target_branch = "feature"
623 624 625 626

      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end

627 628 629 630
    it "is unable to remove the source branch for a project the user cannot push to" do
      expect(subject.can_remove_source_branch?(user2)).to be_falsey
    end

631
    it "can be removed if the last commit is the head of the source branch" do
632
      allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit)
633

634
      expect(subject.can_remove_source_branch?(user)).to be_truthy
635
    end
636 637

    it "cannot be removed if the last commit is not also the head of the source branch" do
638 639
      subject.source_branch = "lfs"

640 641
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end
642 643
  end

644 645 646 647 648 649 650 651 652 653 654 655 656 657 658
  describe '#merge_commit_message' do
    it 'includes merge information as the title' do
      request = build(:merge_request, source_branch: 'source', target_branch: 'target')

      expect(request.merge_commit_message)
        .to match("Merge branch 'source' into 'target'\n\n")
    end

    it 'includes its title in the body' do
      request = build(:merge_request, title: 'Remove all technical debt')

      expect(request.merge_commit_message)
        .to match("Remove all technical debt\n\n")
    end

659
    it 'includes its closed issues in the body' do
660
      issue = create(:issue, project: subject.project)
661

662
      subject.project.team << [subject.author, :developer]
663
      subject.description = "This issue Closes #{issue.to_reference}"
664

665 666
      allow(subject.project).to receive(:default_branch)
        .and_return(subject.target_branch)
667 668

      expect(subject.merge_commit_message)
669
        .to match("Closes #{issue.to_reference}")
670 671 672 673 674 675
    end

    it 'includes its reference in the body' do
      request = build_stubbed(:merge_request)

      expect(request.merge_commit_message)
676
        .to match("See merge request #{request.to_reference(full: true)}")
677 678 679 680 681 682 683
    end

    it 'excludes multiple linebreak runs when description is blank' do
      request = build(:merge_request, title: 'Title', description: nil)

      expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
    end
684 685 686 687 688 689 690 691 692 693 694 695 696 697

    it 'includes its description in the body' do
      request = build(:merge_request, description: 'By removing all code')

      expect(request.merge_commit_message(include_description: true))
        .to match("By removing all code\n\n")
    end

    it 'does not includes its description in the body' do
      request = build(:merge_request, description: 'By removing all code')

      expect(request.merge_commit_message)
        .not_to match("By removing all code\n\n")
    end
698 699
  end

700
  describe "#reset_merge_when_pipeline_succeeds" do
701
    let(:merge_if_green) do
702
      create :merge_request, merge_when_pipeline_succeeds: true, merge_user: create(:user),
703 704
                             merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" }
    end
705

706
    it "sets the item to false" do
707
      merge_if_green.reset_merge_when_pipeline_succeeds
708 709
      merge_if_green.reload

710
      expect(merge_if_green.merge_when_pipeline_succeeds).to be_falsey
711 712
      expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
      expect(merge_if_green.merge_params["commit_message"]).to be_nil
713 714 715
    end
  end

716
  describe '#hook_attrs' do
717 718 719 720 721 722 723 724
    it 'delegates to Gitlab::HookData::MergeRequestBuilder#build' do
      builder = double

      expect(Gitlab::HookData::MergeRequestBuilder)
        .to receive(:new).with(subject).and_return(builder)
      expect(builder).to receive(:build)

      subject.hook_attrs
725
    end
726 727 728
  end

  describe '#diverged_commits_count' do
729
    let(:project)      { create(:project, :repository) }
730
    let(:forked_project) { fork_project(project, nil, repository: true) }
731

732
    context 'when the target branch does not exist anymore' do
733 734 735 736
      subject { create(:merge_request, source_project: project, target_project: project) }

      before do
        project.repository.raw_repository.delete_branch(subject.target_branch)
737
        subject.clear_memoized_shas
738
      end
739 740

      it 'does not crash' do
741
        expect { subject.diverged_commits_count }.not_to raise_error
742 743 744 745 746 747 748
      end

      it 'returns 0' do
        expect(subject.diverged_commits_count).to eq(0)
      end
    end

749 750 751 752
    context 'diverged on same repository' do
      subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }

      it 'counts commits that are on target branch but not on source branch' do
753
        expect(subject.diverged_commits_count).to eq(29)
754 755 756 757
      end
    end

    context 'diverged on fork' do
758
      subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: forked_project, target_project: project) }
759 760

      it 'counts commits that are on target branch but not on source branch' do
761
        expect(subject.diverged_commits_count).to eq(29)
762 763 764 765
      end
    end

    context 'rebased on fork' do
766
      subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: forked_project, target_project: project) }
767 768 769 770 771 772 773

      it 'counts commits that are on target branch but not on source branch' do
        expect(subject.diverged_commits_count).to eq(0)
      end
    end

    describe 'caching' do
774
      before do
775 776 777 778
        allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
      end

      it 'caches the output' do
779 780 781
        expect(subject).to receive(:compute_diverged_commits_count)
          .once
          .and_return(2)
782 783 784 785 786 787

        subject.diverged_commits_count
        subject.diverged_commits_count
      end

      it 'invalidates the cache when the source sha changes' do
788 789 790
        expect(subject).to receive(:compute_diverged_commits_count)
          .twice
          .and_return(2)
791 792

        subject.diverged_commits_count
793
        allow(subject).to receive(:source_branch_sha).and_return('123abc')
794 795 796 797
        subject.diverged_commits_count
      end

      it 'invalidates the cache when the target sha changes' do
798 799 800
        expect(subject).to receive(:compute_diverged_commits_count)
          .twice
          .and_return(2)
801 802

        subject.diverged_commits_count
803
        allow(subject).to receive(:target_branch_sha).and_return('123abc')
804 805 806
        subject.diverged_commits_count
      end
    end
807 808
  end

809
  it_behaves_like 'an editable mentionable' do
810
    subject { create(:merge_request, :simple) }
811

812
    let(:backref_text) { "merge request #{subject.to_reference}" }
813
    let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
814
  end
Vinnie Okada's avatar
Vinnie Okada committed
815 816

  it_behaves_like 'a Taskable' do
817
    subject { create :merge_request, :simple }
Vinnie Okada's avatar
Vinnie Okada committed
818
  end
819

820
  describe '#commit_shas' do
821
    before do
822
      allow(subject.merge_request_diff).to receive(:commit_shas)
823
        .and_return(['sha1'])
824 825
    end

826
    it 'delegates to merge request diff' do
827
      expect(subject.commit_shas).to eq ['sha1']
828 829 830
    end
  end

831
  context 'head pipeline' do
832 833 834
    before do
      allow(subject).to receive(:diff_head_sha).and_return('lastsha')
    end
835

836 837 838
    describe '#head_pipeline' do
      it 'returns nil for MR without head_pipeline_id' do
        subject.update_attribute(:head_pipeline_id, nil)
839

840 841
        expect(subject.head_pipeline).to be_nil
      end
842 843 844 845 846 847 848 849

      context 'when the source project does not exist' do
        it 'returns nil' do
          allow(subject).to receive(:source_project).and_return(nil)

          expect(subject.head_pipeline).to be_nil
        end
      end
850 851
    end

852
    describe '#actual_head_pipeline' do
853 854 855
      it 'returns nil for MR with old pipeline' do
        pipeline = create(:ci_empty_pipeline, sha: 'notlatestsha')
        subject.update_attribute(:head_pipeline_id, pipeline.id)
856

857
        expect(subject.actual_head_pipeline).to be_nil
858
      end
859

860 861 862
      it 'returns the pipeline for MR with recent pipeline' do
        pipeline = create(:ci_empty_pipeline, sha: 'lastsha')
        subject.update_attribute(:head_pipeline_id, pipeline.id)
863

864 865
        expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
        expect(subject.actual_head_pipeline).to eq(pipeline)
866
      end
867 868 869 870 871 872

      it 'returns nil when source project does not exist' do
        allow(subject).to receive(:source_project).and_return(nil)

        expect(subject.actual_head_pipeline).to be_nil
      end
873 874
    end
  end
Yorick Peterse's avatar
Yorick Peterse committed
875

876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918
  describe '#has_ci?' do
    let(:merge_request) { build_stubbed(:merge_request) }

    context 'has ci' do
      it 'returns true if MR has head_pipeline_id and commits' do
        allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
        allow(merge_request).to receive(:head_pipeline_id) { double }
        allow(merge_request).to receive(:has_no_commits?) { false }

        expect(merge_request.has_ci?).to be(true)
      end

      it 'returns true if MR has any pipeline and commits' do
        allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
        allow(merge_request).to receive(:head_pipeline_id) { nil }
        allow(merge_request).to receive(:has_no_commits?) { false }
        allow(merge_request).to receive(:all_pipelines) { [double] }

        expect(merge_request.has_ci?).to be(true)
      end

      it 'returns true if MR has CI service and commits' do
        allow(merge_request).to receive_message_chain(:source_project, :ci_service) { double }
        allow(merge_request).to receive(:head_pipeline_id) { nil }
        allow(merge_request).to receive(:has_no_commits?) { false }
        allow(merge_request).to receive(:all_pipelines) { [] }

        expect(merge_request.has_ci?).to be(true)
      end
    end

    context 'has no ci' do
      it 'returns false if MR has no CI service nor pipeline, and no commits' do
        allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
        allow(merge_request).to receive(:head_pipeline_id) { nil }
        allow(merge_request).to receive(:all_pipelines) { [] }
        allow(merge_request).to receive(:has_no_commits?) { true }

        expect(merge_request.has_ci?).to be(false)
      end
    end
  end

919
  describe '#all_pipelines' do
920
    shared_examples 'returning pipelines with proper ordering' do
921
      let!(:all_pipelines) do
922
        subject.all_commit_shas.map do |sha|
923 924 925 926
          create(:ci_empty_pipeline,
                 project: subject.source_project,
                 sha: sha,
                 ref: subject.source_branch)
927 928 929 930 931
        end
      end

      it 'returns all pipelines' do
        expect(subject.all_pipelines).not_to be_empty
932
        expect(subject.all_pipelines).to eq(all_pipelines.reverse)
933
      end
934 935
    end

936 937 938 939 940 941
    context 'with single merge_request_diffs' do
      it_behaves_like 'returning pipelines with proper ordering'
    end

    context 'with multiple irrelevant merge_request_diffs' do
      before do
942
        subject.update(target_branch: 'v1.0.0')
943 944 945
      end

      it_behaves_like 'returning pipelines with proper ordering'
946
    end
947 948

    context 'with unsaved merge request' do
949
      subject { build(:merge_request) }
950 951 952

      let!(:pipeline) do
        create(:ci_empty_pipeline,
953
               project: subject.project,
954 955
               sha: subject.diff_head_sha,
               ref: subject.source_branch)
956
      end
957 958 959 960 961

      it 'returns pipelines from diff_head_sha' do
        expect(subject.all_pipelines).to contain_exactly(pipeline)
      end
    end
962 963
  end

964
  describe '#all_commit_shas' do
965
    context 'when merge request is persisted' do
966
      let(:all_commit_shas) do
967 968
        subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
      end
969

970
      shared_examples 'returning all SHA' do
971
        it 'returns all SHAs from all merge_request_diffs' do
972
          expect(subject.merge_request_diffs.size).to eq(2)
973
          expect(subject.all_commit_shas).to match_array(all_commit_shas)
974
        end
975 976
      end

977 978
      context 'with a completely different branch' do
        before do
979
          subject.update(target_branch: 'csv')
980 981 982
        end

        it_behaves_like 'returning all SHA'
983 984
      end

985 986
      context 'with a branch having no difference' do
        before do
987
          subject.update(target_branch: 'branch-merged')
988 989 990 991 992
          subject.reload # make sure commits were not cached
        end

        it_behaves_like 'returning all SHA'
      end
993 994
    end

995 996 997 998 999 1000 1001 1002 1003
    context 'when merge request is not persisted' do
      context 'when compare commits are set in the service' do
        let(:commit) { spy('commit') }

        subject do
          build(:merge_request, compare_commits: [commit, commit])
        end

        it 'returns commits from compare commits temporary data' do
1004
          expect(subject.all_commit_shas).to eq [commit, commit]
1005
        end
1006 1007
      end

1008 1009 1010 1011
      context 'when compare commits are not set in the service' do
        subject { build(:merge_request) }

        it 'returns array with diff head sha element only' do
1012
          expect(subject.all_commit_shas).to eq [subject.diff_head_sha]
1013 1014
        end
      end
1015 1016 1017
    end
  end

Yorick Peterse's avatar
Yorick Peterse committed
1018
  describe '#participants' do
1019
    let(:project) { create(:project, :public) }
Yorick Peterse's avatar
Yorick Peterse committed
1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040

    let(:mr) do
      create(:merge_request, source_project: project, target_project: project)
    end

    let!(:note1) do
      create(:note_on_merge_request, noteable: mr, project: project, note: 'a')
    end

    let!(:note2) do
      create(:note_on_merge_request, noteable: mr, project: project, note: 'b')
    end

    it 'includes the merge request author' do
      expect(mr.participants).to include(mr.author)
    end

    it 'includes the authors of the notes' do
      expect(mr.participants).to include(note1.author, note2.author)
    end
  end
1041 1042 1043 1044 1045 1046

  describe 'cached counts' do
    it 'updates when assignees change' do
      user1 = create(:user)
      user2 = create(:user)
      mr = create(:merge_request, assignee: user1)
1047 1048
      mr.project.add_developer(user1)
      mr.project.add_developer(user2)
1049

1050 1051
      expect(user1.assigned_open_merge_requests_count).to eq(1)
      expect(user2.assigned_open_merge_requests_count).to eq(0)
1052 1053 1054 1055

      mr.assignee = user2
      mr.save

1056 1057
      expect(user1.assigned_open_merge_requests_count).to eq(0)
      expect(user2.assigned_open_merge_requests_count).to eq(1)
1058 1059
    end
  end
1060

1061
  describe '#merge_async' do
1062 1063 1064 1065 1066 1067 1068 1069 1070 1071
    it 'enqueues MergeWorker job and updates merge_jid' do
      merge_request = create(:merge_request)
      user_id = double(:user_id)
      params = double(:params)
      merge_jid = 'hash-123'

      expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
        merge_jid
      end

1072
      merge_request.merge_async(user_id, params)
1073 1074 1075 1076 1077

      expect(merge_request.reload.merge_jid).to eq(merge_jid)
    end
  end

1078
  describe '#check_if_can_be_merged' do
1079
    let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) }
1080 1081 1082 1083

    subject { create(:merge_request, source_project: project, merge_status: :unchecked) }

    context 'when it is not broken and has no conflicts' do
1084
      before do
1085
        allow(subject).to receive(:broken?) { false }
1086
        allow(project.repository).to receive(:can_be_merged?).and_return(true)
1087
      end
1088

1089
      it 'is marked as mergeable' do
1090 1091 1092 1093 1094
        expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
      end
    end

    context 'when broken' do
1095 1096 1097
      before do
        allow(subject).to receive(:broken?) { true }
      end
1098 1099 1100 1101 1102 1103 1104 1105 1106

      it 'becomes unmergeable' do
        expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
      end
    end

    context 'when it has conflicts' do
      before do
        allow(subject).to receive(:broken?) { false }
1107
        allow(project.repository).to receive(:can_be_merged?).and_return(false)
1108 1109 1110 1111 1112 1113 1114 1115 1116
      end

      it 'becomes unmergeable' do
        expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
      end
    end
  end

  describe '#mergeable?' do
1117
    let(:project) { create(:project) }
1118 1119 1120

    subject { create(:merge_request, source_project: project) }

1121 1122
    it 'returns false if #mergeable_state? is false' do
      expect(subject).to receive(:mergeable_state?) { false }
1123

1124
      expect(subject.mergeable?).to be_falsey
1125 1126
    end

1127
    it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
1128 1129
      allow(subject).to receive(:mergeable_state?) { true }
      expect(subject).to receive(:check_if_can_be_merged)
1130
      expect(subject).to receive(:can_be_merged?) { true }
1131 1132 1133 1134 1135 1136

      expect(subject.mergeable?).to be_truthy
    end
  end

  describe '#mergeable_state?' do
1137
    let(:project) { create(:project, :repository) }
1138 1139 1140

    subject { create(:merge_request, source_project: project) }

1141
    it 'checks if merge request can be merged' do
1142
      allow(subject).to receive(:mergeable_ci_state?) { true }
1143 1144 1145 1146 1147 1148
      expect(subject).to receive(:check_if_can_be_merged)

      subject.mergeable?
    end

    context 'when not open' do
1149 1150 1151
      before do
        subject.close
      end
1152 1153

      it 'returns false' do
1154
        expect(subject.mergeable_state?).to be_falsey
1155 1156 1157 1158
      end
    end

    context 'when working in progress' do
1159 1160 1161
      before do
        subject.title = 'WIP MR'
      end
1162 1163

      it 'returns false' do
1164
        expect(subject.mergeable_state?).to be_falsey
1165 1166 1167 1168
      end
    end

    context 'when broken' do
1169 1170 1171
      before do
        allow(subject).to receive(:broken?) { true }
      end
1172 1173

      it 'returns false' do
1174
        expect(subject.mergeable_state?).to be_falsey
1175 1176 1177 1178
      end
    end

    context 'when failed' do
1179
      context 'when #mergeable_ci_state? is false' do
1180
        before do
1181
          allow(subject).to receive(:mergeable_ci_state?) { false }
1182 1183 1184 1185
        end

        it 'returns false' do
          expect(subject.mergeable_state?).to be_falsey
1186 1187
        end
      end
1188

1189
      context 'when #mergeable_discussions_state? is false' do
1190 1191 1192 1193 1194 1195 1196 1197
        before do
          allow(subject).to receive(:mergeable_discussions_state?) { false }
        end

        it 'returns false' do
          expect(subject.mergeable_state?).to be_falsey
        end
      end
1198 1199 1200
    end
  end

1201
  describe '#mergeable_ci_state?' do
1202
    let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) }
1203
    let(:pipeline) { create(:ci_empty_pipeline) }
1204 1205 1206

    subject { build(:merge_request, target_project: project) }

1207
    context 'when it is only allowed to merge when build is green' do
1208
      context 'and a failed pipeline is associated' do
1209
        before do
1210
          pipeline.update(status: 'failed', sha: subject.diff_head_sha)
1211
          allow(subject).to receive(:head_pipeline) { pipeline }
1212
        end
1213

1214
        it { expect(subject.mergeable_ci_state?).to be_falsey }
1215 1216
      end

1217 1218
      context 'and a successful pipeline is associated' do
        before do
1219
          pipeline.update(status: 'success', sha: subject.diff_head_sha)
1220
          allow(subject).to receive(:head_pipeline) { pipeline }
1221 1222 1223 1224 1225 1226 1227
        end

        it { expect(subject.mergeable_ci_state?).to be_truthy }
      end

      context 'and a skipped pipeline is associated' do
        before do
1228
          pipeline.update(status: 'skipped', sha: subject.diff_head_sha)
1229
          allow(subject).to receive(:head_pipeline) { pipeline }
1230 1231 1232 1233 1234
        end

        it { expect(subject.mergeable_ci_state?).to be_truthy }
      end

1235
      context 'when no pipeline is associated' do
1236
        before do
1237
          allow(subject).to receive(:head_pipeline) { nil }
1238 1239 1240
        end

        it { expect(subject.mergeable_ci_state?).to be_truthy }
1241 1242 1243
      end
    end

1244
    context 'when merges are not restricted to green builds' do
1245
      subject { build(:merge_request, target_project: build(:project, only_allow_merge_if_pipeline_succeeds: false)) }
1246

1247
      context 'and a failed pipeline is associated' do
1248
        before do
1249
          pipeline.statuses << create(:commit_status, status: 'failed', project: project)
1250
          allow(subject).to receive(:head_pipeline) { pipeline }
1251 1252 1253 1254 1255
        end

        it { expect(subject.mergeable_ci_state?).to be_truthy }
      end

1256
      context 'when no pipeline is associated' do
1257
        before do
1258
          allow(subject).to receive(:head_pipeline) { nil }
1259 1260 1261
        end

        it { expect(subject.mergeable_ci_state?).to be_truthy }
1262 1263 1264
      end
    end
  end
1265

1266
  describe '#mergeable_discussions_state?' do
1267
    let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
1268

1269
    context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do
1270
      let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
1271

1272
      context 'with all discussions resolved' do
1273
        before do
1274
          merge_request.discussions.each { |d| d.resolve!(merge_request.author) }
1275 1276 1277
        end

        it 'returns true' do
1278
          expect(merge_request.mergeable_discussions_state?).to be_truthy
1279 1280 1281
        end
      end

1282
      context 'with unresolved discussions' do
1283
        before do
1284
          merge_request.discussions.each(&:unresolve!)
1285 1286 1287
        end

        it 'returns false' do
1288
          expect(merge_request.mergeable_discussions_state?).to be_falsey
1289 1290
        end
      end
1291 1292 1293 1294 1295 1296 1297 1298 1299 1300

      context 'with no discussions' do
        before do
          merge_request.notes.destroy_all
        end

        it 'returns true' do
          expect(merge_request.mergeable_discussions_state?).to be_truthy
        end
      end
1301 1302
    end

1303
    context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do
1304
      let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: false) }
1305

1306
      context 'with unresolved discussions' do
1307
        before do
1308
          merge_request.discussions.each(&:unresolve!)
1309 1310 1311
        end

        it 'returns true' do
1312
          expect(merge_request.mergeable_discussions_state?).to be_truthy
1313 1314 1315 1316 1317
        end
      end
    end
  end

Douwe Maan's avatar
Douwe Maan committed
1318
  describe "#environments_for" do
1319
    let(:project)       { create(:project, :repository) }
Douwe Maan's avatar
Douwe Maan committed
1320
    let(:user)          { project.creator }
Z.J. van de Weg's avatar
Z.J. van de Weg committed
1321 1322
    let(:merge_request) { create(:merge_request, source_project: project) }

Douwe Maan's avatar
Douwe Maan committed
1323 1324 1325 1326 1327
    before do
      merge_request.source_project.add_master(user)
      merge_request.target_project.add_master(user)
    end

1328 1329
    context 'with multiple environments' do
      let(:environments) { create_list(:environment, 3, project: project) }
1330

1331 1332 1333 1334 1335 1336
      before do
        create(:deployment, environment: environments.first, ref: 'master', sha: project.commit('master').id)
        create(:deployment, environment: environments.second, ref: 'feature', sha: project.commit('feature').id)
      end

      it 'selects deployed environments' do
Douwe Maan's avatar
Douwe Maan committed
1337
        expect(merge_request.environments_for(user)).to contain_exactly(environments.first)
1338 1339 1340 1341
      end
    end

    context 'with environments on source project' do
1342
      let(:source_project) { fork_project(project, nil, repository: true) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1343

1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356
      let(:merge_request) do
        create(:merge_request,
               source_project: source_project, source_branch: 'feature',
               target_project: project)
      end

      let(:source_environment) { create(:environment, project: source_project) }

      before do
        create(:deployment, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
      end

      it 'selects deployed environments' do
Douwe Maan's avatar
Douwe Maan committed
1357
        expect(merge_request.environments_for(user)).to contain_exactly(source_environment)
1358 1359 1360 1361 1362 1363 1364 1365 1366 1367
      end

      context 'with environments on target project' do
        let(:target_environment) { create(:environment, project: project) }

        before do
          create(:deployment, environment: target_environment, tag: true, sha: merge_request.diff_head_sha)
        end

        it 'selects deployed environments' do
Douwe Maan's avatar
Douwe Maan committed
1368
          expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment)
1369 1370
        end
      end
1371 1372 1373 1374 1375 1376 1377 1378
    end

    context 'without a diff_head_commit' do
      before do
        expect(merge_request).to receive(:diff_head_commit).and_return(nil)
      end

      it 'returns an empty array' do
Douwe Maan's avatar
Douwe Maan committed
1379
        expect(merge_request.environments_for(user)).to be_empty
1380
      end
Z.J. van de Weg's avatar
Z.J. van de Weg committed
1381 1382 1383
    end
  end

1384
  describe "#reload_diff" do
1385
    let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
1386 1387
    let(:commit) { subject.project.commit(sample_commit.id) }

1388
    it "does not change existing merge request diff" do
1389
      expect(subject.merge_request_diff).not_to receive(:save_git_content)
1390 1391 1392
      subject.reload_diff
    end

1393 1394 1395 1396
    it "creates new merge request diff" do
      expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
    end

1397 1398 1399 1400 1401 1402
    it "executs diff cache service" do
      expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)

      subject.reload_diff
    end

1403 1404 1405 1406 1407 1408
    it "calls update_diff_discussion_positions" do
      expect(subject).to receive(:update_diff_discussion_positions)

      subject.reload_diff
    end
  end
1409

1410 1411 1412 1413 1414 1415
  describe '#update_diff_discussion_positions' do
    let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
    let(:commit) { subject.project.commit(sample_commit.id) }
    let(:old_diff_refs) { subject.diff_refs }

    before do
1416
      # Update merge_request_diff so that #diff_refs will return commit.diff_refs
1417 1418 1419 1420 1421 1422
      allow(subject).to receive(:create_merge_request_diff) do
        subject.merge_request_diffs.create(
          base_commit_sha: commit.parent_id,
          start_commit_sha: commit.parent_id,
          head_commit_sha: commit.sha
        )
1423 1424

        subject.merge_request_diff(true)
1425
      end
1426
    end
1427

1428
    it "updates diff discussion positions" do
1429
      expect(Discussions::UpdateDiffPositionService).to receive(:new).with(
1430
        subject.project,
1431
        subject.author,
1432 1433
        old_diff_refs: old_diff_refs,
        new_diff_refs: commit.diff_refs,
1434
        paths: discussion.position.paths
1435 1436
      ).and_call_original

1437
      expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original
1438 1439
      expect_any_instance_of(DiffNote).to receive(:save).once

1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459
      subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
                                               new_diff_refs: commit.diff_refs,
                                               current_user: subject.author)
    end

    context 'when resolve_outdated_diff_discussions is set' do
      before do
        discussion

        subject.project.update!(resolve_outdated_diff_discussions: true)
      end

      it 'calls MergeRequests::ResolvedDiscussionNotificationService' do
        expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService)
          .to receive(:execute).with(subject)

        subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
                                                 new_diff_refs: commit.diff_refs,
                                                 current_user: subject.author)
      end
1460 1461
    end
  end
1462 1463 1464 1465 1466 1467 1468 1469 1470

  describe '#branch_merge_base_commit' do
    context 'source and target branch exist' do
      it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
      it { expect(subject.branch_merge_base_commit).to be_a(Commit) }
    end

    context 'when the target branch does not exist' do
      before do
1471
        subject.project.repository.rm_branch(subject.author, subject.target_branch)
1472
        subject.clear_memoized_shas
1473 1474 1475 1476
      end

      it 'returns nil' do
        expect(subject.branch_merge_base_commit).to be_nil
1477 1478 1479 1480
      end
    end
  end

1481
  describe "#diff_refs" do
1482 1483 1484 1485 1486 1487 1488 1489
    context "with diffs" do
      subject { create(:merge_request, :with_diffs) }

      it "does not touch the repository" do
        subject # Instantiate the object

        expect_any_instance_of(Repository).not_to receive(:commit)

1490
        subject.diff_refs
1491 1492 1493 1494 1495 1496 1497 1498 1499
      end

      it "returns expected diff_refs" do
        expected_diff_refs = Gitlab::Diff::DiffRefs.new(
          base_sha:  subject.merge_request_diff.base_commit_sha,
          start_sha: subject.merge_request_diff.start_commit_sha,
          head_sha:  subject.merge_request_diff.head_commit_sha
        )

1500
        expect(subject.diff_refs).to eq(expected_diff_refs)
1501 1502 1503
      end
    end
  end
1504

1505
  describe "#source_project_missing?" do
1506
    let(:project)      { create(:project) }
1507
    let(:forked_project) { fork_project(project) }
1508
    let(:user)         { create(:user) }
1509
    let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
1510

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1511
    context "when the fork exists" do
1512 1513
      let(:merge_request) do
        create(:merge_request,
1514
          source_project: forked_project,
1515 1516 1517
          target_project: project)
      end

1518
      it { expect(merge_request.source_project_missing?).to be_falsey }
1519 1520
    end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1521
    context "when the source project is the same as the target project" do
1522 1523
      let(:merge_request) { create(:merge_request, source_project: project) }

1524
      it { expect(merge_request.source_project_missing?).to be_falsey }
1525 1526
    end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1527
    context "when the fork does not exist" do
1528
      let!(:merge_request) do
1529
        create(:merge_request,
1530
          source_project: forked_project,
1531 1532 1533
          target_project: project)
      end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1534
      it "returns true" do
1535 1536 1537
        unlink_project.execute
        merge_request.reload

1538
        expect(merge_request.source_project_missing?).to be_truthy
1539 1540 1541 1542
      end
    end
  end

1543
  describe '#merge_ongoing?' do
1544 1545 1546 1547 1548 1549
    it 'returns true when the merge request is locked' do
      merge_request = build_stubbed(:merge_request, state: :locked)

      expect(merge_request.merge_ongoing?).to be(true)
    end

1550
    it 'returns true when merge_id, MR is not merged and it has no running job' do
1551
      merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
1552
      allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true }
1553 1554 1555

      expect(merge_request.merge_ongoing?).to be(true)
    end
1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570

    it 'returns false when merge_jid is nil' do
      merge_request = build_stubbed(:merge_request, state: :open, merge_jid: nil)

      expect(merge_request.merge_ongoing?).to be(false)
    end

    it 'returns false if MR is merged' do
      merge_request = build_stubbed(:merge_request, state: :merged, merge_jid: 'foo')

      expect(merge_request.merge_ongoing?).to be(false)
    end

    it 'returns false if there is no merge job running' do
      merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
1571
      allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { false }
1572 1573 1574

      expect(merge_request.merge_ongoing?).to be(false)
    end
1575 1576
  end

1577
  describe "#closed_without_fork?" do
1578
    let(:project)      { create(:project) }
1579
    let(:forked_project) { fork_project(project) }
1580
    let(:user)         { create(:user) }
1581
    let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
1582

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1583
    context "when the merge request is closed" do
1584 1585
      let(:closed_merge_request) do
        create(:closed_merge_request,
1586
          source_project: forked_project,
1587 1588 1589
          target_project: project)
      end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1590
      it "returns false if the fork exist" do
1591 1592 1593
        expect(closed_merge_request.closed_without_fork?).to be_falsey
      end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1594
      it "returns true if the fork does not exist" do
1595 1596 1597 1598 1599 1600
        unlink_project.execute
        closed_merge_request.reload

        expect(closed_merge_request.closed_without_fork?).to be_truthy
      end
    end
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1601

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1602
    context "when the merge request is open" do
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1603 1604
      let(:open_merge_request) do
        create(:merge_request,
1605
          source_project: forked_project,
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1606 1607 1608 1609 1610 1611 1612
          target_project: project)
      end

      it "returns false" do
        expect(open_merge_request.closed_without_fork?).to be_falsey
      end
    end
1613
  end
1614

1615
  describe '#reopenable?' do
1616 1617 1618
    context 'when the merge request is closed' do
      it 'returns true' do
        subject.close
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1619

1620
        expect(subject.reopenable?).to be_truthy
1621 1622 1623
      end

      context 'forked project' do
1624
        let(:project)      { create(:project, :public) }
1625
        let(:user)         { create(:user) }
1626
        let(:forked_project) { fork_project(project, user) }
1627 1628

        let!(:merge_request) do
1629
          create(:closed_merge_request,
1630
            source_project: forked_project,
1631 1632 1633 1634
            target_project: project)
        end

        it 'returns false if unforked' do
1635
          Projects::UnlinkForkService.new(forked_project, user).execute
1636

1637
          expect(merge_request.reload.reopenable?).to be_falsey
1638 1639 1640
        end

        it 'returns false if the source project is deleted' do
1641
          Projects::DestroyService.new(forked_project, user).execute
1642

1643
          expect(merge_request.reload.reopenable?).to be_falsey
1644 1645
        end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1646
        it 'returns false if the merge request is merged' do
1647 1648
          merge_request.update_attributes(state: 'merged')

1649
          expect(merge_request.reload.reopenable?).to be_falsey
1650 1651
        end
      end
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1652 1653
    end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1654
    context 'when the merge request is opened' do
1655
      it 'returns false' do
1656
        expect(subject.reopenable?).to be_falsey
1657
      end
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
1658 1659
    end
  end
1660

1661
  describe '#mergeable_with_quick_action?' do
1662
    def create_pipeline(status)
Felipe Artur's avatar
Felipe Artur committed
1663
      pipeline = create(:ci_pipeline_with_one_job,
1664 1665 1666
        project: project,
        ref:     merge_request.source_branch,
        sha:     merge_request.diff_head_sha,
1667 1668
        status:  status,
        head_pipeline_of: merge_request)
Felipe Artur's avatar
Felipe Artur committed
1669 1670

      pipeline
1671 1672
    end

1673
    let(:project)       { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) }
1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684
    let(:developer)     { create(:user) }
    let(:user)          { create(:user) }
    let(:merge_request) { create(:merge_request, source_project: project) }
    let(:mr_sha)        { merge_request.diff_head_sha }

    before do
      project.team << [developer, :developer]
    end

    context 'when autocomplete_precheck is set to true' do
      it 'is mergeable by developer' do
1685
        expect(merge_request.mergeable_with_quick_action?(developer, autocomplete_precheck: true)).to be_truthy
1686 1687 1688
      end

      it 'is not mergeable by normal user' do
1689
        expect(merge_request.mergeable_with_quick_action?(user, autocomplete_precheck: true)).to be_falsey
1690 1691 1692 1693 1694
      end
    end

    context 'when autocomplete_precheck is set to false' do
      it 'is mergeable by developer' do
1695
        expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy
1696 1697 1698
      end

      it 'is not mergeable by normal user' do
1699
        expect(merge_request.mergeable_with_quick_action?(user, last_diff_sha: mr_sha)).to be_falsey
1700 1701 1702 1703 1704 1705 1706 1707
      end

      context 'closed MR'  do
        before do
          merge_request.update_attribute(:state, :closed)
        end

        it 'is not mergeable' do
1708
          expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey
1709 1710 1711 1712 1713 1714 1715 1716 1717
        end
      end

      context 'MR with WIP'  do
        before do
          merge_request.update_attribute(:title, 'WIP: some MR')
        end

        it 'is not mergeable' do
1718
          expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey
1719 1720 1721 1722 1723
        end
      end

      context 'sha differs from the MR diff_head_sha'  do
        it 'is not mergeable' do
1724
          expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: 'some other sha')).to be_falsey
1725 1726 1727
        end
      end

Jarka Kadlecova's avatar
Jarka Kadlecova committed
1728 1729
      context 'sha is not provided'  do
        it 'is not mergeable' do
1730
          expect(merge_request.mergeable_with_quick_action?(developer)).to be_falsey
Jarka Kadlecova's avatar
Jarka Kadlecova committed
1731 1732 1733
        end
      end

1734 1735 1736 1737 1738 1739
      context 'with pipeline ok'  do
        before do
          create_pipeline(:success)
        end

        it 'is mergeable' do
1740
          expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy
1741 1742 1743 1744 1745 1746 1747 1748 1749
        end
      end

      context 'with failing pipeline'  do
        before do
          create_pipeline(:failed)
        end

        it 'is not mergeable' do
1750
          expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey
1751 1752 1753 1754 1755
        end
      end

      context 'with running pipeline'  do
        before do
Felipe Artur's avatar
Felipe Artur committed
1756
          create_pipeline(:running)
1757 1758 1759
        end

        it 'is mergeable' do
1760
          expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy
1761 1762 1763 1764 1765
        end
      end
    end
  end

1766 1767
  describe '#has_commits?' do
    before do
1768 1769
      allow(subject.merge_request_diff).to receive(:commits_count)
        .and_return(2)
1770 1771 1772 1773 1774 1775 1776 1777 1778
    end

    it 'returns true when merge request diff has commits' do
      expect(subject.has_commits?).to be_truthy
    end
  end

  describe '#has_no_commits?' do
    before do
1779 1780
      allow(subject.merge_request_diff).to receive(:commits_count)
        .and_return(0)
1781 1782 1783 1784 1785 1786
    end

    it 'returns true when merge request diff has 0 commits' do
      expect(subject.has_no_commits?).to be_truthy
    end
  end
1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805

  describe '#merge_request_diff_for' do
    subject { create(:merge_request, importing: true) }
    let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
    let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
    let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }

    context 'with diff refs' do
      it 'returns the diffs' do
        expect(subject.merge_request_diff_for(merge_request_diff1.diff_refs)).to eq(merge_request_diff1)
      end
    end

    context 'with a commit SHA' do
      it 'returns the diffs' do
        expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3)
      end
    end
  end
1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837

  describe '#version_params_for' do
    subject { create(:merge_request, importing: true) }
    let(:project) { subject.project }
    let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
    let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
    let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }

    context 'when the diff refs are for an older merge request version' do
      let(:diff_refs) { merge_request_diff1.diff_refs }

      it 'returns the diff ID for the version to show' do
        expect(subject.version_params_for(diff_refs)).to eq(diff_id: merge_request_diff1.id)
      end
    end

    context 'when the diff refs are for a comparison between merge request versions' do
      let(:diff_refs) { merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs }

      it 'returns the diff ID and start sha of the versions to compare' do
        expect(subject.version_params_for(diff_refs)).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha)
      end
    end

    context 'when the diff refs are not for a merge request version' do
      let(:diff_refs) { project.commit(sample_commit.id).diff_refs }

      it 'returns nil' do
        expect(subject.version_params_for(diff_refs)).to be_nil
      end
    end
  end
1838

1839
  describe '#fetch_ref!' do
micael.bergeron's avatar
micael.bergeron committed
1840
    it 'fetches the ref correctly' do
1841
      expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error
1842

1843 1844
      subject.fetch_ref!
      expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
1845 1846
    end
  end
1847 1848 1849 1850 1851 1852 1853 1854 1855

  describe 'removing a merge request' do
    it 'refreshes the number of open merge requests of the target project' do
      project = subject.target_project

      expect { subject.destroy }
        .to change { project.open_merge_requests_count }.from(1).to(0)
    end
  end
1856 1857 1858 1859

  it_behaves_like 'throttled touch' do
    subject { create(:merge_request, updated_at: 1.hour.ago) }
  end
1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875

  context 'state machine transitions' do
    describe '#unlock_mr' do
      subject { create(:merge_request, state: 'locked', merge_jid: 123) }

      it 'updates merge request head pipeline and sets merge_jid to nil' do
        pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha)

        subject.unlock_mr

        subject.reload
        expect(subject.head_pipeline).to eq(pipeline)
        expect(subject.merge_jid).to be_nil
      end
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1876
end