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

D
Douwe Maan 已提交
3
describe MergeRequest, models: true do
4 5
  subject { create(:merge_request) }

R
Robert Speicher 已提交
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") }
R
Robert Speicher 已提交
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

Z
Zeger-Jan van de Weg 已提交
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) }
Z
Zeger-Jan van de Weg 已提交
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
D
Dmitriy Zaporozhets 已提交
49 50
  end

R
Robert Speicher 已提交
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
A
Andrey Kumanyaev 已提交
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)
D
Dmitriy Zaporozhets 已提交
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
I
Izaak Alpert 已提交
150 151 152

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

156
      expect(subject.for_fork?).to be_truthy
I
Izaak Alpert 已提交
157
    end
D
Dmitriy Zaporozhets 已提交
158

I
Izaak Alpert 已提交
159
    it 'returns false if is not for a fork' do
160
      expect(subject.for_fork?).to be_falsey
I
Izaak Alpert 已提交
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
T
Ted Hogan 已提交
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

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

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

Z
Zeger-Jan van de Weg 已提交
233 234 235 236
      subject.source_branch = "feature"
      subject.target_branch = "master"
      subject.save!
    end
237

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

Z
Zeger-Jan van de Weg 已提交
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)

Z
Zeger-Jan van de Weg 已提交
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
Z
Zeger-Jan van de Weg 已提交
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
V
Vinnie Okada 已提交
388 389

  it_behaves_like 'a Taskable' do
390
    subject { create :merge_request, :simple }
V
Vinnie Okada 已提交
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).
K
Kamil Trzcinski 已提交
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
Y
Yorick Peterse 已提交
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
J
Josh Frye 已提交
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 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
    let(:project) { create(:project) }

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

    it 'calls mergeable_state?' do
      expect(subject).to receive(:mergeable_state?)

      expect(subject.mergeable?).to be_truthy
    end

    it 'calls check_if_can_be_merged' do
      allow(subject).to receive(:mergeable_state?) { true }
      expect(subject).to receive(:check_if_can_be_merged)

      expect(subject.mergeable?).to be_truthy
    end

    it 'calls can_be_merged?' do
      allow(subject).to receive(:mergeable_state?) { true }
      allow(subject).to receive(:can_be_merged?) { true }
      expect(subject).to receive(:check_if_can_be_merged)

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

  describe '#mergeable_state?' do
    let(:project) { create(:project) }
522 523 524

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

525 526
    it 'checks if merge request can be merged' do
      allow(subject).to receive(:cannot_be_merged_because_build_is_not_success?) { false }
527 528 529 530 531 532 533 534 535
      expect(subject).to receive(:check_if_can_be_merged)

      subject.mergeable?
    end

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

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

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

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

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

      it 'returns false' do
552
        expect(subject.mergeable_state?).to be_falsey
553 554 555 556 557 558
      end
    end

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

559 560 561 562 563 564 565 566
      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
          allow(subject).to receive(:cannot_be_merged_because_build_is_not_success?) { true }
        end

        it 'returns false' do
          expect(subject.mergeable_state?).to be_falsey
567 568 569 570 571
        end
      end
    end
  end

572
  describe '#cannot_be_merged_because_build_is_not_success?' do
573 574 575 576 577 578 579 580 581 582 583
    let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
    let(:commit_status) { create(:commit_status, status: 'failed', project: project) }
    let(:ci_commit) { create(:ci_empty_pipeline) }

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

    before do
      ci_commit.statuses << commit_status
      allow(subject).to receive(:ci_commit) { ci_commit }
    end

584 585
    it 'returns true if it is only allowed to merge green build and build has been failed' do
      expect(subject.cannot_be_merged_because_build_is_not_success?).to be_truthy
586 587 588 589 590 591 592 593
    end

    context 'when no ci_commit is associated' do
      before do
        allow(subject).to receive(:ci_commit) { nil }
      end

      it 'returns false' do
594
        expect(subject.cannot_be_merged_because_build_is_not_success?).to be_falsey
595 596 597
      end
    end

598
    context 'when is not only allowed to merge green build at project settings' do
599 600 601
      subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) }

      it 'returns false' do
602
        expect(subject.cannot_be_merged_because_build_is_not_success?).to be_falsey
603 604 605
      end
    end
  end
D
Dmitriy Zaporozhets 已提交
606
end