merge_request_spec.rb 36.7 KB
Newer Older
D
Dmitriy Zaporozhets 已提交
1 2
require 'spec_helper'

D
Douwe Maan 已提交
3
describe MergeRequest, models: true do
4 5
  include RepoHelpers

6 7
  subject { create(:merge_request) }

R
Robert Speicher 已提交
8 9 10
  describe 'associations' do
    it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') }
    it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') }
11
    it { is_expected.to belong_to(:merge_user).class_name("User") }
12
    it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) }
R
Robert Speicher 已提交
13 14
  end

15 16 17 18 19 20 21 22 23 24
  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

Z
Zeger-Jan van de Weg 已提交
25 26 27 28 29
  describe "act_as_paranoid" do
    it { is_expected.to have_db_column(:deleted_at) }
    it { is_expected.to have_db_index(:deleted_at) }
  end

30
  describe 'validation' do
31 32
    it { is_expected.to validate_presence_of(:target_branch) }
    it { is_expected.to validate_presence_of(:source_branch) }
Z
Zeger-Jan van de Weg 已提交
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50

    context "Validation of merge user with Merge When Build succeeds" do
      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
        subject.merge_when_build_succeeds = true
        expect(subject).not_to be_valid
      end

      it "is valid with merge user" do
        subject.merge_when_build_succeeds = true
        subject.merge_user = build(:user)

        expect(subject).to be_valid
      end
    end
D
Dmitriy Zaporozhets 已提交
51 52
  end

R
Robert Speicher 已提交
53
  describe 'respond to' do
54 55 56
    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?) }
57 58
    it { is_expected.to respond_to(:merge_params) }
    it { is_expected.to respond_to(:merge_when_build_succeeds) }
59
  end
A
Andrey Kumanyaev 已提交
60

61 62 63 64 65 66
  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

67
  describe '#target_branch_sha' do
68
    let(:project) { create(:project) }
69

70
    subject { create(:merge_request, source_project: project, target_project: project) }
71

72
    context 'when the target branch does not exist' do
73 74 75
      before do
        project.repository.raw_repository.delete_branch(subject.target_branch)
      end
76 77

      it 'returns nil' do
78
        expect(subject.target_branch_sha).to be_nil
79 80
      end
    end
81 82 83 84 85 86

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

      expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7'
    end
87 88
  end

89
  describe '#source_branch_sha' do
90 91 92 93 94
    let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }

    context 'with diffs' do
      subject { create(:merge_request, :with_diffs) }
      it 'returns the sha of the source branch last commit' do
95
        expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
96 97 98
      end
    end

99 100 101
    context 'without diffs' do
      subject { create(:merge_request, :without_diffs) }
      it 'returns the sha of the source branch last commit' do
102
        expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
103 104 105
      end
    end

106 107 108
    context 'when the merge request is being created' do
      subject { build(:merge_request, source_branch: nil, compare_commits: []) }
      it 'returns nil' do
109
        expect(subject.source_branch_sha).to be_nil
110 111
      end
    end
112 113 114 115 116 117

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

      expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
    end
118 119
  end

120 121 122 123 124 125 126 127 128
  describe '#to_reference' do
    it 'returns a String reference to the object' do
      expect(subject.to_reference).to eq "!#{subject.iid}"
    end

    it 'supports a cross-project reference' do
      cross = double('project')
      expect(subject.to_reference(cross)).to eq "#{subject.source_project.to_reference}!#{subject.iid}"
    end
129
  end
130

131
  describe '#raw_diffs' do
S
Sean McGivern 已提交
132 133 134 135 136 137 138
    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

139
        expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options)
S
Sean McGivern 已提交
140

141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
        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

S
Sean McGivern 已提交
156 157 158 159 160 161
  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
162
        merge_request.save
S
Sean McGivern 已提交
163

164
        expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
S
Sean McGivern 已提交
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180

        merge_request.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(:diffs).with(options)

        merge_request.diffs(options)
      end
    end
  end

181
  describe "#mr_and_commit_notes" do
182
    let!(:merge_request) { create(:merge_request) }
183 184

    before do
185
      allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] }
186 187
      create(:note_on_commit, commit_id: merge_request.commits.first.id,
                              project: merge_request.project)
D
Dmitriy Zaporozhets 已提交
188
      create(:note, noteable: merge_request, project: merge_request.project)
189 190
    end

191
    it "includes notes for commits" do
192 193
      expect(merge_request.commits).not_to be_empty
      expect(merge_request.mr_and_commit_notes.count).to eq(2)
194
    end
195

196
    it "includes notes for commits from target project as well" do
197 198 199
      create(:note_on_commit, commit_id: merge_request.commits.first.id,
                              project: merge_request.target_project)

200 201 202
      expect(merge_request.commits).not_to be_empty
      expect(merge_request.mr_and_commit_notes.count).to eq(3)
    end
203
  end
204 205 206

  describe '#is_being_reassigned?' do
    it 'returns true if the merge_request assignee has changed' do
207
      subject.assignee = create(:user)
208
      expect(subject.is_being_reassigned?).to be_truthy
209 210
    end
    it 'returns false if the merge request assignee has not changed' do
211
      expect(subject.is_being_reassigned?).to be_falsey
212 213
    end
  end
I
Izaak Alpert 已提交
214 215 216

  describe '#for_fork?' do
    it 'returns true if the merge request is for a fork' do
D
Dmitriy Zaporozhets 已提交
217 218
      subject.source_project = create(:project, namespace: create(:group))
      subject.target_project = create(:project, namespace: create(:group))
I
Izaak Alpert 已提交
219

220
      expect(subject.for_fork?).to be_truthy
I
Izaak Alpert 已提交
221
    end
D
Dmitriy Zaporozhets 已提交
222

I
Izaak Alpert 已提交
223
    it 'returns false if is not for a fork' do
224
      expect(subject.for_fork?).to be_falsey
I
Izaak Alpert 已提交
225 226 227
    end
  end

228 229 230
  describe 'detection of issues to be closed' do
    let(:issue0) { create :issue, project: subject.project }
    let(:issue1) { create :issue, project: subject.project }
231 232 233 234

    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}") }
235 236

    before do
237
      subject.project.team << [subject.author, :developer]
238
      allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
239 240 241
    end

    it 'accesses the set of issues that will be closed on acceptance' do
242 243
      allow(subject.project).to receive(:default_branch).
        and_return(subject.target_branch)
244

245 246 247
      closed = subject.closes_issues

      expect(closed).to include(issue0, issue1)
248 249 250
    end

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

254
      expect(subject.closes_issues).to be_empty
255
    end
256 257 258

    it 'detects issues mentioned in the description' do
      issue2 = create(:issue, project: subject.project)
259
      subject.description = "Closes #{issue2.to_reference}"
260 261
      allow(subject.project).to receive(:default_branch).
        and_return(subject.target_branch)
262

263
      expect(subject.closes_issues).to include(issue2)
264
    end
265 266
  end

267
  describe "#work_in_progress?" do
268 269 270
    ['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}"
271
        expect(subject.work_in_progress?).to eq true
272
      end
T
Ted Hogan 已提交
273 274
    end

275 276
    it "doesn't detect WIP for words starting with WIP" do
      subject.title = "Wipwap #{subject.title}"
277
      expect(subject.work_in_progress?).to eq false
278 279
    end

280 281
    it "doesn't detect WIP for words containing with WIP" do
      subject.title = "WupWipwap #{subject.title}"
282
      expect(subject.work_in_progress?).to eq false
283 284
    end

285
    it "doesn't detect WIP by default" do
286
      expect(subject.work_in_progress?).to eq false
287 288 289
    end
  end

Z
Zeger-Jan van de Weg 已提交
290
  describe '#can_remove_source_branch?' do
Z
Zeger-Jan van de Weg 已提交
291 292
    let(:user) { create(:user) }
    let(:user2) { create(:user) }
293 294 295 296

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

Z
Zeger-Jan van de Weg 已提交
297 298 299 300
      subject.source_branch = "feature"
      subject.target_branch = "master"
      subject.save!
    end
301

Z
Zeger-Jan van de Weg 已提交
302 303
    it "can't be removed when its a protected branch" do
      allow(subject.source_project).to receive(:protected_branch?).and_return(true)
304 305 306
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end

307
    it "can't remove a root ref" do
Z
Zeger-Jan van de Weg 已提交
308 309
      subject.source_branch = "master"
      subject.target_branch = "feature"
310 311 312 313

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

Z
Zeger-Jan van de Weg 已提交
314 315 316 317
    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

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

Z
Zeger-Jan van de Weg 已提交
321
      expect(subject.can_remove_source_branch?(user)).to be_truthy
322
    end
323 324

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

327 328
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end
329 330
  end

331
  describe "#reset_merge_when_build_succeeds" do
332 333 334 335
    let(:merge_if_green) do
      create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user),
                             merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" }
    end
Z
Zeger-Jan van de Weg 已提交
336

337 338 339 340 341
    it "sets the item to false" do
      merge_if_green.reset_merge_when_build_succeeds
      merge_if_green.reload

      expect(merge_if_green.merge_when_build_succeeds).to be_falsey
342 343
      expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
      expect(merge_if_green.merge_params["commit_message"]).to be_nil
344 345 346
    end
  end

347
  describe "#hook_attrs" do
348 349 350 351 352 353 354 355 356 357 358
    let(:attrs_hash) { subject.hook_attrs.to_h }

    [:source, :target].each do |key|
      describe "#{key} key" do
        include_examples 'project hook data', project_key: key do
          let(:data)    { attrs_hash }
          let(:project) { subject.send("#{key}_project") }
        end
      end
    end

359
    it "has all the required keys" do
360 361 362 363
      expect(attrs_hash).to include(:source)
      expect(attrs_hash).to include(:target)
      expect(attrs_hash).to include(:last_commit)
      expect(attrs_hash).to include(:work_in_progress)
364
    end
365 366 367 368 369 370
  end

  describe '#diverged_commits_count' do
    let(:project)      { create(:project) }
    let(:fork_project) { create(:project, forked_from_project: project) }

371
    context 'when the target branch does not exist anymore' do
372 373 374 375 376 377
      subject { create(:merge_request, source_project: project, target_project: project) }

      before do
        project.repository.raw_repository.delete_branch(subject.target_branch)
        subject.reload
      end
378 379 380 381 382 383 384 385 386 387

      it 'does not crash' do
        expect{ subject.diverged_commits_count }.not_to raise_error
      end

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

388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
    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
        expect(subject.diverged_commits_count).to eq(5)
      end
    end

    context 'diverged on fork' do
      subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) }

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

    context 'rebased on fork' do
      subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: fork_project, target_project: project) }

      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
      before(:example) do
        allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
      end

      it 'caches the output' do
        expect(subject).to receive(:compute_diverged_commits_count).
          once.
          and_return(2)

        subject.diverged_commits_count
        subject.diverged_commits_count
      end

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

        subject.diverged_commits_count
432
        allow(subject).to receive(:source_branch_sha).and_return('123abc')
433 434 435 436 437 438 439 440 441
        subject.diverged_commits_count
      end

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

        subject.diverged_commits_count
442
        allow(subject).to receive(:target_branch_sha).and_return('123abc')
443 444 445
        subject.diverged_commits_count
      end
    end
446 447
  end

448
  it_behaves_like 'an editable mentionable' do
449
    subject { create(:merge_request) }
450

451 452
    let(:backref_text) { "merge request #{subject.to_reference}" }
    let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
453
  end
V
Vinnie Okada 已提交
454 455

  it_behaves_like 'a Taskable' do
456
    subject { create :merge_request, :simple }
V
Vinnie Okada 已提交
457
  end
458

459 460 461 462 463 464
  describe '#commits_sha' do
    let(:commit0) { double('commit0', sha: 'sha1') }
    let(:commit1) { double('commit1', sha: 'sha2') }
    let(:commit2) { double('commit2', sha: 'sha3') }

    before do
465
      allow(subject.merge_request_diff).to receive(:commits).and_return([commit0, commit1, commit2])
466 467 468 469 470 471 472
    end

    it 'returns sha of commits' do
      expect(subject.commits_sha).to contain_exactly('sha1', 'sha2', 'sha3')
    end
  end

473
  describe '#pipeline' do
474
    describe 'when the source project exists' do
475
      it 'returns the latest pipeline' do
476
        pipeline = double(:ci_pipeline, ref: 'master')
477

478
        allow(subject).to receive(:diff_head_sha).and_return('123abc')
479

L
Lin Jen-Shin 已提交
480 481
        expect(subject.source_project).to receive(:pipeline_for).
          with('master', '123abc').
482
          and_return(pipeline)
483

484
        expect(subject.pipeline).to eq(pipeline)
485 486 487 488 489 490 491
      end
    end

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

492
        expect(subject.pipeline).to be_nil
493 494 495
      end
    end
  end
Y
Yorick Peterse 已提交
496

497
  describe '#all_pipelines' do
498
    shared_examples 'returning pipelines with proper ordering' do
499 500 501 502 503 504
      let!(:all_pipelines) do
        subject.all_commits_sha.map do |sha|
          create(:ci_empty_pipeline,
                 project: subject.source_project,
                 sha: sha,
                 ref: subject.source_branch)
505 506 507 508 509
        end
      end

      it 'returns all pipelines' do
        expect(subject.all_pipelines).not_to be_empty
510
        expect(subject.all_pipelines).to eq(all_pipelines.reverse)
511
      end
512 513
    end

514 515 516 517 518 519 520 521 522 523
    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
        subject.update(target_branch: 'markdown')
      end

      it_behaves_like 'returning pipelines with proper ordering'
524 525 526
    end
  end

527 528 529 530 531 532 533 534 535 536 537 538 539 540 541
  describe '#all_commits_sha' do
    let(:all_commits_sha) do
      subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
    end

    before do
      subject.update(target_branch: 'markdown')
    end

    it 'returns all SHA from all merge_request_diffs' do
      expect(subject.merge_request_diffs.size).to eq(2)
      expect(subject.all_commits_sha).to eq(all_commits_sha)
    end
  end

Y
Yorick Peterse 已提交
542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564
  describe '#participants' do
    let(:project) { create(:project, :public) }

    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
J
Josh Frye 已提交
565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581

  describe 'cached counts' do
    it 'updates when assignees change' do
      user1 = create(:user)
      user2 = create(:user)
      mr = create(:merge_request, assignee: user1)

      expect(user1.assigned_open_merge_request_count).to eq(1)
      expect(user2.assigned_open_merge_request_count).to eq(0)

      mr.assignee = user2
      mr.save

      expect(user1.assigned_open_merge_request_count).to eq(0)
      expect(user2.assigned_open_merge_request_count).to eq(1)
    end
  end
582 583 584 585 586 587 588 589 590

  describe '#check_if_can_be_merged' do
    let(:project) { create(:project, only_allow_merge_if_build_succeeds: true) }

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

    context 'when it is not broken and has no conflicts' do
      it 'is marked as mergeable' do
        allow(subject).to receive(:broken?) { false }
591
        allow(project.repository).to receive(:can_be_merged?).and_return(true)
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607

        expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
      end
    end

    context 'when broken' do
      before { allow(subject).to receive(:broken?) { true } }

      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 }
608
        allow(project.repository).to receive(:can_be_merged?).and_return(false)
609 610 611 612 613 614 615 616 617
      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
618 619 620 621
    let(:project) { create(:project) }

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

622 623
    it 'returns false if #mergeable_state? is false' do
      expect(subject).to receive(:mergeable_state?) { false }
624

625
      expect(subject.mergeable?).to be_falsey
626 627
    end

628
    it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
629 630
      allow(subject).to receive(:mergeable_state?) { true }
      expect(subject).to receive(:check_if_can_be_merged)
631
      expect(subject).to receive(:can_be_merged?) { true }
632 633 634 635 636 637 638

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

  describe '#mergeable_state?' do
    let(:project) { create(:project) }
639 640 641

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

642
    it 'checks if merge request can be merged' do
643
      allow(subject).to receive(:mergeable_ci_state?) { true }
644 645 646 647 648 649 650 651 652
      expect(subject).to receive(:check_if_can_be_merged)

      subject.mergeable?
    end

    context 'when not open' do
      before { subject.close }

      it 'returns false' do
653
        expect(subject.mergeable_state?).to be_falsey
654 655 656 657 658 659 660
      end
    end

    context 'when working in progress' do
      before { subject.title = 'WIP MR' }

      it 'returns false' do
661
        expect(subject.mergeable_state?).to be_falsey
662 663 664 665 666 667 668
      end
    end

    context 'when broken' do
      before { allow(subject).to receive(:broken?) { true } }

      it 'returns false' do
669
        expect(subject.mergeable_state?).to be_falsey
670 671 672 673 674 675
      end
    end

    context 'when failed' do
      before { allow(subject).to receive(:broken?) { false } }

676 677 678
      context 'when project settings restrict to merge only if build succeeds and build failed' do
        before do
          project.only_allow_merge_if_build_succeeds = true
679
          allow(subject).to receive(:mergeable_ci_state?) { false }
680 681 682 683
        end

        it 'returns false' do
          expect(subject.mergeable_state?).to be_falsey
684 685 686 687 688
        end
      end
    end
  end

689
  describe '#mergeable_ci_state?' do
690
    let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
R
Rémy Coutable 已提交
691
    let(:pipeline) { create(:ci_empty_pipeline) }
692 693 694

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

695
    context 'when it is only allowed to merge when build is green' do
R
Rémy Coutable 已提交
696
      context 'and a failed pipeline is associated' do
697
        before do
R
Rémy Coutable 已提交
698 699
          pipeline.statuses << create(:commit_status, status: 'failed', project: project)
          allow(subject).to receive(:pipeline) { pipeline }
700
        end
701

702
        it { expect(subject.mergeable_ci_state?).to be_falsey }
703 704
      end

R
Rémy Coutable 已提交
705
      context 'when no pipeline is associated' do
706
        before do
R
Rémy Coutable 已提交
707
          allow(subject).to receive(:pipeline) { nil }
708 709 710
        end

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

714
    context 'when merges are not restricted to green builds' do
715 716
      subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) }

R
Rémy Coutable 已提交
717
      context 'and a failed pipeline is associated' do
718
        before do
R
Rémy Coutable 已提交
719 720
          pipeline.statuses << create(:commit_status, status: 'failed', project: project)
          allow(subject).to receive(:pipeline) { pipeline }
721 722 723 724 725
        end

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

R
Rémy Coutable 已提交
726
      context 'when no pipeline is associated' do
727
        before do
R
Rémy Coutable 已提交
728
          allow(subject).to receive(:pipeline) { nil }
729 730 731
        end

        it { expect(subject.mergeable_ci_state?).to be_truthy }
732 733 734
      end
    end
  end
735

Z
Z.J. van de Weg 已提交
736 737 738 739 740
  describe "#environments" do
    let(:project)       { create(:project) }
    let(:merge_request) { create(:merge_request, source_project: project) }

    it 'selects deployed environments' do
741 742 743
      environments = create_list(:environment, 3, project: project)
      create(:deployment, environment: environments.first, sha: project.commit('master').id)
      create(:deployment, environment: environments.second, sha: project.commit('feature').id)
744

745 746 747 748 749 750 751 752 753 754 755
      expect(merge_request.environments).to eq [environments.first]
    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
        expect(merge_request.environments).to be_empty
      end
Z
Z.J. van de Weg 已提交
756 757 758
    end
  end

759 760 761 762 763
  describe "#reload_diff" do
    let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) }

    let(:commit) { subject.project.commit(sample_commit.id) }

764
    it "does not change existing merge request diff" do
765
      expect(subject.merge_request_diff).not_to receive(:save_git_content)
766 767 768
      subject.reload_diff
    end

769 770 771 772
    it "creates new merge request diff" do
      expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
    end

773 774 775 776 777 778
    it "executs diff cache service" do
      expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)

      subject.reload_diff
    end

779 780 781 782
    it "updates diff note positions" do
      old_diff_refs = subject.diff_refs

      # Update merge_request_diff so that #diff_refs will return commit.diff_refs
783 784 785 786 787 788
      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
        )
789 790

        subject.merge_request_diff(true)
791 792 793 794 795 796 797 798 799 800
      end

      expect(Notes::DiffPositionUpdateService).to receive(:new).with(
        subject.project,
        nil,
        old_diff_refs: old_diff_refs,
        new_diff_refs: commit.diff_refs,
        paths: note.position.paths
      ).and_call_original

801
      expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
802 803 804 805 806
      expect_any_instance_of(DiffNote).to receive(:save).once

      subject.reload_diff
    end
  end
807 808 809 810 811 812 813 814 815 816 817 818 819 820

  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
        subject.project.repository.raw_repository.delete_branch(subject.target_branch)
      end

      it 'returns nil' do
        expect(subject.branch_merge_base_commit).to be_nil
821 822 823 824
      end
    end
  end

825
  describe "#diff_sha_refs" do
826 827 828 829 830 831 832 833
    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)

834
        subject.diff_sha_refs
835 836 837 838 839 840 841 842 843
      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
        )

844
        expect(subject.diff_sha_refs).to eq(expected_diff_refs)
845 846 847
      end
    end
  end
848 849 850 851 852 853 854

  context "discussion status" do
    let(:first_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
    let(:second_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
    let(:third_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }

    before do
855
      allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion])
856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 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 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939
    end

    describe "#discussions_resolvable?" do
      context "when all discussions are unresolvable" do
        before do
          allow(first_discussion).to receive(:resolvable?).and_return(false)
          allow(second_discussion).to receive(:resolvable?).and_return(false)
          allow(third_discussion).to receive(:resolvable?).and_return(false)
        end

        it "returns false" do
          expect(subject.discussions_resolvable?).to be false
        end
      end

      context "when some discussions are unresolvable and some discussions are resolvable" do
        before do
          allow(first_discussion).to receive(:resolvable?).and_return(true)
          allow(second_discussion).to receive(:resolvable?).and_return(false)
          allow(third_discussion).to receive(:resolvable?).and_return(true)
        end

        it "returns true" do
          expect(subject.discussions_resolvable?).to be true
        end
      end

      context "when all discussions are resolvable" do
        before do
          allow(first_discussion).to receive(:resolvable?).and_return(true)
          allow(second_discussion).to receive(:resolvable?).and_return(true)
          allow(third_discussion).to receive(:resolvable?).and_return(true)
        end

        it "returns true" do
          expect(subject.discussions_resolvable?).to be true
        end
      end
    end

    describe "#discussions_resolved?" do
      context "when discussions are not resolvable" do
        before do
          allow(subject).to receive(:discussions_resolvable?).and_return(false)
        end

        it "returns false" do
          expect(subject.discussions_resolved?).to be false
        end
      end

      context "when discussions are resolvable" do
        before do
          allow(subject).to receive(:discussions_resolvable?).and_return(true)

          allow(first_discussion).to receive(:resolvable?).and_return(true)
          allow(second_discussion).to receive(:resolvable?).and_return(false)
          allow(third_discussion).to receive(:resolvable?).and_return(true)
        end

        context "when all resolvable discussions are resolved" do
          before do
            allow(first_discussion).to receive(:resolved?).and_return(true)
            allow(third_discussion).to receive(:resolved?).and_return(true)
          end

          it "returns true" do
            expect(subject.discussions_resolved?).to be true
          end
        end

        context "when some resolvable discussions are not resolved" do
          before do
            allow(first_discussion).to receive(:resolved?).and_return(true)
            allow(third_discussion).to receive(:resolved?).and_return(false)
          end

          it "returns false" do
            expect(subject.discussions_resolved?).to be false
          end
        end
      end
    end
  end
S
Sean McGivern 已提交
940

941
  describe '#conflicts_can_be_resolved_in_ui?' do
S
Sean McGivern 已提交
942 943 944 945 946 947 948 949 950 951
    def create_merge_request(source_branch)
      create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
        mr.mark_as_unmergeable
      end
    end

    it 'returns a falsey value when the MR can be merged without conflicts' do
      merge_request = create_merge_request('master')
      merge_request.mark_as_mergeable

952
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
953 954
    end

955 956 957 958 959 960
    it 'returns a falsey value when the MR is marked as having conflicts, but has none' do
      merge_request = create_merge_request('master')

      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
    end

961 962 963 964 965 966 967
    it 'returns a falsey value when the MR has a missing ref after a force push' do
      merge_request = create_merge_request('conflict-resolvable')
      allow(merge_request.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)

      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
    end

S
Sean McGivern 已提交
968 969 970 971
    it 'returns a falsey value when the MR does not support new diff notes' do
      merge_request = create_merge_request('conflict-resolvable')
      merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)

972
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
973 974 975 976 977
    end

    it 'returns a falsey value when the conflicts contain a large file' do
      merge_request = create_merge_request('conflict-too-large')

978
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
979 980 981 982 983
    end

    it 'returns a falsey value when the conflicts contain a binary file' do
      merge_request = create_merge_request('conflict-binary-file')

984
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
985 986 987 988 989
    end

    it 'returns a falsey value when the conflicts contain a file with ambiguous conflict markers' do
      merge_request = create_merge_request('conflict-contains-conflict-markers')

990
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
991 992 993 994 995
    end

    it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
      merge_request = create_merge_request('conflict-missing-side')

996
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
997 998 999 1000 1001
    end

    it 'returns a truthy value when the conflicts are resolvable in the UI' do
      merge_request = create_merge_request('conflict-resolvable')

1002
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
S
Sean McGivern 已提交
1003 1004
    end
  end
1005

K
Katarzyna Kobierska 已提交
1006
  describe "#forked_source_project_missing?" do
1007 1008 1009 1010 1011
    let(:project)      { create(:project) }
    let(:fork_project) { create(:project, forked_from_project: project) }
    let(:user)         { create(:user) }
    let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }

K
Katarzyna Kobierska 已提交
1012
    context "when the fork exists" do
1013 1014 1015 1016 1017 1018
      let(:merge_request) do
        create(:merge_request,
          source_project: fork_project,
          target_project: project)
      end

K
Katarzyna Kobierska 已提交
1019
      it { expect(merge_request.forked_source_project_missing?).to be_falsey }
1020 1021
    end

K
Katarzyna Kobierska 已提交
1022
    context "when the source project is the same as the target project" do
1023 1024
      let(:merge_request) { create(:merge_request, source_project: project) }

K
Katarzyna Kobierska 已提交
1025
      it { expect(merge_request.forked_source_project_missing?).to be_falsey }
1026 1027
    end

K
Katarzyna Kobierska 已提交
1028
    context "when the fork does not exist" do
1029 1030 1031 1032 1033 1034
      let(:merge_request) do
        create(:merge_request,
          source_project: fork_project,
          target_project: project)
      end

K
Katarzyna Kobierska 已提交
1035
      it "returns true" do
1036 1037 1038
        unlink_project.execute
        merge_request.reload

K
Katarzyna Kobierska 已提交
1039
        expect(merge_request.forked_source_project_missing?).to be_truthy
1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
      end
    end
  end

  describe "#closed_without_fork?" do
    let(:project)      { create(:project) }
    let(:fork_project) { create(:project, forked_from_project: project) }
    let(:user)         { create(:user) }
    let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }

K
Katarzyna Kobierska 已提交
1050
    context "when the merge request is closed" do
1051 1052 1053 1054 1055 1056
      let(:closed_merge_request) do
        create(:closed_merge_request,
          source_project: fork_project,
          target_project: project)
      end

K
Katarzyna Kobierska 已提交
1057
      it "returns false if the fork exist" do
1058 1059 1060
        expect(closed_merge_request.closed_without_fork?).to be_falsey
      end

K
Katarzyna Kobierska 已提交
1061
      it "returns true if the fork does not exist" do
1062 1063 1064 1065 1066 1067
        unlink_project.execute
        closed_merge_request.reload

        expect(closed_merge_request.closed_without_fork?).to be_truthy
      end
    end
K
Katarzyna Kobierska 已提交
1068

K
Katarzyna Kobierska 已提交
1069
    context "when the merge request is open" do
K
Katarzyna Kobierska 已提交
1070 1071 1072 1073 1074 1075 1076 1077 1078 1079
      let(:open_merge_request) do
        create(:merge_request,
          source_project: fork_project,
          target_project: project)
      end

      it "returns false" do
        expect(open_merge_request.closed_without_fork?).to be_falsey
      end
    end
1080
  end
1081

K
Katarzyna Kobierska 已提交
1082
  describe '#closed_without_source_project?' do
1083 1084
    let(:project)      { create(:project) }
    let(:user)         { create(:user) }
1085
    let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
1086
    let(:destroy_service) { Projects::DestroyService.new(fork_project, user) }
1087

K
Katarzyna Kobierska 已提交
1088
    context 'when the merge request is closed' do
1089 1090 1091 1092 1093 1094
      let(:closed_merge_request) do
        create(:closed_merge_request,
          source_project: fork_project,
          target_project: project)
      end

K
Katarzyna Kobierska 已提交
1095
      it 'returns false if the source project exists' do
1096 1097 1098
        expect(closed_merge_request.closed_without_source_project?).to be_falsey
      end

K
Katarzyna Kobierska 已提交
1099
      it 'returns true if the source project does not exist' do
1100
        destroy_service.execute
1101 1102 1103 1104 1105 1106
        closed_merge_request.reload

        expect(closed_merge_request.closed_without_source_project?).to be_truthy
      end
    end

K
Katarzyna Kobierska 已提交
1107 1108 1109
    context 'when the merge request is open' do
      it 'returns false' do
        expect(subject.closed_without_source_project?).to be_falsey
1110 1111 1112
      end
    end
  end
K
Katarzyna Kobierska 已提交
1113

1114
  describe '#reopenable?' do
K
Katarzyna Kobierska 已提交
1115 1116 1117
    context 'when the merge request is closed' do
      it 'returns true' do
        subject.close
K
Katarzyna Kobierska 已提交
1118

1119
        expect(subject.reopenable?).to be_truthy
K
Katarzyna Kobierska 已提交
1120 1121 1122 1123 1124
      end

      context 'forked project' do
        let(:project)      { create(:project) }
        let(:user)         { create(:user) }
1125
        let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
K
Katarzyna Kobierska 已提交
1126 1127 1128 1129 1130 1131 1132 1133 1134
        let(:merge_request) do
          create(:closed_merge_request,
            source_project: fork_project,
            target_project: project)
        end

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

1135
          expect(merge_request.reload.reopenable?).to be_falsey
K
Katarzyna Kobierska 已提交
1136 1137 1138
        end

        it 'returns false if the source project is deleted' do
1139
          Projects::DestroyService.new(fork_project, user).execute
K
Katarzyna Kobierska 已提交
1140

1141
          expect(merge_request.reload.reopenable?).to be_falsey
K
Katarzyna Kobierska 已提交
1142 1143
        end

K
Katarzyna Kobierska 已提交
1144
        it 'returns false if the merge request is merged' do
K
Katarzyna Kobierska 已提交
1145 1146
          merge_request.update_attributes(state: 'merged')

1147
          expect(merge_request.reload.reopenable?).to be_falsey
K
Katarzyna Kobierska 已提交
1148 1149
        end
      end
K
Katarzyna Kobierska 已提交
1150 1151
    end

K
Katarzyna Kobierska 已提交
1152
    context 'when the merge request is opened' do
K
Katarzyna Kobierska 已提交
1153
      it 'returns false' do
1154
        expect(subject.reopenable?).to be_falsey
K
Katarzyna Kobierska 已提交
1155
      end
K
Katarzyna Kobierska 已提交
1156 1157
    end
  end
D
Dmitriy Zaporozhets 已提交
1158
end