merge_request_spec.rb 41.2 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

T
Thomas Balthazar 已提交
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
  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
    end 

    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

Z
Zeger-Jan van de Weg 已提交
330
  describe '#can_remove_source_branch?' do
Z
Zeger-Jan van de Weg 已提交
331 332
    let(:user) { create(:user) }
    let(:user2) { create(:user) }
333 334 335 336

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

Z
Zeger-Jan van de Weg 已提交
337 338 339 340
      subject.source_branch = "feature"
      subject.target_branch = "master"
      subject.save!
    end
341

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

347
    it "can't remove a root ref" do
Z
Zeger-Jan van de Weg 已提交
348 349
      subject.source_branch = "master"
      subject.target_branch = "feature"
350 351 352 353

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

Z
Zeger-Jan van de Weg 已提交
354 355 356 357
    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

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

Z
Zeger-Jan van de Weg 已提交
361
      expect(subject.can_remove_source_branch?(user)).to be_truthy
362
    end
363 364

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

367 368
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end
369 370
  end

371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
  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

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

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

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

      expect(request.merge_commit_message)
        .to match("See merge request #{request.to_reference}")
    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
  end

407
  describe "#reset_merge_when_build_succeeds" do
408 409 410 411
    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 已提交
412

413 414 415 416 417
    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
418 419
      expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
      expect(merge_if_green.merge_params["commit_message"]).to be_nil
420 421 422
    end
  end

423
  describe "#hook_attrs" do
424 425 426 427 428 429 430 431 432 433 434
    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

435
    it "has all the required keys" do
436 437 438 439
      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)
440
    end
441 442 443 444 445 446
  end

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

447
    context 'when the target branch does not exist anymore' do
448 449 450 451 452 453
      subject { create(:merge_request, source_project: project, target_project: project) }

      before do
        project.repository.raw_repository.delete_branch(subject.target_branch)
        subject.reload
      end
454 455 456 457 458 459 460 461 462 463

      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

464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
    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
508
        allow(subject).to receive(:source_branch_sha).and_return('123abc')
509 510 511 512 513 514 515 516 517
        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
518
        allow(subject).to receive(:target_branch_sha).and_return('123abc')
519 520 521
        subject.diverged_commits_count
      end
    end
522 523
  end

524
  it_behaves_like 'an editable mentionable' do
525
    subject { create(:merge_request) }
526

527 528
    let(:backref_text) { "merge request #{subject.to_reference}" }
    let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
529
  end
V
Vinnie Okada 已提交
530 531

  it_behaves_like 'a Taskable' do
532
    subject { create :merge_request, :simple }
V
Vinnie Okada 已提交
533
  end
534

535 536 537 538 539 540
  describe '#commits_sha' do
    let(:commit0) { double('commit0', sha: 'sha1') }
    let(:commit1) { double('commit1', sha: 'sha2') }
    let(:commit2) { double('commit2', sha: 'sha3') }

    before do
541
      allow(subject.merge_request_diff).to receive(:commits).and_return([commit0, commit1, commit2])
542 543 544 545 546 547 548
    end

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

549
  describe '#pipeline' do
550
    describe 'when the source project exists' do
551
      it 'returns the latest pipeline' do
552
        pipeline = double(:ci_pipeline, ref: 'master')
553

554
        allow(subject).to receive(:diff_head_sha).and_return('123abc')
555

L
Lin Jen-Shin 已提交
556 557
        expect(subject.source_project).to receive(:pipeline_for).
          with('master', '123abc').
558
          and_return(pipeline)
559

560
        expect(subject.pipeline).to eq(pipeline)
561 562 563 564 565 566 567
      end
    end

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

568
        expect(subject.pipeline).to be_nil
569 570 571
      end
    end
  end
Y
Yorick Peterse 已提交
572

573
  describe '#all_pipelines' do
574
    shared_examples 'returning pipelines with proper ordering' do
575 576 577 578 579 580
      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)
581 582 583 584 585
        end
      end

      it 'returns all pipelines' do
        expect(subject.all_pipelines).not_to be_empty
586
        expect(subject.all_pipelines).to eq(all_pipelines.reverse)
587
      end
588 589
    end

590 591 592 593 594 595
    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
596
        subject.update(target_branch: 'v1.0.0')
597 598 599
      end

      it_behaves_like 'returning pipelines with proper ordering'
600
    end
601 602

    context 'with unsaved merge request' do
603
      subject { build(:merge_request) }
604 605 606

      let!(:pipeline) do
        create(:ci_empty_pipeline,
607
               project: subject.project,
608 609
               sha: subject.diff_head_sha,
               ref: subject.source_branch)
610
      end
611 612 613 614 615

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

618 619 620 621 622
  describe '#all_commits_sha' do
    let(:all_commits_sha) do
      subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
    end

623 624 625 626 627 628 629 630 631 632 633 634 635
    shared_examples 'returning all SHA' do
      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

    context 'with a completely different branch' do
      before do
        subject.update(target_branch: 'v1.0.0')
      end

      it_behaves_like 'returning all SHA'
636 637
    end

638 639 640 641 642 643 644
    context 'with a branch having no difference' do
      before do
        subject.update(target_branch: 'v1.1.0')
        subject.reload # make sure commits were not cached
      end

      it_behaves_like 'returning all SHA'
645 646 647
    end
  end

Y
Yorick Peterse 已提交
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
  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 已提交
671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687

  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
688 689 690 691 692 693 694 695 696

  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 }
697
        allow(project.repository).to receive(:can_be_merged?).and_return(true)
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713

        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 }
714
        allow(project.repository).to receive(:can_be_merged?).and_return(false)
715 716 717 718 719 720 721 722 723
      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
724 725 726 727
    let(:project) { create(:project) }

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

728 729
    it 'returns false if #mergeable_state? is false' do
      expect(subject).to receive(:mergeable_state?) { false }
730

731
      expect(subject.mergeable?).to be_falsey
732 733
    end

734
    it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
735 736
      allow(subject).to receive(:mergeable_state?) { true }
      expect(subject).to receive(:check_if_can_be_merged)
737
      expect(subject).to receive(:can_be_merged?) { true }
738 739 740 741 742 743 744

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

  describe '#mergeable_state?' do
    let(:project) { create(:project) }
745 746 747

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

748
    it 'checks if merge request can be merged' do
749
      allow(subject).to receive(:mergeable_ci_state?) { true }
750 751 752 753 754 755 756 757 758
      expect(subject).to receive(:check_if_can_be_merged)

      subject.mergeable?
    end

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

      it 'returns false' do
759
        expect(subject.mergeable_state?).to be_falsey
760 761 762 763 764 765 766
      end
    end

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

      it 'returns false' do
767
        expect(subject.mergeable_state?).to be_falsey
768 769 770 771 772 773 774
      end
    end

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

      it 'returns false' do
775
        expect(subject.mergeable_state?).to be_falsey
776 777 778 779 780 781
      end
    end

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

782 783 784
      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
785
          allow(subject).to receive(:mergeable_ci_state?) { false }
786 787 788 789
        end

        it 'returns false' do
          expect(subject.mergeable_state?).to be_falsey
790 791 792 793 794
        end
      end
    end
  end

795
  describe '#mergeable_ci_state?' do
796
    let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
R
Rémy Coutable 已提交
797
    let(:pipeline) { create(:ci_empty_pipeline) }
798 799 800

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

801
    context 'when it is only allowed to merge when build is green' do
R
Rémy Coutable 已提交
802
      context 'and a failed pipeline is associated' do
803
        before do
R
Rémy Coutable 已提交
804 805
          pipeline.statuses << create(:commit_status, status: 'failed', project: project)
          allow(subject).to receive(:pipeline) { pipeline }
806
        end
807

808
        it { expect(subject.mergeable_ci_state?).to be_falsey }
809 810
      end

R
Rémy Coutable 已提交
811
      context 'when no pipeline is associated' do
812
        before do
R
Rémy Coutable 已提交
813
          allow(subject).to receive(:pipeline) { nil }
814 815 816
        end

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

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

R
Rémy Coutable 已提交
823
      context 'and a failed pipeline is associated' do
824
        before do
R
Rémy Coutable 已提交
825 826
          pipeline.statuses << create(:commit_status, status: 'failed', project: project)
          allow(subject).to receive(:pipeline) { pipeline }
827 828 829 830 831
        end

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

R
Rémy Coutable 已提交
832
      context 'when no pipeline is associated' do
833
        before do
R
Rémy Coutable 已提交
834
          allow(subject).to receive(:pipeline) { nil }
835 836 837
        end

        it { expect(subject.mergeable_ci_state?).to be_truthy }
838 839 840
      end
    end
  end
841

842
  describe '#environments' do
Z
Z.J. van de Weg 已提交
843 844 845
    let(:project)       { create(:project) }
    let(:merge_request) { create(:merge_request, source_project: project) }

846 847
    context 'with multiple environments' do
      let(:environments) { create_list(:environment, 3, project: project) }
848

849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864
      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
        expect(merge_request.environments).to contain_exactly(environments.first)
      end
    end

    context 'with environments on source project' do
      let(:source_project) do
        create(:project) do |fork_project|
          fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
        end
      end
K
Kamil Trzcinski 已提交
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
      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
        expect(merge_request.environments).to contain_exactly(source_environment)
      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
          expect(merge_request.environments).to contain_exactly(source_environment, target_environment)
        end
      end
893 894 895 896 897 898 899 900 901 902
    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 已提交
903 904 905
    end
  end

906 907 908 909 910
  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) }

911
    it "does not change existing merge request diff" do
912
      expect(subject.merge_request_diff).not_to receive(:save_git_content)
913 914 915
      subject.reload_diff
    end

916 917 918 919
    it "creates new merge request diff" do
      expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
    end

920 921 922 923 924 925
    it "executs diff cache service" do
      expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)

      subject.reload_diff
    end

926 927 928 929
    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
930 931 932 933 934 935
      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
        )
936 937

        subject.merge_request_diff(true)
938 939 940 941 942 943 944 945 946 947
      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

948
      expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
949 950 951 952 953
      expect_any_instance_of(DiffNote).to receive(:save).once

      subject.reload_diff
    end
  end
954 955 956 957 958 959 960 961 962 963 964 965 966 967

  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
968 969 970 971
      end
    end
  end

972
  describe "#diff_sha_refs" do
973 974 975 976 977 978 979 980
    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)

981
        subject.diff_sha_refs
982 983 984 985 986 987 988 989 990
      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
        )

991
        expect(subject.diff_sha_refs).to eq(expected_diff_refs)
992 993 994
      end
    end
  end
995 996 997 998 999 1000 1001

  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
1002
      allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion])
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086
    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 已提交
1087

1088
  describe '#conflicts_can_be_resolved_in_ui?' do
S
Sean McGivern 已提交
1089 1090 1091 1092 1093 1094 1095 1096 1097 1098
    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

1099
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
1100 1101
    end

1102 1103 1104 1105 1106 1107
    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

1108 1109 1110 1111 1112 1113 1114
    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 已提交
1115 1116 1117 1118
    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)

1119
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
1120 1121 1122 1123 1124
    end

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

1125
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
1126 1127 1128 1129 1130
    end

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

1131
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
1132 1133 1134 1135 1136
    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')

1137
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
1138 1139 1140 1141 1142
    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')

1143
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
S
Sean McGivern 已提交
1144 1145 1146 1147 1148
    end

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

1149
      expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
S
Sean McGivern 已提交
1150 1151
    end
  end
1152

K
Katarzyna Kobierska 已提交
1153
  describe "#forked_source_project_missing?" do
1154 1155 1156 1157 1158
    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 已提交
1159
    context "when the fork exists" do
1160 1161 1162 1163 1164 1165
      let(:merge_request) do
        create(:merge_request,
          source_project: fork_project,
          target_project: project)
      end

K
Katarzyna Kobierska 已提交
1166
      it { expect(merge_request.forked_source_project_missing?).to be_falsey }
1167 1168
    end

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

K
Katarzyna Kobierska 已提交
1172
      it { expect(merge_request.forked_source_project_missing?).to be_falsey }
1173 1174
    end

K
Katarzyna Kobierska 已提交
1175
    context "when the fork does not exist" do
1176 1177 1178 1179 1180 1181
      let(:merge_request) do
        create(:merge_request,
          source_project: fork_project,
          target_project: project)
      end

K
Katarzyna Kobierska 已提交
1182
      it "returns true" do
1183 1184 1185
        unlink_project.execute
        merge_request.reload

K
Katarzyna Kobierska 已提交
1186
        expect(merge_request.forked_source_project_missing?).to be_truthy
1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
      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 已提交
1197
    context "when the merge request is closed" do
1198 1199 1200 1201 1202 1203
      let(:closed_merge_request) do
        create(:closed_merge_request,
          source_project: fork_project,
          target_project: project)
      end

K
Katarzyna Kobierska 已提交
1204
      it "returns false if the fork exist" do
1205 1206 1207
        expect(closed_merge_request.closed_without_fork?).to be_falsey
      end

K
Katarzyna Kobierska 已提交
1208
      it "returns true if the fork does not exist" do
1209 1210 1211 1212 1213 1214
        unlink_project.execute
        closed_merge_request.reload

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

K
Katarzyna Kobierska 已提交
1216
    context "when the merge request is open" do
K
Katarzyna Kobierska 已提交
1217 1218 1219 1220 1221 1222 1223 1224 1225 1226
      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
1227
  end
1228

K
Katarzyna Kobierska 已提交
1229
  describe '#closed_without_source_project?' do
1230 1231
    let(:project)      { create(:project) }
    let(:user)         { create(:user) }
1232
    let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
1233
    let(:destroy_service) { Projects::DestroyService.new(fork_project, user) }
1234

K
Katarzyna Kobierska 已提交
1235
    context 'when the merge request is closed' do
1236 1237 1238 1239 1240 1241
      let(:closed_merge_request) do
        create(:closed_merge_request,
          source_project: fork_project,
          target_project: project)
      end

K
Katarzyna Kobierska 已提交
1242
      it 'returns false if the source project exists' do
1243 1244 1245
        expect(closed_merge_request.closed_without_source_project?).to be_falsey
      end

K
Katarzyna Kobierska 已提交
1246
      it 'returns true if the source project does not exist' do
1247
        destroy_service.execute
1248 1249 1250 1251 1252 1253
        closed_merge_request.reload

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

K
Katarzyna Kobierska 已提交
1254 1255 1256
    context 'when the merge request is open' do
      it 'returns false' do
        expect(subject.closed_without_source_project?).to be_falsey
1257 1258 1259
      end
    end
  end
K
Katarzyna Kobierska 已提交
1260

1261
  describe '#reopenable?' do
K
Katarzyna Kobierska 已提交
1262 1263 1264
    context 'when the merge request is closed' do
      it 'returns true' do
        subject.close
K
Katarzyna Kobierska 已提交
1265

1266
        expect(subject.reopenable?).to be_truthy
K
Katarzyna Kobierska 已提交
1267 1268 1269 1270 1271
      end

      context 'forked project' do
        let(:project)      { create(:project) }
        let(:user)         { create(:user) }
1272
        let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
K
Katarzyna Kobierska 已提交
1273 1274 1275 1276 1277 1278 1279 1280 1281
        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

1282
          expect(merge_request.reload.reopenable?).to be_falsey
K
Katarzyna Kobierska 已提交
1283 1284 1285
        end

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

1288
          expect(merge_request.reload.reopenable?).to be_falsey
K
Katarzyna Kobierska 已提交
1289 1290
        end

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

1294
          expect(merge_request.reload.reopenable?).to be_falsey
K
Katarzyna Kobierska 已提交
1295 1296
        end
      end
K
Katarzyna Kobierska 已提交
1297 1298
    end

K
Katarzyna Kobierska 已提交
1299
    context 'when the merge request is opened' do
K
Katarzyna Kobierska 已提交
1300
      it 'returns false' do
1301
        expect(subject.reopenable?).to be_falsey
K
Katarzyna Kobierska 已提交
1302
      end
K
Katarzyna Kobierska 已提交
1303 1304
    end
  end
D
Dmitriy Zaporozhets 已提交
1305
end