BigW Consortium Gitlab

merge_request_spec.rb 19.3 KB
Newer Older
1 2
require 'spec_helper'

Douwe Maan committed
3
describe MergeRequest, models: true do
4 5
  subject { create(:merge_request) }

6 7 8
  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') }
9
    it { is_expected.to belong_to(:merge_user).class_name("User") }
10 11 12
    it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) }
  end

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

23 24 25 26 27
  describe "act_as_paranoid" do
    it { is_expected.to have_db_column(:deleted_at) }
    it { is_expected.to have_db_index(:deleted_at) }
  end

28
  describe 'validation' do
29 30
    it { is_expected.to validate_presence_of(:target_branch) }
    it { is_expected.to validate_presence_of(:source_branch) }
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

    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
49 50
  end

51
  describe 'respond to' do
52 53 54
    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?) }
55 56
    it { is_expected.to respond_to(:merge_params) }
    it { is_expected.to respond_to(:merge_when_build_succeeds) }
57
  end
58

59 60 61 62 63 64
  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

65 66
  describe '#target_sha' do
    context 'when the target branch does not exist anymore' do
67 68 69 70 71 72 73
      let(:project) { create(:project) }

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

      before do
        project.repository.raw_repository.delete_branch(subject.target_branch)
      end
74 75 76 77 78 79 80

      it 'returns nil' do
        expect(subject.target_sha).to be_nil
      end
    end
  end

81 82 83 84 85 86 87 88 89 90
  describe '#source_sha' do
    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
        expect(subject.source_sha).to eq(last_branch_commit.sha)
      end
    end

91 92 93 94 95 96 97
    context 'without diffs' do
      subject { create(:merge_request, :without_diffs) }
      it 'returns the sha of the source branch last commit' do
        expect(subject.source_sha).to eq(last_branch_commit.sha)
      end
    end

98 99 100 101 102 103 104 105
    context 'when the merge request is being created' do
      subject { build(:merge_request, source_branch: nil, compare_commits: []) }
      it 'returns nil' do
        expect(subject.source_sha).to be_nil
      end
    end
  end

106 107 108 109 110 111 112 113 114
  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
115
  end
116 117

  describe "#mr_and_commit_notes" do
118
    let!(:merge_request) { create(:merge_request) }
119 120

    before do
121
      allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] }
122 123
      create(:note_on_commit, commit_id: merge_request.commits.first.id,
                              project: merge_request.project)
124
      create(:note, noteable: merge_request, project: merge_request.project)
125 126 127
    end

    it "should include notes for commits" do
128 129
      expect(merge_request.commits).not_to be_empty
      expect(merge_request.mr_and_commit_notes.count).to eq(2)
130
    end
131 132

    it "should include notes for commits from target project as well" do
133 134 135
      create(:note_on_commit, commit_id: merge_request.commits.first.id,
                              project: merge_request.target_project)

136 137 138
      expect(merge_request.commits).not_to be_empty
      expect(merge_request.mr_and_commit_notes.count).to eq(3)
    end
139
  end
140 141 142

  describe '#is_being_reassigned?' do
    it 'returns true if the merge_request assignee has changed' do
143
      subject.assignee = create(:user)
144
      expect(subject.is_being_reassigned?).to be_truthy
145 146
    end
    it 'returns false if the merge request assignee has not changed' do
147
      expect(subject.is_being_reassigned?).to be_falsey
148 149
    end
  end
150 151 152

  describe '#for_fork?' do
    it 'returns true if the merge request is for a fork' do
Dmitriy Zaporozhets committed
153 154
      subject.source_project = create(:project, namespace: create(:group))
      subject.target_project = create(:project, namespace: create(:group))
155

156
      expect(subject.for_fork?).to be_truthy
157
    end
Dmitriy Zaporozhets committed
158

159
    it 'returns false if is not for a fork' do
160
      expect(subject.for_fork?).to be_falsey
161 162 163
    end
  end

164 165 166
  describe 'detection of issues to be closed' do
    let(:issue0) { create :issue, project: subject.project }
    let(:issue1) { create :issue, project: subject.project }
167 168 169 170

    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}") }
171 172

    before do
173
      subject.project.team << [subject.author, :developer]
174
      allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
175 176 177
    end

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

181 182 183
      closed = subject.closes_issues

      expect(closed).to include(issue0, issue1)
184 185 186
    end

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

190
      expect(subject.closes_issues).to be_empty
191
    end
192 193 194

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

199
      expect(subject.closes_issues).to include(issue2)
200
    end
201 202
  end

203
  describe "#work_in_progress?" do
204 205 206
    ['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}"
207
        expect(subject.work_in_progress?).to eq true
208
      end
209 210
    end

211 212
    it "doesn't detect WIP for words starting with WIP" do
      subject.title = "Wipwap #{subject.title}"
213
      expect(subject.work_in_progress?).to eq false
214 215
    end

216 217
    it "doesn't detect WIP for words containing with WIP" do
      subject.title = "WupWipwap #{subject.title}"
218
      expect(subject.work_in_progress?).to eq false
219 220
    end

221
    it "doesn't detect WIP by default" do
222
      expect(subject.work_in_progress?).to eq false
223 224 225
    end
  end

Zeger-Jan van de Weg committed
226
  describe '#can_remove_source_branch?' do
227 228
    let(:user) { create(:user) }
    let(:user2) { create(:user) }
229 230 231 232

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

233 234 235 236
      subject.source_branch = "feature"
      subject.target_branch = "master"
      subject.save!
    end
237

238 239
    it "can't be removed when its a protected branch" do
      allow(subject.source_project).to receive(:protected_branch?).and_return(true)
240 241 242 243
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end

    it "cant remove a root ref" do
244 245
      subject.source_branch = "master"
      subject.target_branch = "feature"
246 247 248 249

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

250 251 252 253
    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

254 255 256
    it "can be removed if the last commit is the head of the source branch" do
      allow(subject.source_project).to receive(:commit).and_return(subject.last_commit)

257
      expect(subject.can_remove_source_branch?(user)).to be_truthy
258
    end
259 260 261 262

    it "cannot be removed if the last commit is not also the head of the source branch" do
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end
263 264
  end

265
  describe "#reset_merge_when_build_succeeds" do
266 267 268 269
    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
270

271 272 273 274 275
    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
276 277
      expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
      expect(merge_if_green.merge_params["commit_message"]).to be_nil
278 279 280
    end
  end

281
  describe "#hook_attrs" do
282 283 284 285 286 287 288 289 290 291 292
    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

293
    it "has all the required keys" do
294 295 296 297
      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)
298
    end
299 300 301 302 303 304
  end

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

305
    context 'when the target branch does not exist anymore' do
306 307 308 309 310 311
      subject { create(:merge_request, source_project: project, target_project: project) }

      before do
        project.repository.raw_repository.delete_branch(subject.target_branch)
        subject.reload
      end
312 313 314 315 316 317 318 319 320 321

      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

322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
    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
        allow(subject).to receive(:source_sha).and_return('123abc')
        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
        allow(subject).to receive(:target_sha).and_return('123abc')
        subject.diverged_commits_count
      end
    end
380 381
  end

382
  it_behaves_like 'an editable mentionable' do
383
    subject { create(:merge_request) }
384

385 386
    let(:backref_text) { "merge request #{subject.to_reference}" }
    let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
387
  end
Vinnie Okada committed
388 389

  it_behaves_like 'a Taskable' do
390
    subject { create :merge_request, :simple }
Vinnie Okada committed
391
  end
392

393
  describe '#pipeline' do
394 395
    describe 'when the source project exists' do
      it 'returns the latest commit' do
396
        commit   = double(:commit, id: '123abc')
397
        pipeline = double(:ci_pipeline, ref: 'master')
398 399 400

        allow(subject).to receive(:last_commit).and_return(commit)

401
        expect(subject.source_project).to receive(:pipeline).
Kamil Trzcinski committed
402
          with('123abc', 'master').
403
          and_return(pipeline)
404

405
        expect(subject.pipeline).to eq(pipeline)
406 407 408 409 410 411 412
      end
    end

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

413
        expect(subject.pipeline).to be_nil
414 415 416
      end
    end
  end
Yorick Peterse committed
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440

  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
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457

  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
458 459 460 461 462 463 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

  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 }
        allow(project).to receive_message_chain(:repository, :can_be_merged?) { true }

        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 }
        allow(project).to receive_message_chain(:repository, :can_be_merged?) { false }
      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
494 495 496 497
    let(:project) { create(:project) }

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

498 499
    it 'returns false if #mergeable_state? is false' do
      expect(subject).to receive(:mergeable_state?) { false }
500

501
      expect(subject.mergeable?).to be_falsey
502 503
    end

504
    it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
505 506
      allow(subject).to receive(:mergeable_state?) { true }
      expect(subject).to receive(:check_if_can_be_merged)
507
      expect(subject).to receive(:can_be_merged?) { true }
508 509 510 511 512 513 514

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

  describe '#mergeable_state?' do
    let(:project) { create(:project) }
515 516 517

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

518
    it 'checks if merge request can be merged' do
519
      allow(subject).to receive(:mergeable_ci_state?) { true }
520 521 522 523 524 525 526 527 528
      expect(subject).to receive(:check_if_can_be_merged)

      subject.mergeable?
    end

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

      it 'returns false' do
529
        expect(subject.mergeable_state?).to be_falsey
530 531 532 533 534 535 536
      end
    end

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

      it 'returns false' do
537
        expect(subject.mergeable_state?).to be_falsey
538 539 540 541 542 543 544
      end
    end

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

      it 'returns false' do
545
        expect(subject.mergeable_state?).to be_falsey
546 547 548 549 550 551
      end
    end

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

552 553 554
      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
555
          allow(subject).to receive(:mergeable_ci_state?) { false }
556 557 558 559
        end

        it 'returns false' do
          expect(subject.mergeable_state?).to be_falsey
560 561 562 563 564
        end
      end
    end
  end

565
  describe '#mergeable_ci_state?' do
566
    let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
567
    let(:pipeline) { create(:ci_empty_pipeline) }
568 569 570

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

571
    context 'when it is only allowed to merge when build is green' do
572
      context 'and a failed pipeline is associated' do
573
        before do
574 575
          pipeline.statuses << create(:commit_status, status: 'failed', project: project)
          allow(subject).to receive(:pipeline) { pipeline }
576
        end
577

578
        it { expect(subject.mergeable_ci_state?).to be_falsey }
579 580
      end

581
      context 'when no pipeline is associated' do
582
        before do
583
          allow(subject).to receive(:pipeline) { nil }
584 585 586
        end

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

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

593
      context 'and a failed pipeline is associated' do
594
        before do
595 596
          pipeline.statuses << create(:commit_status, status: 'failed', project: project)
          allow(subject).to receive(:pipeline) { pipeline }
597 598 599 600 601
        end

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

602
      context 'when no pipeline is associated' do
603
        before do
604
          allow(subject).to receive(:pipeline) { nil }
605 606 607
        end

        it { expect(subject.mergeable_ci_state?).to be_truthy }
608 609 610
      end
    end
  end
611
end